@ -120,6 +120,9 @@ public class DailyNotificationPlugin extends Plugin {
scheduler = new DailyNotificationScheduler ( getContext ( ) , alarmManager ) ;
fetcher = new DailyNotificationFetcher ( getContext ( ) , storage ) ;
// Check if recovery is needed (app startup recovery)
checkAndPerformRecovery ( ) ;
// Phase 1: Initialize TimeSafari Integration Components
eTagManager = new DailyNotificationETagManager ( storage ) ;
jwtManager = new DailyNotificationJWTManager ( storage , eTagManager ) ;
@ -453,6 +456,9 @@ public class DailyNotificationPlugin extends Plugin {
try {
Log . d ( TAG , "Scheduling daily notification" ) ;
// Ensure storage is initialized
ensureStorageInitialized ( ) ;
// Validate required parameters
String time = call . getString ( "time" ) ;
if ( time = = null | | time . isEmpty ( ) ) {
@ -488,7 +494,8 @@ public class DailyNotificationPlugin extends Plugin {
String priority = call . getString ( "priority" , "default" ) ;
String url = call . getString ( "url" , "" ) ;
// Create notification content
// Create notification content with fresh fetch timestamp
// This represents content that was just fetched, so fetchedAt should be now
NotificationContent content = new NotificationContent ( ) ;
content . setTitle ( title ) ;
content . setBody ( body ) ;
@ -496,6 +503,12 @@ public class DailyNotificationPlugin extends Plugin {
content . setPriority ( priority ) ;
content . setUrl ( url ) ;
content . setScheduledTime ( calculateNextScheduledTime ( hour , minute ) ) ;
content . setScheduledAt ( System . currentTimeMillis ( ) ) ;
// Log the timestamps for debugging
Log . d ( TAG , "Created notification content with fetchedAt=" + content . getFetchedAt ( ) +
", scheduledAt=" + content . getScheduledAt ( ) +
", scheduledTime=" + content . getScheduledTime ( ) ) ;
// Store notification content
storage . saveNotificationContent ( content ) ;
@ -529,6 +542,9 @@ public class DailyNotificationPlugin extends Plugin {
try {
Log . d ( TAG , "Getting last notification" ) ;
// Ensure storage is initialized
ensureStorageInitialized ( ) ;
NotificationContent lastNotification = storage . getLastNotification ( ) ;
if ( lastNotification ! = null ) {
@ -560,6 +576,9 @@ public class DailyNotificationPlugin extends Plugin {
try {
Log . d ( TAG , "Cancelling all notifications" ) ;
// Ensure storage is initialized
ensureStorageInitialized ( ) ;
scheduler . cancelAllNotifications ( ) ;
storage . clearAllNotifications ( ) ;
@ -621,6 +640,9 @@ public class DailyNotificationPlugin extends Plugin {
try {
Log . d ( TAG , "Updating notification settings" ) ;
// Ensure storage is initialized
ensureStorageInitialized ( ) ;
// Extract settings
Boolean sound = call . getBoolean ( "sound" ) ;
String priority = call . getString ( "priority" ) ;
@ -706,6 +728,9 @@ public class DailyNotificationPlugin extends Plugin {
try {
Log . d ( TAG , "Setting adaptive scheduling" ) ;
// Ensure storage is initialized
ensureStorageInitialized ( ) ;
boolean enabled = call . getBoolean ( "enabled" , true ) ;
storage . setAdaptiveSchedulingEnabled ( enabled ) ;
@ -842,6 +867,233 @@ public class DailyNotificationPlugin extends Plugin {
return NotificationManagerCompat . from ( getContext ( ) ) . areNotificationsEnabled ( ) ;
}
/ * *
* Ensure storage is initialized
*
* @throws Exception if storage cannot be initialized
* /
private void ensureStorageInitialized ( ) throws Exception {
if ( storage = = null ) {
Log . w ( TAG , "Storage not initialized, initializing now" ) ;
storage = new DailyNotificationStorage ( getContext ( ) ) ;
if ( storage = = null ) {
throw new Exception ( "Failed to initialize storage" ) ;
}
}
}
/ * *
* Check and perform recovery if needed
* This is called on app startup to recover notifications after reboot
* /
private void checkAndPerformRecovery ( ) {
try {
Log . d ( TAG , "Checking if recovery is needed..." ) ;
// Ensure storage is initialized
ensureStorageInitialized ( ) ;
// Check if we have saved notifications
java . util . List < NotificationContent > notifications = storage . getAllNotifications ( ) ;
if ( notifications . isEmpty ( ) ) {
Log . d ( TAG , "No notifications to recover" ) ;
return ;
}
Log . i ( TAG , "Found " + notifications . size ( ) + " notifications to recover" ) ;
// Check if any alarms are currently scheduled
boolean hasScheduledAlarms = checkScheduledAlarms ( ) ;
if ( ! hasScheduledAlarms ) {
Log . i ( TAG , "No scheduled alarms found - performing recovery" ) ;
performRecovery ( notifications ) ;
} else {
Log . d ( TAG , "Alarms already scheduled - no recovery needed" ) ;
}
} catch ( Exception e ) {
Log . e ( TAG , "Error during recovery check" , e ) ;
}
}
/ * *
* Check if any alarms are currently scheduled
* /
private boolean checkScheduledAlarms ( ) {
try {
// This is a simple check - in a real implementation, you'd check AlarmManager
// For now, we'll assume recovery is needed if we have saved notifications
return false ;
} catch ( Exception e ) {
Log . e ( TAG , "Error checking scheduled alarms" , e ) ;
return false ;
}
}
/ * *
* Perform recovery of scheduled notifications
* /
private void performRecovery ( java . util . List < NotificationContent > notifications ) {
try {
Log . i ( TAG , "Performing notification recovery..." ) ;
int recoveredCount = 0 ;
for ( NotificationContent notification : notifications ) {
try {
// Only reschedule future notifications
if ( notification . getScheduledTime ( ) > System . currentTimeMillis ( ) ) {
boolean scheduled = scheduler . scheduleNotification ( notification ) ;
if ( scheduled ) {
recoveredCount + + ;
Log . d ( TAG , "Recovered notification: " + notification . getId ( ) ) ;
} else {
Log . w ( TAG , "Failed to recover notification: " + notification . getId ( ) ) ;
}
} else {
Log . d ( TAG , "Skipping past notification: " + notification . getId ( ) ) ;
}
} catch ( Exception e ) {
Log . e ( TAG , "Error recovering notification: " + notification . getId ( ) , e ) ;
}
}
Log . i ( TAG , "Notification recovery completed: " + recoveredCount + "/" + notifications . size ( ) + " recovered" ) ;
} catch ( Exception e ) {
Log . e ( TAG , "Error during notification recovery" , e ) ;
}
}
/ * *
* Request notification permissions
*
* @param call Plugin call
* /
@PluginMethod
public void requestNotificationPermissions ( PluginCall call ) {
try {
Log . d ( TAG , "Requesting notification permissions" ) ;
if ( Build . VERSION . SDK_INT > = Build . VERSION_CODES . TIRAMISU ) {
// Request POST_NOTIFICATIONS permission for Android 13+
requestPermissionForAlias ( "notifications" , call , "notificationPermissions" ) ;
} else {
// For older versions, check if notifications are enabled
boolean enabled = NotificationManagerCompat . from ( getContext ( ) ) . areNotificationsEnabled ( ) ;
if ( enabled ) {
Log . i ( TAG , "Notifications already enabled" ) ;
call . resolve ( ) ;
} else {
// Open notification settings
openNotificationSettings ( ) ;
call . resolve ( ) ;
}
}
} catch ( Exception e ) {
Log . e ( TAG , "Error requesting notification permissions" , e ) ;
call . reject ( "Error requesting permissions: " + e . getMessage ( ) ) ;
}
}
/ * *
* Check current permission status
*
* @param call Plugin call
* /
@PluginMethod
public void checkPermissionStatus ( PluginCall call ) {
try {
Log . d ( TAG , "Checking permission status" ) ;
JSObject result = new JSObject ( ) ;
// Check notification permissions
boolean notificationsEnabled = areNotificationsEnabled ( ) ;
result . put ( "notificationsEnabled" , notificationsEnabled ) ;
// Check exact alarm permissions (Android 12+)
boolean exactAlarmEnabled = true ;
if ( Build . VERSION . SDK_INT > = Build . VERSION_CODES . S ) {
exactAlarmEnabled = alarmManager . canScheduleExactAlarms ( ) ;
}
result . put ( "exactAlarmEnabled" , exactAlarmEnabled ) ;
// Check wake lock permissions
boolean wakeLockEnabled = getContext ( ) . checkSelfPermission ( Manifest . permission . WAKE_LOCK )
= = PackageManager . PERMISSION_GRANTED ;
result . put ( "wakeLockEnabled" , wakeLockEnabled ) ;
// Overall status
boolean allPermissionsGranted = notificationsEnabled & & exactAlarmEnabled & & wakeLockEnabled ;
result . put ( "allPermissionsGranted" , allPermissionsGranted ) ;
Log . d ( TAG , "Permission status - Notifications: " + notificationsEnabled +
", Exact Alarm: " + exactAlarmEnabled +
", Wake Lock: " + wakeLockEnabled ) ;
call . resolve ( result ) ;
} catch ( Exception e ) {
Log . e ( TAG , "Error checking permission status" , e ) ;
call . reject ( "Error checking permissions: " + e . getMessage ( ) ) ;
}
}
/ * *
* Open notification settings
* /
private void openNotificationSettings ( ) {
try {
Intent intent = new Intent ( ) ;
intent . setAction ( "android.settings.APP_NOTIFICATION_SETTINGS" ) ;
intent . putExtra ( "android.provider.extra.APP_PACKAGE" , getContext ( ) . getPackageName ( ) ) ;
intent . addFlags ( Intent . FLAG_ACTIVITY_NEW_TASK ) ;
getContext ( ) . startActivity ( intent ) ;
Log . d ( TAG , "Opened notification settings" ) ;
} catch ( Exception e ) {
Log . e ( TAG , "Error opening notification settings" , e ) ;
}
}
/ * *
* Open exact alarm settings ( Android 12 + )
*
* @param call Plugin call
* /
@PluginMethod
public void openExactAlarmSettings ( PluginCall call ) {
try {
Log . d ( TAG , "Opening exact alarm settings" ) ;
if ( Build . VERSION . SDK_INT > = Build . VERSION_CODES . S ) {
// Check if exact alarms are already allowed
if ( alarmManager . canScheduleExactAlarms ( ) ) {
Log . d ( TAG , "Exact alarms already allowed" ) ;
call . resolve ( ) ;
return ;
}
// Open exact alarm settings
Intent intent = new Intent ( android . provider . Settings . ACTION_REQUEST_SCHEDULE_EXACT_ALARM ) ;
intent . setData ( android . net . Uri . parse ( "package:" + getContext ( ) . getPackageName ( ) ) ) ;
intent . addFlags ( Intent . FLAG_ACTIVITY_NEW_TASK ) ;
getContext ( ) . startActivity ( intent ) ;
Log . d ( TAG , "Opened exact alarm settings" ) ;
} else {
Log . d ( TAG , "Exact alarm settings not needed on this Android version" ) ;
}
call . resolve ( ) ;
} catch ( Exception e ) {
Log . e ( TAG , "Error opening exact alarm settings" , e ) ;
call . reject ( "Error opening exact alarm settings: " + e . getMessage ( ) ) ;
}
}
/ * *
* Maintain rolling window ( for testing or manual triggers )
*
@ -947,32 +1199,6 @@ public class DailyNotificationPlugin extends Plugin {
}
}
/ * *
* Open exact alarm settings
*
* @param call Plugin call
* /
@PluginMethod
public void openExactAlarmSettings ( PluginCall call ) {
try {
Log . d ( TAG , "Opening exact alarm settings" ) ;
if ( exactAlarmManager ! = null ) {
boolean success = exactAlarmManager . openExactAlarmSettings ( ) ;
if ( success ) {
call . resolve ( ) ;
} else {
call . reject ( "Failed to open exact alarm settings" ) ;
}
} else {
call . reject ( "Exact alarm manager not initialized" ) ;
}
} catch ( Exception e ) {
Log . e ( TAG , "Error opening exact alarm settings" , e ) ;
call . reject ( "Error opening exact alarm settings: " + e . getMessage ( ) ) ;
}
}
/ * *
* Get reboot recovery status
@ -1653,7 +1879,7 @@ public class DailyNotificationPlugin extends Plugin {
reminderContent . setBody ( body ) ;
reminderContent . setSound ( sound ) ;
reminderContent . setPriority ( priority ) ;
reminderContent . setFetchTime ( System . currentTimeMillis ( ) ) ;
// fetchedAt is set in constructor, no need to set it again
// Calculate next trigger time
Calendar calendar = Calendar . getInstance ( ) ;
@ -1770,7 +1996,7 @@ public class DailyNotificationPlugin extends Plugin {
reminderContent . setBody ( body ) ;
reminderContent . setSound ( sound ! = null ? sound : true ) ;
reminderContent . setPriority ( priority ! = null ? priority : "normal" ) ;
reminderContent . setFetchTime ( System . currentTimeMillis ( ) ) ;
// fetchedAt is set in constructor, no need to set it again
// Calculate next trigger time
String [ ] timeParts = time . split ( ":" ) ;