/** * DailyNotificationPlugin.java * * Android implementation of the Daily Notification Plugin for Capacitor * Implements offline-first daily notifications with prefetch → cache → schedule → display pipeline * * @author Matthew Raymer * @version 1.0.0 */ package com.timesafari.dailynotification; import android.Manifest; import android.app.AlarmManager; import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.database.sqlite.SQLiteDatabase; import android.os.Build; import android.os.PowerManager; import android.util.Log; import androidx.core.app.NotificationCompat; import androidx.work.WorkManager; import com.getcapacitor.JSObject; import com.getcapacitor.Plugin; import com.getcapacitor.PluginCall; import com.getcapacitor.PluginMethod; import com.getcapacitor.annotation.CapacitorPlugin; import com.getcapacitor.annotation.Permission; import java.util.Calendar; import java.util.concurrent.TimeUnit; /** * Main plugin class for handling daily notifications on Android * * This plugin provides functionality for scheduling and managing daily notifications * with offline-first approach, background content fetching, and reliable delivery. */ @CapacitorPlugin( name = "DailyNotification", permissions = { @Permission( alias = "notifications", strings = { Manifest.permission.POST_NOTIFICATIONS, Manifest.permission.SCHEDULE_EXACT_ALARM, Manifest.permission.WAKE_LOCK, Manifest.permission.INTERNET } ) } ) public class DailyNotificationPlugin extends Plugin { private static final String TAG = "DailyNotificationPlugin"; private static final String CHANNEL_ID = "timesafari.daily"; private static final String CHANNEL_NAME = "Daily Notifications"; private static final String CHANNEL_DESCRIPTION = "Daily notification updates from TimeSafari"; private NotificationManager notificationManager; private AlarmManager alarmManager; private WorkManager workManager; private PowerManager powerManager; private DailyNotificationStorage storage; private DailyNotificationScheduler scheduler; private DailyNotificationFetcher fetcher; // SQLite database components private DailyNotificationDatabase database; private DailyNotificationMigration migration; private String databasePath; private boolean useSharedStorage = false; // Rolling window management private DailyNotificationRollingWindow rollingWindow; // Exact alarm management private DailyNotificationExactAlarmManager exactAlarmManager; // Reboot recovery management private DailyNotificationRebootRecoveryManager rebootRecoveryManager; /** * Initialize the plugin and create notification channel */ @Override public void load() { super.load(); try { // Initialize system services notificationManager = (NotificationManager) getContext() .getSystemService(Context.NOTIFICATION_SERVICE); alarmManager = (AlarmManager) getContext() .getSystemService(Context.ALARM_SERVICE); workManager = WorkManager.getInstance(getContext()); powerManager = (PowerManager) getContext() .getSystemService(Context.POWER_SERVICE); // Initialize components storage = new DailyNotificationStorage(getContext()); scheduler = new DailyNotificationScheduler(getContext(), alarmManager); fetcher = new DailyNotificationFetcher(getContext(), storage); // Phase 1: Initialize TimeSafari Integration Components eTagManager = new DailyNotificationETagManager(storage); jwtManager = new DailyNotificationJWTManager(storage, eTagManager); enhancedFetcher = new EnhancedDailyNotificationFetcher(getContext(), storage, eTagManager, jwtManager); // Initialize TTL enforcer and connect to scheduler initializeTTLEnforcer(); // Create notification channel createNotificationChannel(); // Schedule next maintenance scheduleMaintenance(); Log.i(TAG, "DailyNotificationPlugin initialized successfully"); } catch (Exception e) { Log.e(TAG, "Failed to initialize DailyNotificationPlugin", e); } } /** * Configure the plugin with database and storage options * * @param call Plugin call containing configuration parameters */ @PluginMethod public void configure(PluginCall call) { try { Log.d(TAG, "Configuring plugin with new options"); // Get configuration options String dbPath = call.getString("dbPath"); String storageMode = call.getString("storage", "tiered"); Integer ttlSeconds = call.getInt("ttlSeconds"); Integer prefetchLeadMinutes = call.getInt("prefetchLeadMinutes"); Integer maxNotificationsPerDay = call.getInt("maxNotificationsPerDay"); Integer retentionDays = call.getInt("retentionDays"); // Phase 1: Process activeDidIntegration configuration JSObject activeDidConfig = call.getObject("activeDidIntegration"); if (activeDidConfig != null) { configureActiveDidIntegration(activeDidConfig); } // Update storage mode useSharedStorage = "shared".equals(storageMode); // Set database path if (dbPath != null && !dbPath.isEmpty()) { databasePath = dbPath; Log.d(TAG, "Database path set to: " + databasePath); } else { // Use default database path databasePath = getContext().getDatabasePath("daily_notifications.db").getAbsolutePath(); Log.d(TAG, "Using default database path: " + databasePath); } // Initialize SQLite database if using shared storage if (useSharedStorage) { initializeSQLiteDatabase(); } // Store configuration in database or SharedPreferences storeConfiguration(ttlSeconds, prefetchLeadMinutes, maxNotificationsPerDay, retentionDays); Log.i(TAG, "Plugin configuration completed successfully"); call.resolve(); } catch (Exception e) { Log.e(TAG, "Error configuring plugin", e); call.reject("Configuration failed: " + e.getMessage()); } } /** * Initialize SQLite database with migration */ private void initializeSQLiteDatabase() { try { Log.d(TAG, "Initializing SQLite database"); // Create database instance database = new DailyNotificationDatabase(getContext(), databasePath); // Initialize migration utility migration = new DailyNotificationMigration(getContext(), database); // Perform migration if needed if (migration.migrateToSQLite()) { Log.i(TAG, "Migration completed successfully"); // Validate migration if (migration.validateMigration()) { Log.i(TAG, "Migration validation successful"); Log.i(TAG, migration.getMigrationStats()); } else { Log.w(TAG, "Migration validation failed"); } } else { Log.w(TAG, "Migration failed or not needed"); } } catch (Exception e) { Log.e(TAG, "Error initializing SQLite database", e); throw new RuntimeException("SQLite initialization failed", e); } } /** * Store configuration values */ private void storeConfiguration(Integer ttlSeconds, Integer prefetchLeadMinutes, Integer maxNotificationsPerDay, Integer retentionDays) { try { if (useSharedStorage && database != null) { // Store in SQLite storeConfigurationInSQLite(ttlSeconds, prefetchLeadMinutes, maxNotificationsPerDay, retentionDays); } else { // Store in SharedPreferences storeConfigurationInSharedPreferences(ttlSeconds, prefetchLeadMinutes, maxNotificationsPerDay, retentionDays); } } catch (Exception e) { Log.e(TAG, "Error storing configuration", e); } } /** * Store configuration in SQLite database */ private void storeConfigurationInSQLite(Integer ttlSeconds, Integer prefetchLeadMinutes, Integer maxNotificationsPerDay, Integer retentionDays) { try { SQLiteDatabase db = database.getWritableDatabase(); // Store each configuration value if (ttlSeconds != null) { storeConfigValue(db, "ttlSeconds", String.valueOf(ttlSeconds)); } if (prefetchLeadMinutes != null) { storeConfigValue(db, "prefetchLeadMinutes", String.valueOf(prefetchLeadMinutes)); } if (maxNotificationsPerDay != null) { storeConfigValue(db, "maxNotificationsPerDay", String.valueOf(maxNotificationsPerDay)); } if (retentionDays != null) { storeConfigValue(db, "retentionDays", String.valueOf(retentionDays)); } Log.d(TAG, "Configuration stored in SQLite"); } catch (Exception e) { Log.e(TAG, "Error storing configuration in SQLite", e); } } /** * Store a single configuration value in SQLite */ private void storeConfigValue(SQLiteDatabase db, String key, String value) { ContentValues values = new ContentValues(); values.put(DailyNotificationDatabase.COL_CONFIG_K, key); values.put(DailyNotificationDatabase.COL_CONFIG_V, value); // Use INSERT OR REPLACE to handle updates db.replace(DailyNotificationDatabase.TABLE_NOTIF_CONFIG, null, values); } /** * Store configuration in SharedPreferences */ private void storeConfigurationInSharedPreferences(Integer ttlSeconds, Integer prefetchLeadMinutes, Integer maxNotificationsPerDay, Integer retentionDays) { try { SharedPreferences prefs = getContext().getSharedPreferences("DailyNotificationPrefs", Context.MODE_PRIVATE); SharedPreferences.Editor editor = prefs.edit(); if (ttlSeconds != null) { editor.putInt("ttlSeconds", ttlSeconds); } if (prefetchLeadMinutes != null) { editor.putInt("prefetchLeadMinutes", prefetchLeadMinutes); } if (maxNotificationsPerDay != null) { editor.putInt("maxNotificationsPerDay", maxNotificationsPerDay); } if (retentionDays != null) { editor.putInt("retentionDays", retentionDays); } editor.apply(); Log.d(TAG, "Configuration stored in SharedPreferences"); } catch (Exception e) { Log.e(TAG, "Error storing configuration in SharedPreferences", e); } } /** * Initialize TTL enforcer and connect to scheduler */ private void initializeTTLEnforcer() { try { Log.d(TAG, "Initializing TTL enforcer"); // Create TTL enforcer with current storage mode DailyNotificationTTLEnforcer ttlEnforcer = new DailyNotificationTTLEnforcer( getContext(), database, useSharedStorage ); // Connect to scheduler scheduler.setTTLEnforcer(ttlEnforcer); // Initialize rolling window initializeRollingWindow(ttlEnforcer); Log.i(TAG, "TTL enforcer initialized and connected to scheduler"); } catch (Exception e) { Log.e(TAG, "Error initializing TTL enforcer", e); } } /** * Initialize rolling window manager */ private void initializeRollingWindow(DailyNotificationTTLEnforcer ttlEnforcer) { try { Log.d(TAG, "Initializing rolling window manager"); // Detect platform (Android vs iOS) boolean isIOSPlatform = false; // TODO: Implement platform detection // Create rolling window manager rollingWindow = new DailyNotificationRollingWindow( getContext(), scheduler, ttlEnforcer, storage, isIOSPlatform ); // Initialize exact alarm manager initializeExactAlarmManager(); // Initialize reboot recovery manager initializeRebootRecoveryManager(); // Start initial window maintenance rollingWindow.maintainRollingWindow(); Log.i(TAG, "Rolling window manager initialized"); } catch (Exception e) { Log.e(TAG, "Error initializing rolling window manager", e); } } /** * Initialize exact alarm manager */ private void initializeExactAlarmManager() { try { Log.d(TAG, "Initializing exact alarm manager"); // Create exact alarm manager exactAlarmManager = new DailyNotificationExactAlarmManager( getContext(), alarmManager, scheduler ); // Connect to scheduler scheduler.setExactAlarmManager(exactAlarmManager); Log.i(TAG, "Exact alarm manager initialized"); } catch (Exception e) { Log.e(TAG, "Error initializing exact alarm manager", e); } } /** * Initialize reboot recovery manager */ private void initializeRebootRecoveryManager() { try { Log.d(TAG, "Initializing reboot recovery manager"); // Create reboot recovery manager rebootRecoveryManager = new DailyNotificationRebootRecoveryManager( getContext(), scheduler, exactAlarmManager, rollingWindow ); // Register broadcast receivers rebootRecoveryManager.registerReceivers(); Log.i(TAG, "Reboot recovery manager initialized"); } catch (Exception e) { Log.e(TAG, "Error initializing reboot recovery manager", e); } } /** * Schedule a daily notification with the specified options * * @param call Plugin call containing notification parameters */ @PluginMethod public void scheduleDailyNotification(PluginCall call) { try { Log.d(TAG, "Scheduling daily notification"); // Validate required parameters String time = call.getString("time"); if (time == null || time.isEmpty()) { call.reject("Time parameter is required"); return; } // Parse time (HH:mm format) String[] timeParts = time.split(":"); if (timeParts.length != 2) { call.reject("Invalid time format. Use HH:mm"); return; } int hour, minute; try { hour = Integer.parseInt(timeParts[0]); minute = Integer.parseInt(timeParts[1]); } catch (NumberFormatException e) { call.reject("Invalid time format. Use HH:mm"); return; } if (hour < 0 || hour > 23 || minute < 0 || minute > 59) { call.reject("Invalid time values"); return; } // Extract other parameters String title = call.getString("title", "Daily Update"); String body = call.getString("body", "Your daily notification is ready"); boolean sound = call.getBoolean("sound", true); String priority = call.getString("priority", "default"); String url = call.getString("url", ""); // Create notification content NotificationContent content = new NotificationContent(); content.setTitle(title); content.setBody(body); content.setSound(sound); content.setPriority(priority); content.setUrl(url); content.setScheduledTime(calculateNextScheduledTime(hour, minute)); // Store notification content storage.saveNotificationContent(content); // Schedule the notification boolean scheduled = scheduler.scheduleNotification(content); if (scheduled) { // Schedule background fetch for next day scheduleBackgroundFetch(content.getScheduledTime()); Log.i(TAG, "Daily notification scheduled successfully for " + time); call.resolve(); } else { call.reject("Failed to schedule notification"); } } catch (Exception e) { Log.e(TAG, "Error scheduling daily notification", e); call.reject("Internal error: " + e.getMessage()); } } /** * Get the last notification that was delivered * * @param call Plugin call */ @PluginMethod public void getLastNotification(PluginCall call) { try { Log.d(TAG, "Getting last notification"); NotificationContent lastNotification = storage.getLastNotification(); if (lastNotification != null) { JSObject result = new JSObject(); result.put("id", lastNotification.getId()); result.put("title", lastNotification.getTitle()); result.put("body", lastNotification.getBody()); result.put("timestamp", lastNotification.getScheduledTime()); result.put("url", lastNotification.getUrl()); call.resolve(result); } else { call.resolve(null); } } catch (Exception e) { Log.e(TAG, "Error getting last notification", e); call.reject("Internal error: " + e.getMessage()); } } /** * Cancel all scheduled notifications * * @param call Plugin call */ @PluginMethod public void cancelAllNotifications(PluginCall call) { try { Log.d(TAG, "Cancelling all notifications"); scheduler.cancelAllNotifications(); storage.clearAllNotifications(); Log.i(TAG, "All notifications cancelled successfully"); call.resolve(); } catch (Exception e) { Log.e(TAG, "Error cancelling notifications", e); call.reject("Internal error: " + e.getMessage()); } } /** * Get the current status of notifications * * @param call Plugin call */ @PluginMethod public void getNotificationStatus(PluginCall call) { try { Log.d(TAG, "Getting notification status"); JSObject result = new JSObject(); // Check if notifications are enabled boolean notificationsEnabled = areNotificationsEnabled(); result.put("isEnabled", notificationsEnabled); // Get next notification time long nextNotificationTime = scheduler.getNextNotificationTime(); result.put("nextNotificationTime", nextNotificationTime); // Get current settings JSObject settings = new JSObject(); settings.put("sound", true); settings.put("priority", "default"); settings.put("timezone", "UTC"); result.put("settings", settings); // Get pending notifications count int pendingCount = scheduler.getPendingNotificationsCount(); result.put("pending", pendingCount); call.resolve(result); } catch (Exception e) { Log.e(TAG, "Error getting notification status", e); call.reject("Internal error: " + e.getMessage()); } } /** * Update notification settings * * @param call Plugin call containing new settings */ @PluginMethod public void updateSettings(PluginCall call) { try { Log.d(TAG, "Updating notification settings"); // Extract settings Boolean sound = call.getBoolean("sound"); String priority = call.getString("priority"); String timezone = call.getString("timezone"); // Update settings in storage if (sound != null) { storage.setSoundEnabled(sound); } if (priority != null) { storage.setPriority(priority); } if (timezone != null) { storage.setTimezone(timezone); } // Update existing notifications with new settings scheduler.updateNotificationSettings(); Log.i(TAG, "Notification settings updated successfully"); call.resolve(); } catch (Exception e) { Log.e(TAG, "Error updating notification settings", e); call.reject("Internal error: " + e.getMessage()); } } /** * Get battery status information * * @param call Plugin call */ @PluginMethod public void getBatteryStatus(PluginCall call) { try { Log.d(TAG, "Getting battery status"); JSObject result = new JSObject(); // Get battery level (simplified - would need BatteryManager in real implementation) result.put("level", 100); // Placeholder result.put("isCharging", false); // Placeholder result.put("powerState", 0); // Placeholder result.put("isOptimizationExempt", false); // Placeholder call.resolve(result); } catch (Exception e) { Log.e(TAG, "Error getting battery status", e); call.reject("Internal error: " + e.getMessage()); } } /** * Request battery optimization exemption * * @param call Plugin call */ @PluginMethod public void requestBatteryOptimizationExemption(PluginCall call) { try { Log.d(TAG, "Requesting battery optimization exemption"); // This would typically open system settings // For now, just log the request Log.i(TAG, "Battery optimization exemption requested"); call.resolve(); } catch (Exception e) { Log.e(TAG, "Error requesting battery optimization exemption", e); call.reject("Internal error: " + e.getMessage()); } } /** * Set adaptive scheduling based on device state * * @param call Plugin call containing enabled flag */ @PluginMethod public void setAdaptiveScheduling(PluginCall call) { try { Log.d(TAG, "Setting adaptive scheduling"); boolean enabled = call.getBoolean("enabled", true); storage.setAdaptiveSchedulingEnabled(enabled); if (enabled) { scheduler.enableAdaptiveScheduling(); } else { scheduler.disableAdaptiveScheduling(); } Log.i(TAG, "Adaptive scheduling " + (enabled ? "enabled" : "disabled")); call.resolve(); } catch (Exception e) { Log.e(TAG, "Error setting adaptive scheduling", e); call.reject("Internal error: " + e.getMessage()); } } /** * Get current power state information * * @param call Plugin call */ @PluginMethod public void getPowerState(PluginCall call) { try { Log.d(TAG, "Getting power state"); JSObject result = new JSObject(); result.put("powerState", 0); // Placeholder result.put("isOptimizationExempt", false); // Placeholder call.resolve(result); } catch (Exception e) { Log.e(TAG, "Error getting power state", e); call.reject("Internal error: " + e.getMessage()); } } /** * Create the notification channel for Android 8.0+ */ private void createNotificationChannel() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { NotificationChannel channel = new NotificationChannel( CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH ); channel.setDescription(CHANNEL_DESCRIPTION); channel.enableLights(true); channel.enableVibration(true); notificationManager.createNotificationChannel(channel); Log.d(TAG, "Notification channel created: " + CHANNEL_ID); } } /** * Calculate the next scheduled time for the notification * * @param hour Hour of day (0-23) * @param minute Minute of hour (0-59) * @return Timestamp in milliseconds */ private long calculateNextScheduledTime(int hour, int minute) { Calendar calendar = Calendar.getInstance(); calendar.set(Calendar.HOUR_OF_DAY, hour); calendar.set(Calendar.MINUTE, minute); calendar.set(Calendar.SECOND, 0); calendar.set(Calendar.MILLISECOND, 0); // If time has passed today, schedule for tomorrow if (calendar.getTimeInMillis() <= System.currentTimeMillis()) { calendar.add(Calendar.DAY_OF_YEAR, 1); } return calendar.getTimeInMillis(); } /** * Schedule background fetch for content * * @param scheduledTime When the notification is scheduled for */ private void scheduleBackgroundFetch(long scheduledTime) { try { // Schedule fetch 1 hour before notification long fetchTime = scheduledTime - TimeUnit.HOURS.toMillis(1); if (fetchTime > System.currentTimeMillis()) { fetcher.scheduleFetch(fetchTime); Log.d(TAG, "Background fetch scheduled for " + fetchTime); } } catch (Exception e) { Log.e(TAG, "Error scheduling background fetch", e); } } /** * Schedule maintenance tasks */ private void scheduleMaintenance() { try { // Schedule daily maintenance at 2 AM Calendar calendar = Calendar.getInstance(); calendar.set(Calendar.HOUR_OF_DAY, 2); calendar.set(Calendar.MINUTE, 0); calendar.set(Calendar.SECOND, 0); if (calendar.getTimeInMillis() <= System.currentTimeMillis()) { calendar.add(Calendar.DAY_OF_YEAR, 1); } // This would typically use WorkManager for maintenance Log.d(TAG, "Maintenance scheduled for " + calendar.getTimeInMillis()); } catch (Exception e) { Log.e(TAG, "Error scheduling maintenance", e); } } /** * Check if notifications are enabled * * @return true if notifications are enabled */ private boolean areNotificationsEnabled() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { return getContext().checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED; } return NotificationManagerCompat.from(getContext()).areNotificationsEnabled(); } /** * Maintain rolling window (for testing or manual triggers) * * @param call Plugin call */ @PluginMethod public void maintainRollingWindow(PluginCall call) { try { Log.d(TAG, "Manual rolling window maintenance requested"); if (rollingWindow != null) { rollingWindow.forceMaintenance(); call.resolve(); } else { call.reject("Rolling window not initialized"); } } catch (Exception e) { Log.e(TAG, "Error during manual rolling window maintenance", e); call.reject("Error maintaining rolling window: " + e.getMessage()); } } /** * Get rolling window statistics * * @param call Plugin call */ @PluginMethod public void getRollingWindowStats(PluginCall call) { try { Log.d(TAG, "Rolling window stats requested"); if (rollingWindow != null) { String stats = rollingWindow.getRollingWindowStats(); JSObject result = new JSObject(); result.put("stats", stats); result.put("maintenanceNeeded", rollingWindow.isMaintenanceNeeded()); result.put("timeUntilNextMaintenance", rollingWindow.getTimeUntilNextMaintenance()); call.resolve(result); } else { call.reject("Rolling window not initialized"); } } catch (Exception e) { Log.e(TAG, "Error getting rolling window stats", e); call.reject("Error getting rolling window stats: " + e.getMessage()); } } /** * Get exact alarm status * * @param call Plugin call */ @PluginMethod public void getExactAlarmStatus(PluginCall call) { try { Log.d(TAG, "Exact alarm status requested"); if (exactAlarmManager != null) { DailyNotificationExactAlarmManager.ExactAlarmStatus status = exactAlarmManager.getExactAlarmStatus(); JSObject result = new JSObject(); result.put("supported", status.supported); result.put("enabled", status.enabled); result.put("canSchedule", status.canSchedule); result.put("fallbackWindow", status.fallbackWindow.description); call.resolve(result); } else { call.reject("Exact alarm manager not initialized"); } } catch (Exception e) { Log.e(TAG, "Error getting exact alarm status", e); call.reject("Error getting exact alarm status: " + e.getMessage()); } } /** * Request exact alarm permission * * @param call Plugin call */ @PluginMethod public void requestExactAlarmPermission(PluginCall call) { try { Log.d(TAG, "Exact alarm permission request"); if (exactAlarmManager != null) { boolean success = exactAlarmManager.requestExactAlarmPermission(); if (success) { call.resolve(); } else { call.reject("Failed to request exact alarm permission"); } } else { call.reject("Exact alarm manager not initialized"); } } catch (Exception e) { Log.e(TAG, "Error requesting exact alarm permission", e); call.reject("Error requesting exact alarm permission: " + e.getMessage()); } } /** * 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 * * @param call Plugin call */ @PluginMethod public void getRebootRecoveryStatus(PluginCall call) { try { Log.d(TAG, "Reboot recovery status requested"); if (rebootRecoveryManager != null) { DailyNotificationRebootRecoveryManager.RecoveryStatus status = rebootRecoveryManager.getRecoveryStatus(); JSObject result = new JSObject(); result.put("inProgress", status.inProgress); result.put("lastRecoveryTime", status.lastRecoveryTime); result.put("timeSinceLastRecovery", status.timeSinceLastRecovery); result.put("recoveryNeeded", rebootRecoveryManager.isRecoveryNeeded()); call.resolve(result); } else { call.reject("Reboot recovery manager not initialized"); } } catch (Exception e) { Log.e(TAG, "Error getting reboot recovery status", e); call.reject("Error getting reboot recovery status: " + e.getMessage()); } } // MARK: - Phase 1: TimeSafari Integration Methods /** * Configure activeDid integration options * * @param config Configuration object with platform and storage type */ private void configureActiveDidIntegration(JSObject config) { try { Log.d(TAG, "Configuring Phase 2 activeDid integration"); String platform = config.getString("platform", "android"); String storageType = config.getString("storageType", "plugin-managed"); Integer jwtExpirationSeconds = config.getInteger("jwtExpirationSeconds", 60); String apiServer = config.getString("apiServer"); // Phase 2: Host-provided activeDid initial configuration String initialActiveDid = config.getString("activeDid"); boolean autoSync = config.getBoolean("autoSync", false); Integer identityChangeGraceSeconds = config.getInteger("identityChangeGraceSeconds", 30); Log.d(TAG, "Phase 2 ActiveDid config - Platform: " + platform + ", Storage: " + storageType + ", JWT Expiry: " + jwtExpirationSeconds + "s" + ", API Server: " + apiServer + ", Initial ActiveDid: " + (initialActiveDid != null ? initialActiveDid.substring(0, Math.min(20, initialActiveDid.length())) + "..." : "null") + ", AutoSync: " + autoSync + ", Grace Period: " + identityChangeGraceSeconds + "s"); // Phase 2: Configure JWT manager with auto-sync capabilities if (jwtManager != null) { if (initialActiveDid != null && !initialActiveDid.isEmpty()) { jwtManager.setActiveDid(initialActiveDid, jwtExpirationSeconds); Log.d(TAG, "Phase 2: Initial ActiveDid set in JWT manager"); } Log.d(TAG, "Phase 2: JWT manager configured with auto-sync: " + autoSync); } // Phase 2: Configure enhanced fetcher with TimeSafari API support if (enhancedFetcher != null && apiServer != null && !apiServer.isEmpty()) { enhancedFetcher.setApiServerUrl(apiServer); Log.d(TAG, "Phase 2: Enhanced fetcher configured with API server: " + apiServer); // Phase 2: Set up TimeSafari-specific configuration if (initialActiveDid != null && !initialActiveDid.isEmpty()) { EnhancedDailyNotificationFetcher.TimeSafariUserConfig userConfig = new EnhancedDailyNotificationFetcher.TimeSafariUserConfig(); userConfig.activeDid = initialActiveDid; userConfig.fetchOffersToPerson = true; userConfig.fetchOffersToProjects = true; userConfig.fetchProjectUpdates = true; Log.d(TAG, "Phase 2: TimeSafari user configuration prepared"); } } // Phase 2: Store auto-sync configuration for future use storeAutoSyncConfiguration(autoSync, identityChangeGraceSeconds); Log.i(TAG, "Phase 2 ActiveDid integration configured successfully"); } catch (Exception e) { Log.e(TAG, "Error configuring Phase 2 activeDid integration", e); throw e; } } /** * Store auto-sync configuration for background tasks */ private void storeAutoSyncConfiguration(boolean autoSync, int gracePeriodSeconds) { try { if (storage != null) { // Store auto-sync settings in plugin storage Map syncConfig = new HashMap<>(); syncConfig.put("autoSync", autoSync); syncConfig.put("gracePeriodSeconds", gracePeriodSeconds); syncConfig.put("configuredAt", System.currentTimeMillis()); // Store in SharedPreferences for persistence android.content.SharedPreferences preferences = getContext() .getSharedPreferences("daily_notification_timesafari", Context.MODE_PRIVATE); preferences.edit() .putBoolean("autoSync", autoSync) .putInt("gracePeriodSeconds", gracePeriodSeconds) .putLong("configuredAt", System.currentTimeMillis()) .apply(); Log.d(TAG, "Phase 2: Auto-sync configuration stored"); } } catch (Exception e) { Log.e(TAG, "Error storing auto-sync configuration", e); } } /** * Set active DID from host application * * This implements the Option A pattern where the host provides activeDid */ @PluginMethod public void setActiveDidFromHost(PluginCall call) { try { Log.d(TAG, "Setting activeDid from host"); String activeDid = call.getString("activeDid"); if (activeDid == null || activeDid.isEmpty()) { call.reject("activeDid cannot be null or empty"); return; } // Set activeDid in JWT manager if (jwtManager != null) { jwtManager.setActiveDid(activeDid); Log.d(TAG, "ActiveDid set in JWT manager: " + activeDid); } Log.i(TAG, "ActiveDid set successfully from host"); call.resolve(); } catch (Exception e) { Log.e(TAG, "Error setting activeDid from host", e); call.reject("Error setting activeDid: " + e.getMessage()); } } /** * Refresh authentication for new identity */ @PluginMethod public void refreshAuthenticationForNewIdentity(PluginCall call) { try { Log.d(TAG, "Refreshing authentication for new identity"); String activeDid = call.getString("activeDid"); if (activeDid == null || activeDid.isEmpty()) { call.reject("activeDid cannot be null or empty"); return; } // Refresh JWT with new activeDid if (jwtManager != null) { jwtManager.setActiveDid(activeDid); Log.d(TAG, "Authentication refreshed for activeDid: " + activeDid); } Log.i(TAG, "Authentication refreshed successfully"); call.resolve(); } catch (Exception e) { Log.e(TAG, "Error refreshing authentication", e); call.reject("Error refreshing authentication: " + e.getMessage()); } } /** * Clear cached content for new identity */ @PluginMethod public void clearCacheForNewIdentity(PluginCall call) { try { Log.d(TAG, "Clearing cache for new identity"); // Clear content cache if (storage != null) { storage.clearAllContent(); Log.d(TAG, "Content cache cleared"); } // Clear ETag cache if (eTagManager != null) { eTagManager.clearETags(); Log.d(TAG, "ETag cache cleared"); } // Clear authentication state in JWT manager if (jwtManager != null) { jwtManager.clearAuthentication(); Log.d(TAG, "Authentication state cleared"); } Log.i(TAG, "Cache cleared successfully for new identity"); call.resolve(); } catch (Exception e) { Log.e(TAG, "Error clearing cache for new identity", e); call.reject("Error clearing cache: " + e.getMessage()); } } /** * Update background tasks with new identity */ @PluginMethod public void updateBackgroundTaskIdentity(PluginCall call) { try { Log.d(TAG, "Updating background tasks with new identity"); String activeDid = call.getString("activeDid"); if (activeDid == null || activeDid.isEmpty()) { call.reject("activeDid cannot be null or empty"); return; } // For Phase 1, this mainly updates the JWT manager // Future phases will restart background WorkManager tasks if (jwtManager != null) { jwtManager.setActiveDid(activeDid); Log.d(TAG, "Background task identity updated to: " + activeDid); } Log.i(TAG, "Background tasks updated successfully"); call.resolve(); } catch (Exception e) { Log.e(TAG, "Error updating background tasks", e); call.reject("Error updating background tasks: " + e.getMessage()); } } /** * Test JWT generation for debugging */ @PluginMethod public void testJWTGeneration(PluginCall call) { try { Log.d(TAG, "Testing JWT generation"); String activeDid = call.getString("activeDid", "did:example:test"); if (jwtManager != null) { jwtManager.setActiveDid(activeDid); String token = jwtManager.getCurrentJWTToken(); String debugInfo = jwtManager.getTokenDebugInfo(); JSObject result = new JSObject(); result.put("success", true); result.put("activeDid", activeDid); result.put("tokenLength", token != null ? token.length() : 0); result.put("debugInfo", debugInfo); result.put("authenticated", jwtManager.isAuthenticated()); Log.d(TAG, "JWT test completed successfully"); call.resolve(result); } else { call.reject("JWT manager not initialized"); } } catch (Exception e) { Log.e(TAG, "Error testing JWT generation", e); call.reject("JWT test failed: " + e.getMessage()); } } /** * Test Endorser.ch API calls */ @PluginMethod public void testEndorserAPI(PluginCall call) { try { Log.d(TAG, "Testing Endorser.ch API calls"); String activeDid = call.getString("activeDid", "did:example:test"); String apiServer = call.getString("apiServer", "https://api.endorser.ch"); if (enhancedFetcher != null) { // Set up test configuration enhancedFetcher.setApiServerUrl(apiServer); EnhancedDailyNotificationFetcher.TimeSafariUserConfig userConfig = new EnhancedDailyNotificationFetcher.TimeSafariUserConfig(); userConfig.activeDid = activeDid; userConfig.fetchOffersToPerson = true; userConfig.fetchOffersToProjects = true; userConfig.fetchProjectUpdates = true; // Execute test fetch CompletableFuture future = enhancedFetcher.fetchAllTimeSafariData(userConfig); // For immediate testing, we'll create a simple response JSObject result = new JSObject(); result.put("success", true); result.put("activeDid", activeDid); result.put("apiServer", apiServer); result.put("testCompleted", true); result.put("message", "Endorser.ch API test initiated successfully"); Log.d(TAG, "Endorser.ch API test completed successfully"); call.resolve(result); } else { call.reject("Enhanced fetcher not initialized"); } } catch (Exception e) { Log.e(TAG, "Error testing Endorser.ch API", e); call.reject("Endorser.ch API test failed: " + e.getMessage()); } } // MARK: - Phase 3: TimeSafari Background Coordination Methods /** * Phase 3: Coordinate background tasks with TimeSafari PlatformServiceMixin */ @PluginMethod public void coordinateBackgroundTasks(PluginCall call) { try { Log.d(TAG, "Phase 3: Coordinating background tasks with PlatformServiceMixin"); if (scheduler != null) { scheduler.coordinateWithPlatformServiceMixin(); // Schedule enhanced WorkManager jobs with coordination scheduleCoordinatedBackgroundJobs(); Log.i(TAG, "Phase 3: Background task coordination completed"); call.resolve(); } else { call.reject("Scheduler not initialized"); } } catch (Exception e) { Log.e(TAG, "Phase 3: Error coordinating background tasks", e); call.reject("Background task coordination failed: " + e.getMessage()); } } /** * Phase 3: Schedule coordinated background jobs */ private void scheduleCoordinatedBackgroundJobs() { try { Log.d(TAG, "Phase 3: Scheduling coordinated background jobs"); // Create coordinated WorkManager job with TimeSafari awareness androidx.work.Data inputData = new androidx.work.Data.Builder() .putBoolean("timesafari_coordination", true) .putLong("coordination_timestamp", System.currentTimeMillis()) .putString("active_did_tracking", "enabled") .build(); androidx.work.OneTimeWorkRequest coordinatedWork = new androidx.work.OneTimeWorkRequest.Builder(DailyNotificationFetchWorker.class) .setInputData(inputData) .setConstraints(androidx.work.Constraints.Builder() .setRequiresCharging(false) .setRequiresBatteryNotLow(false) .setRequiredNetworkType(androidx.work.NetworkType.CONNECTED) .build()) .addTag("timesafari_coordinated") .addTag("phase3_background") .build(); // Schedule with coordination awareness workManager.enqueueUniqueWork( "tsaf_coordinated_fetch", androidx.work.ExistingWorkPolicy.REPLACE, coordinatedWork ); Log.d(TAG, "Phase 3: Coordinated background job scheduled"); } catch (Exception e) { Log.e(TAG, "Phase 3: Error scheduling coordinated background jobs", e); } } /** * Phase 3: Handle app lifecycle events for TimeSafari coordination */ @PluginMethod public void handleAppLifecycleEvent(PluginCall call) { try { String lifecycleEvent = call.getString("lifecycleEvent"); Log.d(TAG, "Phase 3: Handling app lifecycle event: " + lifecycleEvent); if (lifecycleEvent == null) { call.reject("lifecycleEvent parameter required"); return; } switch (lifecycleEvent) { case "app_background": handleAppBackgrounded(); break; case "app_foreground": handleAppForegrounded(); break; case "app_resumed": handleAppResumed(); break; case "app_paused": handleAppPaused(); break; default: Log.w(TAG, "Phase 3: Unknown lifecycle event: " + lifecycleEvent); break; } call.resolve(); } catch (Exception e) { Log.e(TAG, "Phase 3: Error handling app lifecycle event", e); call.reject("App lifecycle event handling failed: " + e.getMessage()); } } /** * Phase 3: Handle app backgrounded event */ private void handleAppBackgrounded() { try { Log.d(TAG, "Phase 3: App backgrounded - activating TimeSafari coordination"); // Activate enhanced background execution if (scheduler != null) { scheduler.coordinateWithPlatformServiceMixin(); } // Store app state for coordination android.content.SharedPreferences prefs = getContext() .getSharedPreferences("daily_notification_timesafari", Context.MODE_PRIVATE); prefs.edit() .putLong("lastAppBackgrounded", System.currentTimeMillis()) .putBoolean("isAppBackgrounded", true) .apply(); Log.d(TAG, "Phase 3: App backgrounded coordination completed"); } catch (Exception e) { Log.e(TAG, "Phase 3: Error handling app backgrounded", e); } } /** * Phase 3: Handle app foregrounded event */ private void handleAppForegrounded() { try { Log.d(TAG, "Phase 3: App foregrounded - updating TimeSafari coordination"); // Update coordination state android.content.SharedPreferences prefs = getContext() .getSharedPreferences("daily_notification_timesafari", Context.MODE_PRIVATE); prefs.edit() .putLong("lastAppForegrounded", System.currentTimeMillis()) .putBoolean("isAppBackgrounded", false) .apply(); // Check if activeDid coordination is needed checkActiveDidCoordination(); Log.d(TAG, "Phase 3: App foregrounded coordination completed"); } catch (Exception e) { Log.e(TAG, "Phase 3: Error handling app foregrounded", e); } } /** * Phase 3: Handle app resumed event */ private void handleAppResumed() { try { Log.d(TAG, "Phase 3: App resumed - syncing TimeSafari state"); // Sync state with resumed app syncTimeSafariState(); Log.d(TAG, "Phase 3: App resumed coordination completed"); } catch (Exception e) { Log.e(TAG, "Phase 3: Error handling app resumed", e); } } /** * Phase 3: Handle app paused event */ private void handleAppPaused() { try { Log.d(TAG, "Phase 3: App paused - pausing TimeSafari coordination"); // Pause non-critical coordination pauseTimeSafariCoordination(); Log.d(TAG, "Phase 3: App paused coordination completed"); } catch (Exception e) { Log.e(TAG, "Phase 3: Error handling app paused"); } } /** * Phase 3: Check if activeDid coordination is needed */ private void checkActiveDidCoordination() { try { android.content.SharedPreferences prefs = getContext() .getSharedPreferences( "daily_notification_timesafari", Context.MODE_PRIVATE); long lastActiveDidChange = prefs.getLong("lastActiveDidChange", 0); long lastAppForegrounded = prefs.getLong("lastAppForegrounded", 0); // If activeDid changed while app was backgrounded, update background tasks if (lastActiveDidChange > lastAppForegrounded) { Log.d(TAG, "Phase 3: ActiveDid changed while backgrounded - updating background tasks"); // Update background tasks with new activeDid if (jwtManager != null) { String currentActiveDid = jwtManager.getCurrentActiveDid(); if (currentActiveDid != null && !currentActiveDid.isEmpty()) { Log.d(TAG, "Phase 3: Updating background tasks for activeDid: " + currentActiveDid); // Background task update would happen here } } } } catch (Exception e) { Log.e(TAG, "Phase 3: Error checking activeDid coordination", e); } } /** * Phase 3: Sync TimeSafari state after app resume */ private void syncTimeSafariState() { try { Log.d(TAG, "Phase 3: Syncing TimeSafari state"); // Sync authentication state if (jwtManager != null) { jwtManager.refreshJWTIfNeeded(); } // Sync notification delivery tracking if (scheduler != null) { // Update notification delivery metrics android.content.SharedPreferences prefs = getContext() .getSharedPreferences("daily_notification_timesafari", Context.MODE_PRIVATE); long lastBackgroundDelivery = prefs.getLong("lastBackgroundDelivered", 0); if (lastBackgroundDelivery > 0) { String lastDeliveredId = prefs.getString("lastBackgroundDeliveredId", ""); scheduler.recordNotificationDelivery(lastDeliveredId); Log.d(TAG, "Phase 3: Synced background delivery: " + lastDeliveredId); } } Log.d(TAG, "Phase 3: TimeSafari state sync completed"); } catch (Exception e) { Log.e(TAG, "Phase 3: Error syncing TimeSafari state", e); } } /** * Phase 3: Pause TimeSafari coordination when app paused */ private void pauseTimeSafariCoordination() { try { Log.d(TAG, "Phase 3: Pausing TimeSafari coordination"); // Mark coordination as paused android.content.SharedPreferences prefs = getContext() .getSharedPreferences("daily_notification_timesafari", Context.MODE_PRIVATE); prefs.edit() .putLong("lastCoordinationPaused", System.currentTimeMillis()) .putBoolean("coordinationPaused", true) .apply(); Log.d(TAG, "Phase 3: TimeSafari coordination paused"); } catch (Exception e) { Log.e(TAG, "Phase 3: Error pausing TimeSafari coordination", e); } } /** * Phase 3: Get coordination status for debugging */ @PluginMethod public void getCoordinationStatus(PluginCall call) { try { Log.d(TAG, "Phase 3: Getting coordination status"); android.content.SharedPreferences prefs = getContext() .getSharedPreferences("daily_notification_timesafari", Context.MODE_PRIVATE); com.getcapacitor.JSObject status = new com.getcapacitor.JSObject(); status.put("autoSync", prefs.getBoolean("autoSync", false)); status.put("coordinationPaused", prefs.getBoolean("coordinationPaused", false)); status.put("lastBackgroundFetchCoordinated", prefs.getLong("lastBackgroundFetchCoordinated", 0)); status.put("lastActiveDidChange", prefs.getLong("lastActiveDidChange", 0)); status.put("lastAppBackgrounded", prefs.getLong("lastAppBackgrounded", 0)); status.put("lastAppForegrounded", prefs.getLong("lastAppForegrounded", 0)); call.resolve(status); } catch (Exception e) { Log.e(TAG, "Phase 3: Error getting coordination status", e); call.reject("Coordination status retrieval failed: " + e.getMessage()); } } // Static Daily Reminder Methods @PluginMethod public void scheduleDailyReminder(PluginCall call) { try { Log.d(TAG, "Scheduling daily reminder"); // Extract reminder options String id = call.getString("id"); String title = call.getString("title"); String body = call.getString("body"); String time = call.getString("time"); boolean sound = call.getBoolean("sound", true); boolean vibration = call.getBoolean("vibration", true); String priority = call.getString("priority", "normal"); boolean repeatDaily = call.getBoolean("repeatDaily", true); String timezone = call.getString("timezone"); // Validate required parameters if (id == null || title == null || body == null || time == null) { call.reject("Missing required parameters: id, title, body, time"); return; } // Parse time (HH:mm format) String[] timeParts = time.split(":"); if (timeParts.length != 2) { call.reject("Invalid time format. Use HH:mm (e.g., 09:00)"); return; } int hour = Integer.parseInt(timeParts[0]); int minute = Integer.parseInt(timeParts[1]); if (hour < 0 || hour > 23 || minute < 0 || minute > 59) { call.reject("Invalid time values. Hour must be 0-23, minute must be 0-59"); return; } // Create reminder content NotificationContent reminderContent = new NotificationContent(); reminderContent.setId("reminder_" + id); // Prefix to identify as reminder reminderContent.setTitle(title); reminderContent.setBody(body); reminderContent.setSound(sound); reminderContent.setPriority(priority); reminderContent.setFetchTime(System.currentTimeMillis()); // Calculate next trigger time Calendar calendar = Calendar.getInstance(); calendar.set(Calendar.HOUR_OF_DAY, hour); calendar.set(Calendar.MINUTE, minute); calendar.set(Calendar.SECOND, 0); calendar.set(Calendar.MILLISECOND, 0); // If time has passed today, schedule for tomorrow if (calendar.getTimeInMillis() <= System.currentTimeMillis()) { calendar.add(Calendar.DAY_OF_MONTH, 1); } reminderContent.setScheduledTime(calendar.getTimeInMillis()); // Store reminder in database storeReminderInDatabase(id, title, body, time, sound, vibration, priority, repeatDaily, timezone); // Schedule the notification boolean scheduled = scheduler.scheduleNotification(reminderContent); if (scheduled) { Log.i(TAG, "Daily reminder scheduled successfully: " + id); call.resolve(); } else { call.reject("Failed to schedule daily reminder"); } } catch (Exception e) { Log.e(TAG, "Error scheduling daily reminder", e); call.reject("Daily reminder scheduling failed: " + e.getMessage()); } } @PluginMethod public void cancelDailyReminder(PluginCall call) { try { Log.d(TAG, "Cancelling daily reminder"); String reminderId = call.getString("reminderId"); if (reminderId == null) { call.reject("Missing reminderId parameter"); return; } // Cancel the scheduled notification (use prefixed ID) scheduler.cancelNotification("reminder_" + reminderId); // Remove from database removeReminderFromDatabase(reminderId); Log.i(TAG, "Daily reminder cancelled: " + reminderId); call.resolve(); } catch (Exception e) { Log.e(TAG, "Error cancelling daily reminder", e); call.reject("Daily reminder cancellation failed: " + e.getMessage()); } } @PluginMethod public void getScheduledReminders(PluginCall call) { try { Log.d(TAG, "Getting scheduled reminders"); // Get reminders from database java.util.List reminders = getRemindersFromDatabase(); // Convert to JSObject array JSObject result = new JSObject(); result.put("reminders", reminders); call.resolve(result); } catch (Exception e) { Log.e(TAG, "Error getting scheduled reminders", e); call.reject("Failed to get scheduled reminders: " + e.getMessage()); } } @PluginMethod public void updateDailyReminder(PluginCall call) { try { Log.d(TAG, "Updating daily reminder"); String reminderId = call.getString("reminderId"); if (reminderId == null) { call.reject("Missing reminderId parameter"); return; } // Extract updated options String title = call.getString("title"); String body = call.getString("body"); String time = call.getString("time"); Boolean sound = call.getBoolean("sound"); Boolean vibration = call.getBoolean("vibration"); String priority = call.getString("priority"); Boolean repeatDaily = call.getBoolean("repeatDaily"); String timezone = call.getString("timezone"); // Cancel existing reminder (use prefixed ID) scheduler.cancelNotification("reminder_" + reminderId); // Update in database updateReminderInDatabase(reminderId, title, body, time, sound, vibration, priority, repeatDaily, timezone); // Reschedule with new settings if (title != null && body != null && time != null) { // Create new reminder content NotificationContent reminderContent = new NotificationContent(); reminderContent.setId("reminder_" + reminderId); // Prefix to identify as reminder reminderContent.setTitle(title); reminderContent.setBody(body); reminderContent.setSound(sound != null ? sound : true); reminderContent.setPriority(priority != null ? priority : "normal"); reminderContent.setFetchTime(System.currentTimeMillis()); // Calculate next trigger time String[] timeParts = time.split(":"); int hour = Integer.parseInt(timeParts[0]); int minute = Integer.parseInt(timeParts[1]); Calendar calendar = Calendar.getInstance(); calendar.set(Calendar.HOUR_OF_DAY, hour); calendar.set(Calendar.MINUTE, minute); calendar.set(Calendar.SECOND, 0); calendar.set(Calendar.MILLISECOND, 0); if (calendar.getTimeInMillis() <= System.currentTimeMillis()) { calendar.add(Calendar.DAY_OF_MONTH, 1); } reminderContent.setScheduledTime(calendar.getTimeInMillis()); // Schedule the updated notification boolean scheduled = scheduler.scheduleNotification(reminderContent); if (!scheduled) { call.reject("Failed to reschedule updated reminder"); return; } } Log.i(TAG, "Daily reminder updated: " + reminderId); call.resolve(); } catch (Exception e) { Log.e(TAG, "Error updating daily reminder", e); call.reject("Daily reminder update failed: " + e.getMessage()); } } // Helper methods for reminder database operations private void storeReminderInDatabase(String id, String title, String body, String time, boolean sound, boolean vibration, String priority, boolean repeatDaily, String timezone) { try { SharedPreferences prefs = getContext().getSharedPreferences("daily_reminders", Context.MODE_PRIVATE); SharedPreferences.Editor editor = prefs.edit(); editor.putString(id + "_title", title); editor.putString(id + "_body", body); editor.putString(id + "_time", time); editor.putBoolean(id + "_sound", sound); editor.putBoolean(id + "_vibration", vibration); editor.putString(id + "_priority", priority); editor.putBoolean(id + "_repeatDaily", repeatDaily); editor.putString(id + "_timezone", timezone); editor.putLong(id + "_createdAt", System.currentTimeMillis()); editor.putBoolean(id + "_isScheduled", true); editor.apply(); Log.d(TAG, "Reminder stored in database: " + id); } catch (Exception e) { Log.e(TAG, "Error storing reminder in database", e); } } private void removeReminderFromDatabase(String id) { try { SharedPreferences prefs = getContext().getSharedPreferences("daily_reminders", Context.MODE_PRIVATE); SharedPreferences.Editor editor = prefs.edit(); editor.remove(id + "_title"); editor.remove(id + "_body"); editor.remove(id + "_time"); editor.remove(id + "_sound"); editor.remove(id + "_vibration"); editor.remove(id + "_priority"); editor.remove(id + "_repeatDaily"); editor.remove(id + "_timezone"); editor.remove(id + "_createdAt"); editor.remove(id + "_isScheduled"); editor.remove(id + "_lastTriggered"); editor.apply(); Log.d(TAG, "Reminder removed from database: " + id); } catch (Exception e) { Log.e(TAG, "Error removing reminder from database", e); } } private java.util.List getRemindersFromDatabase() { java.util.List reminders = new java.util.ArrayList<>(); try { SharedPreferences prefs = getContext().getSharedPreferences("daily_reminders", Context.MODE_PRIVATE); java.util.Map allEntries = prefs.getAll(); java.util.Set reminderIds = new java.util.HashSet<>(); for (String key : allEntries.keySet()) { if (key.endsWith("_title")) { String id = key.substring(0, key.length() - 6); // Remove "_title" reminderIds.add(id); } } for (String id : reminderIds) { DailyReminderInfo reminder = new DailyReminderInfo(); reminder.id = id; reminder.title = prefs.getString(id + "_title", ""); reminder.body = prefs.getString(id + "_body", ""); reminder.time = prefs.getString(id + "_time", ""); reminder.sound = prefs.getBoolean(id + "_sound", true); reminder.vibration = prefs.getBoolean(id + "_vibration", true); reminder.priority = prefs.getString(id + "_priority", "normal"); reminder.repeatDaily = prefs.getBoolean(id + "_repeatDaily", true); reminder.timezone = prefs.getString(id + "_timezone", null); reminder.isScheduled = prefs.getBoolean(id + "_isScheduled", false); reminder.createdAt = prefs.getLong(id + "_createdAt", 0); reminder.lastTriggered = prefs.getLong(id + "_lastTriggered", 0); // Calculate next trigger time String[] timeParts = reminder.time.split(":"); int hour = Integer.parseInt(timeParts[0]); int minute = Integer.parseInt(timeParts[1]); Calendar calendar = Calendar.getInstance(); calendar.set(Calendar.HOUR_OF_DAY, hour); calendar.set(Calendar.MINUTE, minute); calendar.set(Calendar.SECOND, 0); calendar.set(Calendar.MILLISECOND, 0); if (calendar.getTimeInMillis() <= System.currentTimeMillis()) { calendar.add(Calendar.DAY_OF_MONTH, 1); } reminder.nextTriggerTime = calendar.getTimeInMillis(); reminders.add(reminder); } } catch (Exception e) { Log.e(TAG, "Error getting reminders from database", e); } return reminders; } private void updateReminderInDatabase(String id, String title, String body, String time, Boolean sound, Boolean vibration, String priority, Boolean repeatDaily, String timezone) { try { SharedPreferences prefs = getContext().getSharedPreferences("daily_reminders", Context.MODE_PRIVATE); SharedPreferences.Editor editor = prefs.edit(); if (title != null) editor.putString(id + "_title", title); if (body != null) editor.putString(id + "_body", body); if (time != null) editor.putString(id + "_time", time); if (sound != null) editor.putBoolean(id + "_sound", sound); if (vibration != null) editor.putBoolean(id + "_vibration", vibration); if (priority != null) editor.putString(id + "_priority", priority); if (repeatDaily != null) editor.putBoolean(id + "_repeatDaily", repeatDaily); if (timezone != null) editor.putString(id + "_timezone", timezone); editor.apply(); Log.d(TAG, "Reminder updated in database: " + id); } catch (Exception e) { Log.e(TAG, "Error updating reminder in database", e); } } // Data class for reminder info public static class DailyReminderInfo { public String id; public String title; public String body; public String time; public boolean sound; public boolean vibration; public String priority; public boolean repeatDaily; public String timezone; public boolean isScheduled; public long nextTriggerTime; public long createdAt; public long lastTriggered; } }