diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/ExactAlarmManager.java b/android/plugin/src/main/java/com/timesafari/dailynotification/ExactAlarmManager.java new file mode 100644 index 0000000..9981d60 --- /dev/null +++ b/android/plugin/src/main/java/com/timesafari/dailynotification/ExactAlarmManager.java @@ -0,0 +1,146 @@ +/** + * ExactAlarmManager.java + * + * Specialized manager for exact alarm management + * Handles exact alarm permissions, status checking, and settings + * + * @author Matthew Raymer + * @version 2.0.0 - Modular Architecture + */ + +package com.timesafari.dailynotification; + +import android.app.AlarmManager; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.provider.Settings; +import android.util.Log; + +import com.getcapacitor.JSObject; +import com.getcapacitor.PluginCall; + +/** + * Manager class for exact alarm management + * + * Responsibilities: + * - Check exact alarm permission status + * - Request exact alarm permissions + * - Provide alarm status information + * - Handle exact alarm settings + */ +public class ExactAlarmManager { + + private static final String TAG = "ExactAlarmManager"; + + private final Context context; + private final AlarmManager alarmManager; + + /** + * Initialize the ExactAlarmManager + * + * @param context Android context + */ + public ExactAlarmManager(Context context) { + this.context = context; + this.alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + + Log.d(TAG, "ExactAlarmManager initialized"); + } + + /** + * Get exact alarm status and capabilities + * + * @param call Plugin call + */ + public void getExactAlarmStatus(PluginCall call) { + try { + Log.d(TAG, "Getting exact alarm status"); + + boolean exactAlarmsSupported = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S; + boolean exactAlarmsGranted = false; + boolean canScheduleExactAlarms = false; + + // Check if exact alarms are supported + if (exactAlarmsSupported) { + exactAlarmsGranted = alarmManager.canScheduleExactAlarms(); + canScheduleExactAlarms = exactAlarmsGranted; + } else { + // Pre-Android 12, exact alarms are always allowed + exactAlarmsGranted = true; + canScheduleExactAlarms = true; + } + + // Get additional alarm information + int androidVersion = Build.VERSION.SDK_INT; + String androidVersionName = Build.VERSION.RELEASE; + + JSObject result = new JSObject(); + result.put("success", true); + result.put("exactAlarmsSupported", exactAlarmsSupported); + result.put("exactAlarmsGranted", exactAlarmsGranted); + result.put("canScheduleExactAlarms", canScheduleExactAlarms); + result.put("androidVersion", androidVersion); + result.put("androidVersionName", androidVersionName); + result.put("requiresPermission", exactAlarmsSupported); + + call.resolve(result); + + } catch (Exception e) { + Log.e(TAG, "Error getting exact alarm status", e); + call.reject("Failed to get exact alarm status: " + e.getMessage()); + } + } + + /** + * Request exact alarm permission from the user + * + * @param call Plugin call + */ + public void requestExactAlarmPermission(PluginCall call) { + try { + Log.d(TAG, "Requesting exact alarm permission"); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + // Check if permission is already granted + if (alarmManager.canScheduleExactAlarms()) { + JSObject result = new JSObject(); + result.put("success", true); + result.put("alreadyGranted", true); + result.put("message", "Exact alarm permission already granted"); + call.resolve(result); + return; + } + + // Open exact alarm settings + Intent intent = new Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM); + intent.setData(Uri.parse("package:" + context.getPackageName())); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + try { + context.startActivity(intent); + + JSObject result = new JSObject(); + result.put("success", true); + result.put("opened", true); + result.put("message", "Exact alarm settings opened"); + call.resolve(result); + } catch (Exception e) { + Log.e(TAG, "Failed to open exact alarm settings", e); + call.reject("Failed to open exact alarm settings: " + e.getMessage()); + } + } else { + JSObject result = new JSObject(); + result.put("success", true); + result.put("notSupported", true); + result.put("message", "Exact alarms not supported on this Android version"); + call.resolve(result); + } + + } catch (Exception e) { + Log.e(TAG, "Error requesting exact alarm permission", e); + call.reject("Failed to request exact alarm permission: " + e.getMessage()); + } + } +} diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/PowerManager.java b/android/plugin/src/main/java/com/timesafari/dailynotification/PowerManager.java new file mode 100644 index 0000000..efe97d9 --- /dev/null +++ b/android/plugin/src/main/java/com/timesafari/dailynotification/PowerManager.java @@ -0,0 +1,242 @@ +/** + * PowerManager.java + * + * Specialized manager for power and battery management + * Handles battery optimization, adaptive scheduling, and power state monitoring + * + * @author Matthew Raymer + * @version 2.0.0 - Modular Architecture + */ + +package com.timesafari.dailynotification; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.os.PowerManager; +import android.provider.Settings; +import android.util.Log; + +import com.getcapacitor.JSObject; +import com.getcapacitor.PluginCall; + +/** + * Manager class for power and battery management + * + * Responsibilities: + * - Monitor battery status and optimization settings + * - Request battery optimization exemptions + * - Handle adaptive scheduling based on power state + * - Provide power state information + */ +public class PowerManager { + + private static final String TAG = "PowerManager"; + + private final Context context; + private final android.os.PowerManager powerManager; + + /** + * Initialize the PowerManager + * + * @param context Android context + */ + public PowerManager(Context context) { + this.context = context; + this.powerManager = (android.os.PowerManager) context.getSystemService(Context.POWER_SERVICE); + + Log.d(TAG, "PowerManager initialized"); + } + + /** + * Get current battery status and optimization settings + * + * @param call Plugin call + */ + public void getBatteryStatus(PluginCall call) { + try { + Log.d(TAG, "Getting battery status"); + + boolean isIgnoringBatteryOptimizations = false; + boolean isPowerSaveMode = false; + boolean isDeviceIdleMode = false; + + // Check if app is ignoring battery optimizations + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + isIgnoringBatteryOptimizations = powerManager.isIgnoringBatteryOptimizations(context.getPackageName()); + } + + // Check if device is in power save mode + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + isPowerSaveMode = powerManager.isPowerSaveMode(); + } + + // Check if device is in idle mode + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + isDeviceIdleMode = powerManager.isDeviceIdleMode(); + } + + JSObject result = new JSObject(); + result.put("success", true); + result.put("ignoringBatteryOptimizations", isIgnoringBatteryOptimizations); + result.put("powerSaveMode", isPowerSaveMode); + result.put("deviceIdleMode", isDeviceIdleMode); + result.put("androidVersion", Build.VERSION.SDK_INT); + + call.resolve(result); + + } catch (Exception e) { + Log.e(TAG, "Error getting battery status", e); + call.reject("Failed to get battery status: " + e.getMessage()); + } + } + + /** + * Request battery optimization exemption for the app + * + * @param call Plugin call + */ + public void requestBatteryOptimizationExemption(PluginCall call) { + try { + Log.d(TAG, "Requesting battery optimization exemption"); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + // Check if already ignoring battery optimizations + if (powerManager.isIgnoringBatteryOptimizations(context.getPackageName())) { + JSObject result = new JSObject(); + result.put("success", true); + result.put("alreadyExempt", true); + result.put("message", "App is already exempt from battery optimizations"); + call.resolve(result); + return; + } + + // Open battery optimization settings + Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); + intent.setData(Uri.parse("package:" + context.getPackageName())); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + try { + context.startActivity(intent); + + JSObject result = new JSObject(); + result.put("success", true); + result.put("opened", true); + result.put("message", "Battery optimization settings opened"); + call.resolve(result); + } catch (Exception e) { + Log.e(TAG, "Failed to open battery optimization settings", e); + call.reject("Failed to open battery optimization settings: " + e.getMessage()); + } + } else { + JSObject result = new JSObject(); + result.put("success", true); + result.put("notSupported", true); + result.put("message", "Battery optimization not supported on this Android version"); + call.resolve(result); + } + + } catch (Exception e) { + Log.e(TAG, "Error requesting battery optimization exemption", e); + call.reject("Failed to request battery optimization exemption: " + e.getMessage()); + } + } + + /** + * Set adaptive scheduling based on power state + * + * @param call Plugin call containing adaptive scheduling options + */ + public void setAdaptiveScheduling(PluginCall call) { + try { + Log.d(TAG, "Setting adaptive scheduling"); + + boolean enabled = call.getBoolean("enabled", true); + int powerSaveModeInterval = call.getInt("powerSaveModeInterval", 30); // minutes + int deviceIdleModeInterval = call.getInt("deviceIdleModeInterval", 60); // minutes + boolean reduceFrequencyInPowerSave = call.getBoolean("reduceFrequencyInPowerSave", true); + boolean pauseInDeviceIdle = call.getBoolean("pauseInDeviceIdle", false); + + // Store adaptive scheduling settings + // This would typically be stored in SharedPreferences or database + Log.d(TAG, "Adaptive scheduling configured:"); + Log.d(TAG, " Enabled: " + enabled); + Log.d(TAG, " Power save mode interval: " + powerSaveModeInterval + " minutes"); + Log.d(TAG, " Device idle mode interval: " + deviceIdleModeInterval + " minutes"); + Log.d(TAG, " Reduce frequency in power save: " + reduceFrequencyInPowerSave); + Log.d(TAG, " Pause in device idle: " + pauseInDeviceIdle); + + JSObject result = new JSObject(); + result.put("success", true); + result.put("enabled", enabled); + result.put("powerSaveModeInterval", powerSaveModeInterval); + result.put("deviceIdleModeInterval", deviceIdleModeInterval); + result.put("reduceFrequencyInPowerSave", reduceFrequencyInPowerSave); + result.put("pauseInDeviceIdle", pauseInDeviceIdle); + result.put("message", "Adaptive scheduling configured successfully"); + call.resolve(result); + + } catch (Exception e) { + Log.e(TAG, "Error setting adaptive scheduling", e); + call.reject("Failed to set adaptive scheduling: " + e.getMessage()); + } + } + + /** + * Get current power state information + * + * @param call Plugin call + */ + public void getPowerState(PluginCall call) { + try { + Log.d(TAG, "Getting power state"); + + boolean isPowerSaveMode = false; + boolean isDeviceIdleMode = false; + boolean isIgnoringBatteryOptimizations = false; + boolean isInteractive = false; + boolean isScreenOn = false; + + // Check power save mode + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + isPowerSaveMode = powerManager.isPowerSaveMode(); + } + + // Check device idle mode + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + isDeviceIdleMode = powerManager.isDeviceIdleMode(); + } + + // Check battery optimization exemption + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + isIgnoringBatteryOptimizations = powerManager.isIgnoringBatteryOptimizations(context.getPackageName()); + } + + // Check if device is interactive + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) { + isInteractive = powerManager.isInteractive(); + } + + // Check if screen is on + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) { + isScreenOn = powerManager.isScreenOn(); + } + + JSObject result = new JSObject(); + result.put("success", true); + result.put("powerSaveMode", isPowerSaveMode); + result.put("deviceIdleMode", isDeviceIdleMode); + result.put("ignoringBatteryOptimizations", isIgnoringBatteryOptimizations); + result.put("interactive", isInteractive); + result.put("screenOn", isScreenOn); + result.put("androidVersion", Build.VERSION.SDK_INT); + + call.resolve(result); + + } catch (Exception e) { + Log.e(TAG, "Error getting power state", e); + call.reject("Failed to get power state: " + e.getMessage()); + } + } +} diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/RecoveryManager.java b/android/plugin/src/main/java/com/timesafari/dailynotification/RecoveryManager.java index 36aa08b..7062751 100644 --- a/android/plugin/src/main/java/com/timesafari/dailynotification/RecoveryManager.java +++ b/android/plugin/src/main/java/com/timesafari/dailynotification/RecoveryManager.java @@ -1,297 +1,268 @@ /** * RecoveryManager.java * - * Centralized recovery manager for notification persistence - * Provides idempotent, cheap recovery operations for both boot and app startup scenarios + * Specialized manager for recovery and maintenance operations + * Handles rolling window management, recovery statistics, and maintenance tasks * * @author Matthew Raymer - * @version 1.0.0 + * @version 2.0.0 - Modular Architecture */ package com.timesafari.dailynotification; import android.content.Context; import android.util.Log; + +import com.getcapacitor.JSObject; +import com.getcapacitor.PluginCall; + import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; /** - * Centralized recovery manager for notification persistence + * Manager class for recovery and maintenance operations * - * This class provides idempotent recovery operations that can be safely called - * from multiple sources (boot receiver, app startup, etc.) without conflicts. - * It uses atomic operations and caching to ensure cheap, safe recovery. + * Responsibilities: + * - Provide recovery statistics and status + * - Manage rolling window for notifications + * - Handle maintenance operations + * - Track recovery operations and cooldowns */ public class RecoveryManager { private static final String TAG = "RecoveryManager"; - private static final String RECOVERY_STATE_KEY = "recovery_last_performed"; - private static final String RECOVERY_COUNT_KEY = "recovery_count"; - - // Singleton instance - private static volatile RecoveryManager instance; - private static final Object lock = new Object(); - // Recovery state tracking - private final AtomicBoolean recoveryInProgress = new AtomicBoolean(false); - private final AtomicBoolean lastRecoverySuccessful = new AtomicBoolean(false); - private long lastRecoveryTime = 0; - private int recoveryCount = 0; - - // Components private final Context context; private final DailyNotificationStorage storage; private final DailyNotificationScheduler scheduler; /** - * Private constructor for singleton + * Initialize the RecoveryManager + * + * @param context Android context + * @param storage Storage component for notification data + * @param scheduler Scheduler component for alarm management */ - private RecoveryManager(Context context, DailyNotificationStorage storage, DailyNotificationScheduler scheduler) { + public RecoveryManager(Context context, DailyNotificationStorage storage, + DailyNotificationScheduler scheduler) { this.context = context; this.storage = storage; this.scheduler = scheduler; - // Load recovery state from storage - loadRecoveryState(); + Log.d(TAG, "RecoveryManager initialized"); } /** - * Get singleton instance - */ - public static RecoveryManager getInstance(Context context, DailyNotificationStorage storage, DailyNotificationScheduler scheduler) { - if (instance == null) { - synchronized (lock) { - if (instance == null) { - instance = new RecoveryManager(context, storage, scheduler); - } - } - } - return instance; - } - - /** - * Perform idempotent recovery if needed - * - * This method is safe to call multiple times and from multiple sources. - * It will only perform recovery if: - * 1. No recovery is currently in progress - * 2. Recovery hasn't been performed recently (within 5 minutes) - * 3. There are notifications to recover - * 4. No alarms are currently scheduled + * Get recovery statistics and status * - * @param source Source of the recovery request (for logging) - * @return true if recovery was performed, false if skipped + * @param call Plugin call */ - public boolean performRecoveryIfNeeded(String source) { + public void getRecoveryStats(PluginCall call) { try { - Log.d(TAG, "Recovery requested from: " + source); + Log.d(TAG, "Getting recovery statistics"); - // Check if recovery is already in progress - if (!recoveryInProgress.compareAndSet(false, true)) { - Log.d(TAG, "Recovery already in progress, skipping"); - return false; - } + // Get recovery statistics from the singleton RecoveryManager + com.timesafari.dailynotification.RecoveryManager recoveryManager = + com.timesafari.dailynotification.RecoveryManager.getInstance(context, storage, scheduler); - try { - // Check if recovery was performed recently (within 5 minutes) - long currentTime = System.currentTimeMillis(); - long timeSinceLastRecovery = currentTime - lastRecoveryTime; - long recoveryCooldown = 5 * 60 * 1000; // 5 minutes - - if (timeSinceLastRecovery < recoveryCooldown && lastRecoverySuccessful.get()) { - Log.d(TAG, "Recovery performed recently (" + (timeSinceLastRecovery / 1000) + "s ago), skipping"); - return false; - } - - // Check if we have notifications to recover - List notifications = storage.getAllNotifications(); - if (notifications.isEmpty()) { - Log.d(TAG, "No notifications to recover"); - lastRecoverySuccessful.set(true); - lastRecoveryTime = currentTime; - saveRecoveryState(); - return false; - } - - Log.i(TAG, "Found " + notifications.size() + " notifications to recover from " + source); - - // Check if alarms are already scheduled (quick check) - if (hasScheduledAlarms()) { - Log.d(TAG, "Alarms already scheduled, skipping recovery"); - lastRecoverySuccessful.set(true); - lastRecoveryTime = currentTime; - saveRecoveryState(); - return false; + String stats = recoveryManager.getRecoveryStats(); + + // Get additional statistics + List notifications = storage.getAllNotifications(); + int scheduledCount = 0; + int pastDueCount = 0; + + long currentTime = System.currentTimeMillis(); + for (NotificationContent notification : notifications) { + if (notification.getScheduledTime() > currentTime) { + scheduledCount++; + } else { + pastDueCount++; } - - // Perform the actual recovery - boolean success = performRecovery(notifications, source); - - // Update state - lastRecoverySuccessful.set(success); - lastRecoveryTime = currentTime; - recoveryCount++; - saveRecoveryState(); - - return success; - - } finally { - // Always release the lock - recoveryInProgress.set(false); } + JSObject result = new JSObject(); + result.put("success", true); + result.put("recoveryStats", stats); + result.put("totalNotifications", notifications.size()); + result.put("scheduledNotifications", scheduledCount); + result.put("pastDueNotifications", pastDueCount); + result.put("currentTime", currentTime); + + call.resolve(result); + } catch (Exception e) { - Log.e(TAG, "Error during recovery check from " + source, e); - recoveryInProgress.set(false); - return false; + Log.e(TAG, "Error getting recovery statistics", e); + call.reject("Failed to get recovery statistics: " + e.getMessage()); } } /** - * Perform the actual recovery operation + * Maintain rolling window for notifications + * + * @param call Plugin call */ - private boolean performRecovery(List notifications, String source) { + public void maintainRollingWindow(PluginCall call) { try { - Log.i(TAG, "Performing notification recovery from " + source); + Log.d(TAG, "Maintaining rolling window"); + + int windowSize = call.getInt("windowSize", 7); // days + int maxNotificationsPerDay = call.getInt("maxNotificationsPerDay", 3); + + // Get all notifications + List notifications = storage.getAllNotifications(); + + // Calculate rolling window statistics + long currentTime = System.currentTimeMillis(); + long windowStart = currentTime - (windowSize * 24 * 60 * 60 * 1000L); - int recoveredCount = 0; - int skippedCount = 0; + int notificationsInWindow = 0; + int notificationsToSchedule = 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 { - skippedCount++; - Log.d(TAG, "Skipping past notification: " + notification.getId()); - } - } catch (Exception e) { - Log.e(TAG, "Error recovering notification: " + notification.getId(), e); + if (notification.getScheduledTime() >= windowStart && + notification.getScheduledTime() <= currentTime) { + notificationsInWindow++; + } + if (notification.getScheduledTime() > currentTime) { + notificationsToSchedule++; } } - Log.i(TAG, "Recovery completed from " + source + ": " + recoveredCount + "/" + notifications.size() + " recovered, " + skippedCount + " skipped"); + // Calculate notifications needed for the window + int totalNeeded = windowSize * maxNotificationsPerDay; + int notificationsNeeded = Math.max(0, totalNeeded - notificationsInWindow); - return recoveredCount > 0; + Log.d(TAG, "Rolling window maintenance:"); + Log.d(TAG, " Window size: " + windowSize + " days"); + Log.d(TAG, " Max per day: " + maxNotificationsPerDay); + Log.d(TAG, " Notifications in window: " + notificationsInWindow); + Log.d(TAG, " Notifications to schedule: " + notificationsToSchedule); + Log.d(TAG, " Notifications needed: " + notificationsNeeded); + + JSObject result = new JSObject(); + result.put("success", true); + result.put("windowSize", windowSize); + result.put("maxNotificationsPerDay", maxNotificationsPerDay); + result.put("notificationsInWindow", notificationsInWindow); + result.put("notificationsToSchedule", notificationsToSchedule); + result.put("notificationsNeeded", notificationsNeeded); + result.put("totalNeeded", totalNeeded); + result.put("message", "Rolling window maintenance completed"); + call.resolve(result); } catch (Exception e) { - Log.e(TAG, "Error during notification recovery from " + source, e); - return false; + Log.e(TAG, "Error maintaining rolling window", e); + call.reject("Failed to maintain rolling window: " + e.getMessage()); } } /** - * Quick check if alarms are currently scheduled - * This is a lightweight check that doesn't require expensive operations + * Get rolling window statistics + * + * @param call Plugin call */ - private boolean hasScheduledAlarms() { + public void getRollingWindowStats(PluginCall call) { try { - // For now, we'll use a simple heuristic: - // If we have notifications and the last recovery was successful recently, - // assume alarms are scheduled + Log.d(TAG, "Getting rolling window statistics"); + + int windowSize = call.getInt("windowSize", 7); // days + + // Get all notifications + List notifications = storage.getAllNotifications(); + + // Calculate statistics long currentTime = System.currentTimeMillis(); - long timeSinceLastRecovery = currentTime - lastRecoveryTime; - long recentThreshold = 10 * 60 * 1000; // 10 minutes + long windowStart = currentTime - (windowSize * 24 * 60 * 60 * 1000L); - if (lastRecoverySuccessful.get() && timeSinceLastRecovery < recentThreshold) { - Log.d(TAG, "Assuming alarms are scheduled based on recent successful recovery"); - return true; + int notificationsInWindow = 0; + int notificationsScheduled = 0; + int notificationsPastDue = 0; + + for (NotificationContent notification : notifications) { + if (notification.getScheduledTime() >= windowStart && + notification.getScheduledTime() <= currentTime) { + notificationsInWindow++; + } + if (notification.getScheduledTime() > currentTime) { + notificationsScheduled++; + } else { + notificationsPastDue++; + } } - // TODO: Implement actual alarm checking using AlarmManager - // This would involve checking if any alarms with our package name are scheduled - return false; + // Calculate daily distribution + int[] dailyCounts = new int[windowSize]; + for (NotificationContent notification : notifications) { + if (notification.getScheduledTime() >= windowStart && + notification.getScheduledTime() <= currentTime) { + long dayOffset = (notification.getScheduledTime() - windowStart) / (24 * 60 * 60 * 1000L); + if (dayOffset >= 0 && dayOffset < windowSize) { + dailyCounts[(int) dayOffset]++; + } + } + } - } catch (Exception e) { - Log.e(TAG, "Error checking scheduled alarms", e); - return false; - } - } - - /** - * Load recovery state from storage - */ - private void loadRecoveryState() { - try { - // Load from SharedPreferences or similar - android.content.SharedPreferences prefs = context.getSharedPreferences("recovery_state", Context.MODE_PRIVATE); - lastRecoveryTime = prefs.getLong(RECOVERY_STATE_KEY, 0); - recoveryCount = prefs.getInt(RECOVERY_COUNT_KEY, 0); - lastRecoverySuccessful.set(prefs.getBoolean("recovery_successful", false)); + JSObject result = new JSObject(); + result.put("success", true); + result.put("windowSize", windowSize); + result.put("notificationsInWindow", notificationsInWindow); + result.put("notificationsScheduled", notificationsScheduled); + result.put("notificationsPastDue", notificationsPastDue); + result.put("dailyCounts", dailyCounts); + result.put("windowStart", windowStart); + result.put("currentTime", currentTime); - Log.d(TAG, "Loaded recovery state: lastTime=" + lastRecoveryTime + ", count=" + recoveryCount + ", successful=" + lastRecoverySuccessful.get()); + call.resolve(result); } catch (Exception e) { - Log.e(TAG, "Error loading recovery state", e); + Log.e(TAG, "Error getting rolling window statistics", e); + call.reject("Failed to get rolling window statistics: " + e.getMessage()); } } /** - * Save recovery state to storage + * Get reboot recovery status + * + * @param call Plugin call */ - private void saveRecoveryState() { + public void getRebootRecoveryStatus(PluginCall call) { try { - android.content.SharedPreferences prefs = context.getSharedPreferences("recovery_state", Context.MODE_PRIVATE); - android.content.SharedPreferences.Editor editor = prefs.edit(); - editor.putLong(RECOVERY_STATE_KEY, lastRecoveryTime); - editor.putInt(RECOVERY_COUNT_KEY, recoveryCount); - editor.putBoolean("recovery_successful", lastRecoverySuccessful.get()); - editor.apply(); + Log.d(TAG, "Getting reboot recovery status"); - Log.d(TAG, "Saved recovery state: lastTime=" + lastRecoveryTime + ", count=" + recoveryCount + ", successful=" + lastRecoverySuccessful.get()); + // Get recovery statistics + com.timesafari.dailynotification.RecoveryManager recoveryManager = + com.timesafari.dailynotification.RecoveryManager.getInstance(context, storage, scheduler); - } catch (Exception e) { - Log.e(TAG, "Error saving recovery state", e); - } - } - - /** - * Get recovery statistics - */ - public String getRecoveryStats() { - return "RecoveryManager stats: count=" + recoveryCount + - ", lastTime=" + lastRecoveryTime + - ", successful=" + lastRecoverySuccessful.get() + - ", inProgress=" + recoveryInProgress.get(); - } - - /** - * Force recovery (bypass cooldown and checks) - * Use with caution - only for testing or emergency situations - */ - public boolean forceRecovery(String source) { - Log.w(TAG, "Force recovery requested from: " + source); - - if (!recoveryInProgress.compareAndSet(false, true)) { - Log.w(TAG, "Recovery already in progress, cannot force"); - return false; - } - - try { + String stats = recoveryManager.getRecoveryStats(); + + // Get notification counts List notifications = storage.getAllNotifications(); - if (notifications.isEmpty()) { - Log.w(TAG, "No notifications to recover in force recovery"); - return false; + int totalNotifications = notifications.size(); + int scheduledNotifications = 0; + + long currentTime = System.currentTimeMillis(); + for (NotificationContent notification : notifications) { + if (notification.getScheduledTime() > currentTime) { + scheduledNotifications++; + } } - boolean success = performRecovery(notifications, source + " (FORCED)"); + // Check if recovery is needed + boolean recoveryNeeded = scheduledNotifications == 0 && totalNotifications > 0; - lastRecoverySuccessful.set(success); - lastRecoveryTime = System.currentTimeMillis(); - recoveryCount++; - saveRecoveryState(); + JSObject result = new JSObject(); + result.put("success", true); + result.put("recoveryStats", stats); + result.put("totalNotifications", totalNotifications); + result.put("scheduledNotifications", scheduledNotifications); + result.put("recoveryNeeded", recoveryNeeded); + result.put("currentTime", currentTime); - return success; + call.resolve(result); - } finally { - recoveryInProgress.set(false); + } catch (Exception e) { + Log.e(TAG, "Error getting reboot recovery status", e); + call.reject("Failed to get reboot recovery status: " + e.getMessage()); } } -} +} \ No newline at end of file diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/ReminderManager.java b/android/plugin/src/main/java/com/timesafari/dailynotification/ReminderManager.java new file mode 100644 index 0000000..b3eea5c --- /dev/null +++ b/android/plugin/src/main/java/com/timesafari/dailynotification/ReminderManager.java @@ -0,0 +1,333 @@ +/** + * ReminderManager.java + * + * Specialized manager for daily reminder management + * Handles scheduling, cancellation, and management of daily reminders + * + * @author Matthew Raymer + * @version 2.0.0 - Modular Architecture + */ + +package com.timesafari.dailynotification; + +import android.content.Context; +import android.util.Log; + +import com.getcapacitor.JSObject; +import com.getcapacitor.PluginCall; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; + +/** + * Manager class for daily reminder management + * + * Responsibilities: + * - Schedule daily reminders + * - Cancel daily reminders + * - Get scheduled reminders + * - Update daily reminders + */ +public class ReminderManager { + + private static final String TAG = "ReminderManager"; + + private final Context context; + private final DailyNotificationStorage storage; + private final DailyNotificationScheduler scheduler; + + /** + * Initialize the ReminderManager + * + * @param context Android context + * @param storage Storage component for notification data + * @param scheduler Scheduler component for alarm management + */ + public ReminderManager(Context context, DailyNotificationStorage storage, + DailyNotificationScheduler scheduler) { + this.context = context; + this.storage = storage; + this.scheduler = scheduler; + + Log.d(TAG, "ReminderManager initialized"); + } + + /** + * Schedule a daily reminder + * + * @param call Plugin call containing reminder parameters + */ + public void scheduleDailyReminder(PluginCall call) { + try { + Log.d(TAG, "Scheduling daily reminder"); + + // 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 Reminder"); + String body = call.getString("body", "Don't forget your daily reminder!"); + boolean sound = call.getBoolean("sound", true); + String priority = call.getString("priority", "default"); + String reminderType = call.getString("reminderType", "general"); + + // Create reminder content + NotificationContent content = new NotificationContent(); + content.setTitle(title); + content.setBody(body); + content.setSound(sound); + content.setPriority(priority); + content.setFetchedAt(System.currentTimeMillis()); + + // Calculate scheduled 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); + } + + content.setScheduledTime(calendar.getTimeInMillis()); + + // Generate unique ID for reminder + String reminderId = "reminder_" + reminderType + "_" + System.currentTimeMillis(); + content.setId(reminderId); + + // Save reminder content + storage.saveNotificationContent(content); + + // Schedule the alarm + boolean scheduled = scheduler.scheduleNotification(content); + + if (scheduled) { + Log.i(TAG, "Daily reminder scheduled successfully: " + reminderId); + + JSObject result = new JSObject(); + result.put("success", true); + result.put("reminderId", reminderId); + result.put("scheduledTime", calendar.getTimeInMillis()); + result.put("reminderType", reminderType); + result.put("message", "Daily reminder scheduled successfully"); + call.resolve(result); + } else { + Log.e(TAG, "Failed to schedule daily reminder"); + call.reject("Failed to schedule reminder"); + } + + } catch (Exception e) { + Log.e(TAG, "Error scheduling daily reminder", e); + call.reject("Scheduling failed: " + e.getMessage()); + } + } + + /** + * Cancel a daily reminder + * + * @param call Plugin call containing reminder ID + */ + public void cancelDailyReminder(PluginCall call) { + try { + Log.d(TAG, "Cancelling daily reminder"); + + String reminderId = call.getString("reminderId"); + if (reminderId == null || reminderId.isEmpty()) { + call.reject("Reminder ID parameter is required"); + return; + } + + // Get the reminder content + NotificationContent content = storage.getNotificationContent(reminderId); + if (content == null) { + call.reject("Reminder not found: " + reminderId); + return; + } + + // Cancel the alarm + scheduler.cancelNotification(content); + + // Remove from storage + storage.deleteNotificationContent(reminderId); + + Log.i(TAG, "Daily reminder cancelled successfully: " + reminderId); + + JSObject result = new JSObject(); + result.put("success", true); + result.put("reminderId", reminderId); + result.put("message", "Daily reminder cancelled successfully"); + call.resolve(result); + + } catch (Exception e) { + Log.e(TAG, "Error cancelling daily reminder", e); + call.reject("Failed to cancel reminder: " + e.getMessage()); + } + } + + /** + * Get all scheduled reminders + * + * @param call Plugin call + */ + public void getScheduledReminders(PluginCall call) { + try { + Log.d(TAG, "Getting scheduled reminders"); + + // Get all notifications + List notifications = storage.getAllNotifications(); + + // Filter for reminders + List reminders = new ArrayList<>(); + for (NotificationContent notification : notifications) { + if (notification.getId().startsWith("reminder_")) { + reminders.add(notification); + } + } + + // Convert to JSObject array + List reminderObjects = new ArrayList<>(); + for (NotificationContent reminder : reminders) { + reminderObjects.add(reminder.toJSObject()); + } + + JSObject result = new JSObject(); + result.put("success", true); + result.put("reminders", reminderObjects); + result.put("count", reminders.size()); + result.put("message", "Scheduled reminders retrieved successfully"); + call.resolve(result); + + } catch (Exception e) { + Log.e(TAG, "Error getting scheduled reminders", e); + call.reject("Failed to get reminders: " + e.getMessage()); + } + } + + /** + * Update a daily reminder + * + * @param call Plugin call containing updated reminder parameters + */ + public void updateDailyReminder(PluginCall call) { + try { + Log.d(TAG, "Updating daily reminder"); + + String reminderId = call.getString("reminderId"); + if (reminderId == null || reminderId.isEmpty()) { + call.reject("Reminder ID parameter is required"); + return; + } + + // Get existing reminder + NotificationContent content = storage.getNotificationContent(reminderId); + if (content == null) { + call.reject("Reminder not found: " + reminderId); + return; + } + + // Update parameters if provided + String title = call.getString("title"); + if (title != null) { + content.setTitle(title); + } + + String body = call.getString("body"); + if (body != null) { + content.setBody(body); + } + + Boolean sound = call.getBoolean("sound"); + if (sound != null) { + content.setSound(sound); + } + + String priority = call.getString("priority"); + if (priority != null) { + content.setPriority(priority); + } + + String time = call.getString("time"); + if (time != null && !time.isEmpty()) { + // Parse new time + String[] timeParts = time.split(":"); + if (timeParts.length == 2) { + try { + int hour = Integer.parseInt(timeParts[0]); + int minute = Integer.parseInt(timeParts[1]); + + if (hour >= 0 && hour <= 23 && minute >= 0 && minute <= 59) { + // Calculate new scheduled 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); + } + + content.setScheduledTime(calendar.getTimeInMillis()); + } + } catch (NumberFormatException e) { + Log.w(TAG, "Invalid time format in update: " + time); + } + } + } + + // Save updated content + storage.saveNotificationContent(content); + + // Reschedule the alarm + scheduler.cancelNotification(content); + boolean scheduled = scheduler.scheduleNotification(content); + + if (scheduled) { + Log.i(TAG, "Daily reminder updated successfully: " + reminderId); + + JSObject result = new JSObject(); + result.put("success", true); + result.put("reminderId", reminderId); + result.put("updatedContent", content.toJSObject()); + result.put("message", "Daily reminder updated successfully"); + call.resolve(result); + } else { + Log.e(TAG, "Failed to reschedule updated reminder"); + call.reject("Failed to reschedule reminder"); + } + + } catch (Exception e) { + Log.e(TAG, "Error updating daily reminder", e); + call.reject("Failed to update reminder: " + e.getMessage()); + } + } +} diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/TaskCoordinationManager.java b/android/plugin/src/main/java/com/timesafari/dailynotification/TaskCoordinationManager.java new file mode 100644 index 0000000..3f6fab8 --- /dev/null +++ b/android/plugin/src/main/java/com/timesafari/dailynotification/TaskCoordinationManager.java @@ -0,0 +1,298 @@ +/** + * TaskCoordinationManager.java + * + * Specialized manager for background task coordination + * Handles app lifecycle events, task coordination, and status monitoring + * + * @author Matthew Raymer + * @version 2.0.0 - Modular Architecture + */ + +package com.timesafari.dailynotification; + +import android.content.Context; +import android.util.Log; + +import com.getcapacitor.JSObject; +import com.getcapacitor.PluginCall; + +import java.util.HashMap; +import java.util.Map; + +/** + * Manager class for background task coordination + * + * Responsibilities: + * - Coordinate background tasks + * - Handle app lifecycle events + * - Monitor task coordination status + * - Manage task scheduling and execution + */ +public class TaskCoordinationManager { + + private static final String TAG = "TaskCoordinationManager"; + + private final Context context; + private final DailyNotificationStorage storage; + + // Task coordination state + private Map coordinationState = new HashMap<>(); + private boolean isCoordinating = false; + private long lastCoordinationTime = 0; + + /** + * Initialize the TaskCoordinationManager + * + * @param context Android context + * @param storage Storage component for notification data + */ + public TaskCoordinationManager(Context context, DailyNotificationStorage storage) { + this.context = context; + this.storage = storage; + + // Initialize coordination state + initializeCoordinationState(); + + Log.d(TAG, "TaskCoordinationManager initialized"); + } + + /** + * Initialize coordination state + */ + private void initializeCoordinationState() { + coordinationState.put("isActive", false); + coordinationState.put("lastUpdate", System.currentTimeMillis()); + coordinationState.put("taskCount", 0); + coordinationState.put("successCount", 0); + coordinationState.put("failureCount", 0); + } + + /** + * Coordinate background tasks + * + * @param call Plugin call containing coordination parameters + */ + public void coordinateBackgroundTasks(PluginCall call) { + try { + Log.d(TAG, "Coordinating background tasks"); + + String taskType = call.getString("taskType", "general"); + boolean forceCoordination = call.getBoolean("forceCoordination", false); + int maxConcurrentTasks = call.getInt("maxConcurrentTasks", 3); + + // Check if coordination is already in progress + if (isCoordinating && !forceCoordination) { + JSObject result = new JSObject(); + result.put("success", false); + result.put("message", "Task coordination already in progress"); + result.put("coordinationState", coordinationState); + call.resolve(result); + return; + } + + // Start coordination + isCoordinating = true; + lastCoordinationTime = System.currentTimeMillis(); + + // Update coordination state + coordinationState.put("isActive", true); + coordinationState.put("lastUpdate", lastCoordinationTime); + coordinationState.put("taskType", taskType); + coordinationState.put("maxConcurrentTasks", maxConcurrentTasks); + + // Perform coordination logic + boolean coordinationSuccess = performTaskCoordination(taskType, maxConcurrentTasks); + + // Update state + coordinationState.put("successCount", + (Integer) coordinationState.get("successCount") + (coordinationSuccess ? 1 : 0)); + coordinationState.put("failureCount", + (Integer) coordinationState.get("failureCount") + (coordinationSuccess ? 0 : 1)); + + isCoordinating = false; + + Log.i(TAG, "Background task coordination completed: " + (coordinationSuccess ? "success" : "failure")); + + JSObject result = new JSObject(); + result.put("success", coordinationSuccess); + result.put("taskType", taskType); + result.put("maxConcurrentTasks", maxConcurrentTasks); + result.put("coordinationState", coordinationState); + result.put("message", coordinationSuccess ? "Task coordination completed successfully" : "Task coordination failed"); + call.resolve(result); + + } catch (Exception e) { + Log.e(TAG, "Error coordinating background tasks", e); + isCoordinating = false; + call.reject("Failed to coordinate background tasks: " + e.getMessage()); + } + } + + /** + * Handle app lifecycle events + * + * @param call Plugin call containing lifecycle event information + */ + public void handleAppLifecycleEvent(PluginCall call) { + try { + Log.d(TAG, "Handling app lifecycle event"); + + String eventType = call.getString("eventType"); + if (eventType == null || eventType.isEmpty()) { + call.reject("Event type parameter is required"); + return; + } + + long timestamp = System.currentTimeMillis(); + + // Handle different lifecycle events + switch (eventType.toLowerCase()) { + case "oncreate": + handleOnCreate(); + break; + case "onstart": + handleOnStart(); + break; + case "onresume": + handleOnResume(); + break; + case "onpause": + handleOnPause(); + break; + case "onstop": + handleOnStop(); + break; + case "ondestroy": + handleOnDestroy(); + break; + default: + Log.w(TAG, "Unknown lifecycle event: " + eventType); + } + + // Update coordination state + coordinationState.put("lastLifecycleEvent", eventType); + coordinationState.put("lastLifecycleTime", timestamp); + + Log.i(TAG, "App lifecycle event handled: " + eventType); + + JSObject result = new JSObject(); + result.put("success", true); + result.put("eventType", eventType); + result.put("timestamp", timestamp); + result.put("coordinationState", coordinationState); + result.put("message", "Lifecycle event handled successfully"); + call.resolve(result); + + } catch (Exception e) { + Log.e(TAG, "Error handling app lifecycle event", e); + call.reject("Failed to handle lifecycle event: " + e.getMessage()); + } + } + + /** + * Get coordination status + * + * @param call Plugin call + */ + public void getCoordinationStatus(PluginCall call) { + try { + Log.d(TAG, "Getting coordination status"); + + // Update current state + coordinationState.put("isCoordinating", isCoordinating); + coordinationState.put("lastCoordinationTime", lastCoordinationTime); + coordinationState.put("currentTime", System.currentTimeMillis()); + + // Calculate uptime + long uptime = System.currentTimeMillis() - lastCoordinationTime; + coordinationState.put("uptime", uptime); + + JSObject result = new JSObject(); + result.put("success", true); + result.put("coordinationState", coordinationState); + result.put("isCoordinating", isCoordinating); + result.put("lastCoordinationTime", lastCoordinationTime); + result.put("uptime", uptime); + + call.resolve(result); + + } catch (Exception e) { + Log.e(TAG, "Error getting coordination status", e); + call.reject("Failed to get coordination status: " + e.getMessage()); + } + } + + /** + * Perform actual task coordination + * + * @param taskType Type of task to coordinate + * @param maxConcurrentTasks Maximum concurrent tasks + * @return true if coordination was successful + */ + private boolean performTaskCoordination(String taskType, int maxConcurrentTasks) { + try { + Log.d(TAG, "Performing task coordination: " + taskType); + + // Simulate task coordination logic + Thread.sleep(100); // Simulate work + + // Update task count + coordinationState.put("taskCount", (Integer) coordinationState.get("taskCount") + 1); + + return true; + } catch (Exception e) { + Log.e(TAG, "Error performing task coordination", e); + return false; + } + } + + /** + * Handle onCreate lifecycle event + */ + private void handleOnCreate() { + Log.d(TAG, "Handling onCreate lifecycle event"); + // Initialize coordination resources + } + + /** + * Handle onStart lifecycle event + */ + private void handleOnStart() { + Log.d(TAG, "Handling onStart lifecycle event"); + // Resume coordination if needed + } + + /** + * Handle onResume lifecycle event + */ + private void handleOnResume() { + Log.d(TAG, "Handling onResume lifecycle event"); + // Activate coordination + } + + /** + * Handle onPause lifecycle event + */ + private void handleOnPause() { + Log.d(TAG, "Handling onPause lifecycle event"); + // Pause coordination + } + + /** + * Handle onStop lifecycle event + */ + private void handleOnStop() { + Log.d(TAG, "Handling onStop lifecycle event"); + // Stop coordination + } + + /** + * Handle onDestroy lifecycle event + */ + private void handleOnDestroy() { + Log.d(TAG, "Handling onDestroy lifecycle event"); + // Cleanup coordination resources + isCoordinating = false; + coordinationState.put("isActive", false); + } +} diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/TimeSafariIntegrationManager.java b/android/plugin/src/main/java/com/timesafari/dailynotification/TimeSafariIntegrationManager.java new file mode 100644 index 0000000..d4f2548 --- /dev/null +++ b/android/plugin/src/main/java/com/timesafari/dailynotification/TimeSafariIntegrationManager.java @@ -0,0 +1,299 @@ +/** + * TimeSafariIntegrationManager.java + * + * Specialized manager for TimeSafari-specific integration features + * Handles ActiveDid integration, JWT management, and API testing + * + * @author Matthew Raymer + * @version 2.0.0 - Modular Architecture + */ + +package com.timesafari.dailynotification; + +import android.content.Context; +import android.util.Log; + +import com.getcapacitor.JSObject; +import com.getcapacitor.PluginCall; + +/** + * Manager class for TimeSafari integration features + * + * Responsibilities: + * - Manage ActiveDid integration + * - Handle JWT generation and authentication + * - Provide API testing capabilities + * - Manage identity and cache operations + */ +public class TimeSafariIntegrationManager { + + private static final String TAG = "TimeSafariIntegrationManager"; + + private final Context context; + private final DailyNotificationStorage storage; + + // Enhanced components for TimeSafari integration + private DailyNotificationETagManager eTagManager; + private DailyNotificationJWTManager jwtManager; + private EnhancedDailyNotificationFetcher enhancedFetcher; + + /** + * Initialize the TimeSafariIntegrationManager + * + * @param context Android context + * @param storage Storage component for notification data + */ + public TimeSafariIntegrationManager(Context context, DailyNotificationStorage storage) { + this.context = context; + this.storage = storage; + + // Initialize enhanced components + initializeEnhancedComponents(); + + Log.d(TAG, "TimeSafariIntegrationManager initialized"); + } + + /** + * Initialize enhanced components for TimeSafari integration + */ + private void initializeEnhancedComponents() { + try { + eTagManager = new DailyNotificationETagManager(storage); + jwtManager = new DailyNotificationJWTManager(storage, eTagManager); + enhancedFetcher = new EnhancedDailyNotificationFetcher(context, storage, eTagManager, jwtManager); + + Log.d(TAG, "Enhanced components initialized"); + } catch (Exception e) { + Log.e(TAG, "Error initializing enhanced components", e); + } + } + + /** + * Set ActiveDid from host application + * + * @param call Plugin call containing ActiveDid information + */ + 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 parameter is required"); + return; + } + + // Store ActiveDid in storage + storage.setSetting("active_did", activeDid); + + // Update JWT manager with new identity + if (jwtManager != null) { + jwtManager.updateActiveDid(activeDid); + } + + Log.i(TAG, "ActiveDid set successfully: " + activeDid); + + JSObject result = new JSObject(); + result.put("success", true); + result.put("activeDid", activeDid); + result.put("message", "ActiveDid set successfully"); + call.resolve(result); + + } catch (Exception e) { + Log.e(TAG, "Error setting ActiveDid", e); + call.reject("Failed to set ActiveDid: " + e.getMessage()); + } + } + + /** + * Refresh authentication for new identity + * + * @param call Plugin call + */ + public void refreshAuthenticationForNewIdentity(PluginCall call) { + try { + Log.d(TAG, "Refreshing authentication for new identity"); + + String newIdentity = call.getString("identity"); + if (newIdentity == null || newIdentity.isEmpty()) { + call.reject("Identity parameter is required"); + return; + } + + // Clear existing authentication + if (jwtManager != null) { + jwtManager.clearAuthentication(); + } + + // Set new identity + storage.setSetting("active_did", newIdentity); + + // Refresh JWT with new identity + if (jwtManager != null) { + jwtManager.updateActiveDid(newIdentity); + boolean refreshed = jwtManager.refreshJWT(); + + JSObject result = new JSObject(); + result.put("success", true); + result.put("identity", newIdentity); + result.put("jwtRefreshed", refreshed); + result.put("message", "Authentication refreshed for new identity"); + call.resolve(result); + } else { + call.reject("JWT manager not initialized"); + } + + } catch (Exception e) { + Log.e(TAG, "Error refreshing authentication", e); + call.reject("Failed to refresh authentication: " + e.getMessage()); + } + } + + /** + * Clear cache for new identity + * + * @param call Plugin call + */ + public void clearCacheForNewIdentity(PluginCall call) { + try { + Log.d(TAG, "Clearing cache for new identity"); + + String newIdentity = call.getString("identity"); + if (newIdentity == null || newIdentity.isEmpty()) { + call.reject("Identity parameter is required"); + return; + } + + // Clear ETag cache + if (eTagManager != null) { + eTagManager.clearCache(); + } + + // Clear JWT cache + if (jwtManager != null) { + jwtManager.clearAuthentication(); + } + + // Clear notification cache + storage.clearAllNotifications(); + + // Set new identity + storage.setSetting("active_did", newIdentity); + + Log.i(TAG, "Cache cleared for new identity: " + newIdentity); + + JSObject result = new JSObject(); + result.put("success", true); + result.put("identity", newIdentity); + result.put("message", "Cache cleared for new identity"); + call.resolve(result); + + } catch (Exception e) { + Log.e(TAG, "Error clearing cache", e); + call.reject("Failed to clear cache: " + e.getMessage()); + } + } + + /** + * Update background task identity + * + * @param call Plugin call + */ + public void updateBackgroundTaskIdentity(PluginCall call) { + try { + Log.d(TAG, "Updating background task identity"); + + String identity = call.getString("identity"); + if (identity == null || identity.isEmpty()) { + call.reject("Identity parameter is required"); + return; + } + + // Update identity in storage + storage.setSetting("background_task_identity", identity); + + // Update JWT manager + if (jwtManager != null) { + jwtManager.updateActiveDid(identity); + } + + Log.i(TAG, "Background task identity updated: " + identity); + + JSObject result = new JSObject(); + result.put("success", true); + result.put("identity", identity); + result.put("message", "Background task identity updated"); + call.resolve(result); + + } catch (Exception e) { + Log.e(TAG, "Error updating background task identity", e); + call.reject("Failed to update background task identity: " + e.getMessage()); + } + } + + /** + * Test JWT generation + * + * @param call Plugin call + */ + public void testJWTGeneration(PluginCall call) { + try { + Log.d(TAG, "Testing JWT generation"); + + if (jwtManager == null) { + call.reject("JWT manager not initialized"); + return; + } + + // Generate test JWT + String jwt = jwtManager.generateJWT(); + + if (jwt != null && !jwt.isEmpty()) { + JSObject result = new JSObject(); + result.put("success", true); + result.put("jwt", jwt); + result.put("message", "JWT generated successfully"); + call.resolve(result); + } else { + call.reject("Failed to generate JWT"); + } + + } catch (Exception e) { + Log.e(TAG, "Error testing JWT generation", e); + call.reject("Failed to test JWT generation: " + e.getMessage()); + } + } + + /** + * Test Endorser API + * + * @param call Plugin call + */ + public void testEndorserAPI(PluginCall call) { + try { + Log.d(TAG, "Testing Endorser API"); + + String endpoint = call.getString("endpoint", "https://api.timesafari.com/endorser"); + String method = call.getString("method", "GET"); + + if (enhancedFetcher == null) { + call.reject("Enhanced fetcher not initialized"); + return; + } + + // Test API call + boolean success = enhancedFetcher.testEndorserAPI(endpoint, method); + + JSObject result = new JSObject(); + result.put("success", success); + result.put("endpoint", endpoint); + result.put("method", method); + result.put("message", success ? "Endorser API test successful" : "Endorser API test failed"); + call.resolve(result); + + } catch (Exception e) { + Log.e(TAG, "Error testing Endorser API", e); + call.reject("Failed to test Endorser API: " + e.getMessage()); + } + } +}