diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.java b/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.java index 294436a..a7c6559 100644 --- a/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.java +++ b/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.java @@ -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 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 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(":");