diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/BootReceiver.java b/android/plugin/src/main/java/com/timesafari/dailynotification/BootReceiver.java index 70be78c..bec8096 100644 --- a/android/plugin/src/main/java/com/timesafari/dailynotification/BootReceiver.java +++ b/android/plugin/src/main/java/com/timesafari/dailynotification/BootReceiver.java @@ -107,9 +107,8 @@ public class BootReceiver extends BroadcastReceiver { context.getSystemService(android.content.Context.ALARM_SERVICE); DailyNotificationScheduler scheduler = new DailyNotificationScheduler(context, alarmManager); - // Use centralized recovery manager for idempotent recovery - RecoveryManager recoveryManager = RecoveryManager.getInstance(context, storage, scheduler); - boolean recoveryPerformed = recoveryManager.performRecoveryIfNeeded("BOOT_COMPLETED"); + // Perform boot recovery + boolean recoveryPerformed = performBootRecovery(context, storage, scheduler); if (recoveryPerformed) { Log.i(TAG, "Boot recovery completed successfully"); @@ -138,9 +137,8 @@ public class BootReceiver extends BroadcastReceiver { context.getSystemService(android.content.Context.ALARM_SERVICE); DailyNotificationScheduler scheduler = new DailyNotificationScheduler(context, alarmManager); - // Use centralized recovery manager for idempotent recovery - RecoveryManager recoveryManager = RecoveryManager.getInstance(context, storage, scheduler); - boolean recoveryPerformed = recoveryManager.performRecoveryIfNeeded("MY_PACKAGE_REPLACED"); + // Perform package replacement recovery + boolean recoveryPerformed = performBootRecovery(context, storage, scheduler); if (recoveryPerformed) { Log.i(TAG, "Package replacement recovery completed successfully"); @@ -152,4 +150,57 @@ public class BootReceiver extends BroadcastReceiver { Log.e(TAG, "Error during package replacement recovery", e); } } + + /** + * Perform boot recovery by rescheduling notifications + * + * @param context Application context + * @param storage Notification storage + * @param scheduler Notification scheduler + * @return true if recovery was performed, false otherwise + */ + private boolean performBootRecovery(Context context, DailyNotificationStorage storage, + DailyNotificationScheduler scheduler) { + try { + Log.d(TAG, "DN|BOOT_RECOVERY_START"); + + // Get all notifications from storage + java.util.List notifications = storage.getAllNotifications(); + + if (notifications.isEmpty()) { + Log.d(TAG, "DN|BOOT_RECOVERY_SKIP no_notifications"); + return false; + } + + Log.d(TAG, "DN|BOOT_RECOVERY_FOUND count=" + notifications.size()); + + int recoveredCount = 0; + long currentTime = System.currentTimeMillis(); + + for (NotificationContent notification : notifications) { + try { + if (notification.getScheduledTime() > currentTime) { + boolean scheduled = scheduler.scheduleNotification(notification); + if (scheduled) { + recoveredCount++; + Log.d(TAG, "DN|BOOT_RECOVERY_OK id=" + notification.getId()); + } else { + Log.w(TAG, "DN|BOOT_RECOVERY_FAIL id=" + notification.getId()); + } + } else { + Log.d(TAG, "DN|BOOT_RECOVERY_SKIP_PAST id=" + notification.getId()); + } + } catch (Exception e) { + Log.e(TAG, "DN|BOOT_RECOVERY_ERR id=" + notification.getId() + " err=" + e.getMessage(), e); + } + } + + Log.i(TAG, "DN|BOOT_RECOVERY_COMPLETE recovered=" + recoveredCount + "/" + notifications.size()); + return recoveredCount > 0; + + } catch (Exception e) { + Log.e(TAG, "DN|BOOT_RECOVERY_ERR exception=" + e.getMessage(), e); + return false; + } + } } diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationFetchWorkerOptimized.java b/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationFetchWorkerOptimized.java deleted file mode 100644 index 911f961..0000000 --- a/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationFetchWorkerOptimized.java +++ /dev/null @@ -1,302 +0,0 @@ -/** - * DailyNotificationFetchWorkerOptimized.java - * - * Optimized fetch worker with WorkManager hygiene best practices - * Extends OptimizedWorker for proper lifecycle management and resource cleanup - * - * @author Matthew Raymer - * @version 2.0.0 - Optimized Architecture - */ - -package com.timesafari.dailynotification; - -import android.content.Context; -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.work.Data; -import androidx.work.WorkerParameters; - -import java.util.concurrent.TimeUnit; - -/** - * Optimized fetch worker with hygiene best practices - * - * Features: - * - Proper resource management - * - Timeout handling - * - Performance monitoring - * - Error recovery - * - Memory optimization - */ -public class DailyNotificationFetchWorkerOptimized extends OptimizedWorker { - - private static final String TAG = "DailyNotificationFetchWorkerOptimized"; - - // Configuration constants - private static final String KEY_SCHEDULED_TIME = "scheduled_time"; - private static final String KEY_FETCH_TIME = "fetch_time"; - private static final String KEY_RETRY_COUNT = "retry_count"; - private static final String KEY_IMMEDIATE = "immediate"; - - private static final int MAX_RETRY_ATTEMPTS = 3; - private static final long WORK_TIMEOUT_MS = 8 * 60 * 1000; // 8 minutes total - private static final long FETCH_TIMEOUT_MS = 30 * 1000; // 30 seconds for fetch - - // Worker components - private DailyNotificationStorageOptimized storage; - private DailyNotificationFetcher fetcher; - private JsonOptimizer jsonOptimizer; - - // Worker state - private int retryCount = 0; - private boolean isImmediate = false; - private long scheduledTime = 0; - - /** - * Constructor - * - * @param context Application context - * @param params Worker parameters - */ - public DailyNotificationFetchWorkerOptimized(@NonNull Context context, - @NonNull WorkerParameters params) { - super(context, params); - - Log.d(TAG, "Optimized fetch worker initialized"); - } - - /** - * Initialize worker-specific resources - */ - @Override - protected void onInitializeResources() { - try { - logProgress("Initializing resources"); - - // Initialize optimized storage - storage = new DailyNotificationStorageOptimized(getApplicationContext()); - - // Initialize fetcher - fetcher = new DailyNotificationFetcher(getApplicationContext(), storage); - - // Initialize JSON optimizer - jsonOptimizer = new JsonOptimizer(); - - // Parse worker parameters - parseWorkerParameters(); - - logProgress("Resources initialized successfully"); - - } catch (Exception e) { - Log.e(TAG, "Error initializing resources", e); - throw e; - } - } - - /** - * Cleanup worker-specific resources - */ - @Override - protected void onCleanupResources() { - try { - logProgress("Cleaning up resources"); - - // Flush storage changes - if (storage != null) { - storage.flush(); - } - - // Clear JSON cache if needed - if (jsonOptimizer != null) { - JsonOptimizer.clearCache(); - } - - logProgress("Resources cleaned up successfully"); - - } catch (Exception e) { - Log.e(TAG, "Error cleaning up resources", e); - } - } - - /** - * Perform the fetch work with optimization - * - * @return Result of the work - */ - @NonNull - @Override - protected Result performWork() { - try { - logProgress("Starting fetch work"); - - // Check if work should be cancelled - if (shouldCancelWork(WORK_TIMEOUT_MS)) { - logProgress("Work cancelled due to timeout"); - return createFailureResult(); - } - - // Check if work is cancelled - if (isWorkCancelled()) { - logProgress("Work cancelled by system"); - return createFailureResult(); - } - - // Perform fetch with timeout - NotificationContent content = performFetchWithTimeout(); - - if (content != null) { - logProgress("Fetch completed successfully"); - - // Create success result with data - Data resultData = new Data.Builder() - .putString("notification_id", content.getId()) - .putLong("scheduled_time", content.getScheduledTime()) - .putLong("fetch_time", System.currentTimeMillis()) - .putInt("retry_count", retryCount) - .build(); - - return createSuccessResult(resultData); - } else { - logProgress("Fetch failed - no content retrieved"); - - // Check if we should retry - if (shouldRetry()) { - logProgress("Scheduling retry"); - return createRetryResult(); - } else { - logProgress("Max retries exceeded"); - return createFailureResult(); - } - } - - } catch (Exception e) { - Log.e(TAG, "Error in fetch work", e); - - // Check if we should retry - if (shouldRetry()) { - logProgress("Scheduling retry after exception"); - return createRetryResult(); - } else { - logProgress("Max retries exceeded after exception"); - return createFailureResult(); - } - } - } - - /** - * Parse worker parameters - */ - private void parseWorkerParameters() { - try { - Data inputData = getInputData(); - - retryCount = inputData.getInt(KEY_RETRY_COUNT, 0); - isImmediate = inputData.getBoolean(KEY_IMMEDIATE, false); - scheduledTime = inputData.getLong(KEY_SCHEDULED_TIME, 0); - - logProgress("Parsed parameters - retry: " + retryCount + - ", immediate: " + isImmediate + - ", scheduled: " + scheduledTime); - - } catch (Exception e) { - Log.e(TAG, "Error parsing worker parameters", e); - } - } - - /** - * Perform fetch with timeout handling - * - * @return Notification content or null if failed - */ - private NotificationContent performFetchWithTimeout() { - try { - logProgress("Starting fetch with timeout: " + FETCH_TIMEOUT_MS + "ms"); - - long fetchStartTime = System.currentTimeMillis(); - - // Perform the actual fetch - NotificationContent content = fetcher.fetchContentImmediately(); - - long fetchDuration = System.currentTimeMillis() - fetchStartTime; - logProgress("Fetch completed in: " + fetchDuration + "ms"); - - // Validate content - if (content != null && isValidContent(content)) { - logProgress("Content validation passed"); - - // Save content using optimized storage - storage.saveNotificationContent(content); - - return content; - } else { - logProgress("Content validation failed"); - return null; - } - - } catch (Exception e) { - Log.e(TAG, "Error in fetch operation", e); - return null; - } - } - - /** - * Validate notification content - * - * @param content Content to validate - * @return true if content is valid - */ - private boolean isValidContent(NotificationContent content) { - if (content == null) { - return false; - } - - // Check essential fields - if (content.getId() == null || content.getId().isEmpty()) { - logProgress("Invalid content: missing ID"); - return false; - } - - if (content.getTitle() == null || content.getTitle().isEmpty()) { - logProgress("Invalid content: missing title"); - return false; - } - - if (content.getBody() == null || content.getBody().isEmpty()) { - logProgress("Invalid content: missing body"); - return false; - } - - if (content.getScheduledTime() <= 0) { - logProgress("Invalid content: invalid scheduled time"); - return false; - } - - return true; - } - - /** - * Check if work should be retried - * - * @return true if should retry - */ - private boolean shouldRetry() { - return retryCount < MAX_RETRY_ATTEMPTS; - } - - /** - * Get worker performance metrics - * - * @return Performance metrics - */ - public WorkerMetrics getFetchMetrics() { - WorkerMetrics metrics = getMetrics(); - - // Add fetch-specific metrics - metrics.retryCount = retryCount; - metrics.isImmediate = isImmediate; - metrics.scheduledTime = scheduledTime; - - return metrics; - } -} diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.java b/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.java index 98d0d45..2ffcea1 100644 --- a/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.java +++ b/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.java @@ -23,6 +23,8 @@ import android.content.pm.PackageManager; import android.database.sqlite.SQLiteDatabase; import android.os.Build; import android.os.PowerManager; +import android.os.StrictMode; +import android.os.Trace; import android.util.Log; import androidx.core.app.NotificationCompat; @@ -34,11 +36,13 @@ import com.getcapacitor.PluginCall; import com.getcapacitor.PluginMethod; import com.getcapacitor.annotation.CapacitorPlugin; import com.getcapacitor.annotation.Permission; +// BuildConfig will be available at compile time import java.util.Calendar; import java.util.concurrent.TimeUnit; import java.util.Map; import java.util.HashMap; +import java.util.List; import java.util.concurrent.CompletableFuture; import androidx.core.app.NotificationManagerCompat; @@ -104,9 +108,14 @@ public class DailyNotificationPlugin extends Plugin { @Override public void load() { super.load(); - Log.i(TAG, "Plugin loaded"); + Log.i(TAG, "DN|PLUGIN_LOAD_START"); + + // Initialize performance monitoring (debug builds only) + initializePerformanceMonitoring(); try { + Trace.beginSection("DN:pluginLoad"); + // Initialize system services notificationManager = (NotificationManager) getContext() .getSystemService(Context.NOTIFICATION_SERVICE); @@ -145,10 +154,128 @@ public class DailyNotificationPlugin extends Plugin { // Schedule next maintenance scheduleMaintenance(); - Log.i(TAG, "DailyNotificationPlugin initialized successfully"); + Log.i(TAG, "DN|PLUGIN_LOAD_OK"); + + } catch (Exception e) { + Log.e(TAG, "DN|PLUGIN_LOAD_ERR err=" + e.getMessage(), e); + } finally { + Trace.endSection(); + } + } + + /** + * Initialize performance monitoring for debug builds + * + * Enables StrictMode to catch main thread violations and adds + * performance monitoring capabilities for development. + */ + private void initializePerformanceMonitoring() { + try { + // Only enable StrictMode in debug builds + if (android.util.Log.isLoggable(TAG, android.util.Log.DEBUG)) { + Log.d(TAG, "DN|PERF_MONITOR_INIT debug_build=true"); + + // Enable StrictMode to catch main thread violations + StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() + .detectDiskReads() + .detectDiskWrites() + .detectNetwork() + .penaltyLog() + .penaltyFlashScreen() + .build()); + + StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() + .detectLeakedSqlLiteObjects() + .detectLeakedClosableObjects() + .penaltyLog() + .build()); + + Log.d(TAG, "DN|PERF_MONITOR_OK strictmode_enabled"); + } else { + Log.d(TAG, "DN|PERF_MONITOR_SKIP release_build"); + } } catch (Exception e) { - Log.e(TAG, "Failed to initialize DailyNotificationPlugin", e); + Log.e(TAG, "DN|PERF_MONITOR_ERR err=" + e.getMessage(), e); + } + } + + /** + * Perform app startup recovery + * + * @return true if recovery was performed, false otherwise + */ + private boolean performAppStartupRecovery() { + try { + Log.d(TAG, "DN|RECOVERY_START source=APP_STARTUP"); + + // Get all notifications from storage + List notifications = storage.getAllNotifications(); + + if (notifications.isEmpty()) { + Log.d(TAG, "DN|RECOVERY_SKIP no_notifications"); + return false; + } + + Log.d(TAG, "DN|RECOVERY_FOUND count=" + notifications.size()); + + int recoveredCount = 0; + long currentTime = System.currentTimeMillis(); + + for (NotificationContent notification : notifications) { + try { + if (notification.getScheduledTime() > currentTime) { + boolean scheduled = scheduler.scheduleNotification(notification); + if (scheduled) { + recoveredCount++; + Log.d(TAG, "DN|RECOVERY_OK id=" + notification.getId()); + } else { + Log.w(TAG, "DN|RECOVERY_FAIL id=" + notification.getId()); + } + } else { + Log.d(TAG, "DN|RECOVERY_SKIP_PAST id=" + notification.getId()); + } + } catch (Exception e) { + Log.e(TAG, "DN|RECOVERY_ERR id=" + notification.getId() + " err=" + e.getMessage(), e); + } + } + + Log.i(TAG, "DN|RECOVERY_COMPLETE recovered=" + recoveredCount + "/" + notifications.size()); + return recoveredCount > 0; + + } catch (Exception e) { + Log.e(TAG, "DN|RECOVERY_ERR exception=" + e.getMessage(), e); + return false; + } + } + + /** + * Get recovery statistics + * + * @return Recovery statistics string + */ + private String getRecoveryStats() { + try { + List notifications = storage.getAllNotifications(); + long currentTime = System.currentTimeMillis(); + + int futureCount = 0; + int pastCount = 0; + + for (NotificationContent notification : notifications) { + if (notification.getScheduledTime() > currentTime) { + futureCount++; + } else { + pastCount++; + } + } + + return String.format("Total: %d, Future: %d, Past: %d", + notifications.size(), futureCount, pastCount); + + } catch (Exception e) { + Log.e(TAG, "DN|RECOVERY_STATS_ERR err=" + e.getMessage(), e); + return "Error getting recovery stats: " + e.getMessage(); } } @@ -888,9 +1015,8 @@ public class DailyNotificationPlugin extends Plugin { // Ensure storage is initialized ensureStorageInitialized(); - // Use centralized recovery manager for idempotent recovery - RecoveryManager recoveryManager = RecoveryManager.getInstance(getContext(), storage, scheduler); - boolean recoveryPerformed = recoveryManager.performRecoveryIfNeeded("APP_STARTUP"); + // Perform app startup recovery + boolean recoveryPerformed = performAppStartupRecovery(); if (recoveryPerformed) { Log.i(TAG, "App startup recovery completed successfully"); @@ -911,8 +1037,7 @@ public class DailyNotificationPlugin extends Plugin { try { ensureStorageInitialized(); - RecoveryManager recoveryManager = RecoveryManager.getInstance(getContext(), storage, scheduler); - String stats = recoveryManager.getRecoveryStats(); + String stats = getRecoveryStats(); JSObject result = new JSObject(); result.put("stats", stats); @@ -1117,59 +1242,24 @@ public class DailyNotificationPlugin extends Plugin { */ @PluginMethod public void checkStatus(PluginCall call) { + Trace.beginSection("DN:checkStatus"); try { - Log.d(TAG, "Checking comprehensive notification status"); + Log.d(TAG, "DN|STATUS_CHECK_START"); ensureStorageInitialized(); - // Check POST_NOTIFICATIONS permission - boolean postNotificationsGranted = false; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - postNotificationsGranted = getContext().checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) - == PackageManager.PERMISSION_GRANTED; - } else { - postNotificationsGranted = true; // Pre-Android 13, always granted - } - - // Check channel status - boolean channelEnabled = channelManager.isChannelEnabled(); - int channelImportance = channelManager.getChannelImportance(); - - // Check exact alarm permission using PendingIntentManager - PendingIntentManager.AlarmStatus alarmStatus = scheduler.getAlarmStatus(); - boolean exactAlarmsGranted = alarmStatus.exactAlarmsGranted; - boolean canScheduleNow = postNotificationsGranted && channelEnabled && exactAlarmsGranted; - - // Get next scheduled notification time (if any) - long nextScheduledAt = -1; - try { - // This would need to be implemented to check actual scheduled alarms - // For now, return -1 to indicate unknown - } catch (Exception e) { - Log.w(TAG, "Could not determine next scheduled time", e); - } - - JSObject result = new JSObject(); - result.put("postNotificationsGranted", postNotificationsGranted); - result.put("channelEnabled", channelEnabled); - result.put("channelImportance", channelImportance); - result.put("exactAlarmsGranted", exactAlarmsGranted); - result.put("exactAlarmsSupported", alarmStatus.exactAlarmsSupported); - result.put("canScheduleNow", canScheduleNow); - result.put("nextScheduledAt", nextScheduledAt); - result.put("channelId", channelManager.getDefaultChannelId()); - result.put("androidVersion", alarmStatus.androidVersion); + // Use the comprehensive status checker + NotificationStatusChecker statusChecker = new NotificationStatusChecker(getContext()); + JSObject result = statusChecker.getComprehensiveStatus(); - Log.i(TAG, "Status check - canSchedule: " + canScheduleNow + - ", postNotifications: " + postNotificationsGranted + - ", channelEnabled: " + channelEnabled + - ", exactAlarms: " + exactAlarmsGranted + - ", alarmStatus: " + alarmStatus.toString()); + Log.i(TAG, "DN|STATUS_CHECK_OK canSchedule=" + result.getBoolean("canScheduleNow")); call.resolve(result); } catch (Exception e) { - Log.e(TAG, "Error checking status", e); + Log.e(TAG, "DN|STATUS_CHECK_ERR err=" + e.getMessage(), e); call.reject("Error checking status: " + e.getMessage()); + } finally { + Trace.endSection(); } } diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationPluginModular.java b/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationPluginModular.java deleted file mode 100644 index 5f5556a..0000000 --- a/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationPluginModular.java +++ /dev/null @@ -1,411 +0,0 @@ -/** - * DailyNotificationPlugin.java - Core Plugin Interface - * - * Modular Android implementation of the Daily Notification Plugin for Capacitor - * Delegates functionality to specialized manager classes for better maintainability - * - * @author Matthew Raymer - * @version 2.0.0 - Modular Architecture - */ - -package com.timesafari.dailynotification; - -import android.Manifest; -import android.content.Context; -import android.util.Log; - -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; - -/** - * Core plugin class for handling daily notifications on Android - * - * This modular plugin delegates functionality to specialized manager classes: - * - NotificationManager: Core notification operations - * - PermissionManager: Permission handling and settings - * - PowerManager: Battery and power management - * - RecoveryManager: Recovery and maintenance operations - * - ExactAlarmManager: Exact alarm management - * - TimeSafariIntegrationManager: TimeSafari-specific features - * - TaskCoordinationManager: Background task coordination - * - ReminderManager: Daily reminder management - */ -@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"; - - // Core system services - private Context context; - - // Specialized manager components - private NotificationManager notificationManager; - private PermissionManager permissionManager; - private PowerManager powerManager; - private RecoveryManager recoveryManager; - private ExactAlarmManager exactAlarmManager; - private TimeSafariIntegrationManager timeSafariIntegrationManager; - private TaskCoordinationManager taskCoordinationManager; - private ReminderManager reminderManager; - - // Core storage and scheduling components - private DailyNotificationStorage storage; - private DailyNotificationScheduler scheduler; - private ChannelManager channelManager; - - /** - * Initialize the plugin and all manager components - */ - @Override - public void load() { - super.load(); - Log.i(TAG, "Modular DailyNotificationPlugin loaded"); - - try { - context = getContext(); - - // Initialize core components - initializeCoreComponents(); - - // Initialize specialized managers - initializeManagers(); - - // Perform startup recovery if needed - performStartupRecovery(); - - Log.i(TAG, "Modular DailyNotificationPlugin initialized successfully"); - - } catch (Exception e) { - Log.e(TAG, "Failed to initialize DailyNotificationPlugin", e); - } - } - - /** - * Initialize core storage and scheduling components - */ - private void initializeCoreComponents() { - Log.d(TAG, "Initializing core components..."); - - storage = new DailyNotificationStorage(context); - scheduler = new DailyNotificationScheduler(context, - (android.app.AlarmManager) context.getSystemService(Context.ALARM_SERVICE)); - channelManager = new ChannelManager(context); - - // Ensure notification channel exists - if (!channelManager.ensureChannelExists()) { - Log.w(TAG, "Notification channel is blocked - notifications will not appear"); - channelManager.logChannelStatus(); - } - - Log.d(TAG, "Core components initialized"); - } - - /** - * Initialize all specialized manager components - */ - private void initializeManagers() { - Log.d(TAG, "Initializing specialized managers..."); - - notificationManager = new NotificationManager(context, storage, scheduler, channelManager); - permissionManager = new PermissionManager(context, channelManager); - powerManager = new PowerManager(context); - recoveryManager = new RecoveryManager(context, storage, scheduler); - exactAlarmManager = new ExactAlarmManager(context); - timeSafariIntegrationManager = new TimeSafariIntegrationManager(context, storage); - taskCoordinationManager = new TaskCoordinationManager(context, storage); - reminderManager = new ReminderManager(context, storage, scheduler); - - Log.d(TAG, "Specialized managers initialized"); - } - - /** - * Perform startup recovery if needed - */ - private void performStartupRecovery() { - Log.d(TAG, "Checking if startup recovery is needed..."); - - try { - RecoveryManager recoveryManager = RecoveryManager.getInstance(context, storage, scheduler); - boolean recoveryPerformed = recoveryManager.performRecoveryIfNeeded("APP_STARTUP"); - - if (recoveryPerformed) { - Log.i(TAG, "Startup recovery completed successfully"); - } else { - Log.d(TAG, "Startup recovery skipped (not needed or already performed)"); - } - } catch (Exception e) { - Log.e(TAG, "Error during startup recovery", e); - } - } - - // ============================================================================ - // CORE PLUGIN METHODS - Delegation to specialized managers - // ============================================================================ - - /** - * Configure the plugin with database and storage options - * Delegates to NotificationManager for configuration handling - */ - @PluginMethod - public void configure(PluginCall call) { - Log.d(TAG, "Delegating configure to NotificationManager"); - notificationManager.configure(call); - } - - /** - * Get comprehensive status of the notification system - * Delegates to PermissionManager for status checking - */ - @PluginMethod - public void checkStatus(PluginCall call) { - Log.d(TAG, "Delegating checkStatus to PermissionManager"); - permissionManager.checkStatus(call); - } - - // ============================================================================ - // NOTIFICATION MANAGEMENT METHODS - Delegation to NotificationManager - // ============================================================================ - - @PluginMethod - public void scheduleDailyNotification(PluginCall call) { - Log.d(TAG, "Delegating scheduleDailyNotification to NotificationManager"); - notificationManager.scheduleDailyNotification(call); - } - - @PluginMethod - public void getLastNotification(PluginCall call) { - Log.d(TAG, "Delegating getLastNotification to NotificationManager"); - notificationManager.getLastNotification(call); - } - - @PluginMethod - public void cancelAllNotifications(PluginCall call) { - Log.d(TAG, "Delegating cancelAllNotifications to NotificationManager"); - notificationManager.cancelAllNotifications(call); - } - - @PluginMethod - public void getNotificationStatus(PluginCall call) { - Log.d(TAG, "Delegating getNotificationStatus to NotificationManager"); - notificationManager.getNotificationStatus(call); - } - - @PluginMethod - public void updateSettings(PluginCall call) { - Log.d(TAG, "Delegating updateSettings to NotificationManager"); - notificationManager.updateSettings(call); - } - - // ============================================================================ - // PERMISSION MANAGEMENT METHODS - Delegation to PermissionManager - // ============================================================================ - - @PluginMethod - public void requestNotificationPermissions(PluginCall call) { - Log.d(TAG, "Delegating requestNotificationPermissions to PermissionManager"); - permissionManager.requestNotificationPermissions(call); - } - - @PluginMethod - public void checkPermissionStatus(PluginCall call) { - Log.d(TAG, "Delegating checkPermissionStatus to PermissionManager"); - permissionManager.checkPermissionStatus(call); - } - - @PluginMethod - public void openExactAlarmSettings(PluginCall call) { - Log.d(TAG, "Delegating openExactAlarmSettings to PermissionManager"); - permissionManager.openExactAlarmSettings(call); - } - - @PluginMethod - public void isChannelEnabled(PluginCall call) { - Log.d(TAG, "Delegating isChannelEnabled to PermissionManager"); - permissionManager.isChannelEnabled(call); - } - - @PluginMethod - public void openChannelSettings(PluginCall call) { - Log.d(TAG, "Delegating openChannelSettings to PermissionManager"); - permissionManager.openChannelSettings(call); - } - - // ============================================================================ - // POWER MANAGEMENT METHODS - Delegation to PowerManager - // ============================================================================ - - @PluginMethod - public void getBatteryStatus(PluginCall call) { - Log.d(TAG, "Delegating getBatteryStatus to PowerManager"); - powerManager.getBatteryStatus(call); - } - - @PluginMethod - public void requestBatteryOptimizationExemption(PluginCall call) { - Log.d(TAG, "Delegating requestBatteryOptimizationExemption to PowerManager"); - powerManager.requestBatteryOptimizationExemption(call); - } - - @PluginMethod - public void setAdaptiveScheduling(PluginCall call) { - Log.d(TAG, "Delegating setAdaptiveScheduling to PowerManager"); - powerManager.setAdaptiveScheduling(call); - } - - @PluginMethod - public void getPowerState(PluginCall call) { - Log.d(TAG, "Delegating getPowerState to PowerManager"); - powerManager.getPowerState(call); - } - - // ============================================================================ - // RECOVERY MANAGEMENT METHODS - Delegation to RecoveryManager - // ============================================================================ - - @PluginMethod - public void getRecoveryStats(PluginCall call) { - Log.d(TAG, "Delegating getRecoveryStats to RecoveryManager"); - recoveryManager.getRecoveryStats(call); - } - - @PluginMethod - public void maintainRollingWindow(PluginCall call) { - Log.d(TAG, "Delegating maintainRollingWindow to RecoveryManager"); - recoveryManager.maintainRollingWindow(call); - } - - @PluginMethod - public void getRollingWindowStats(PluginCall call) { - Log.d(TAG, "Delegating getRollingWindowStats to RecoveryManager"); - recoveryManager.getRollingWindowStats(call); - } - - @PluginMethod - public void getRebootRecoveryStatus(PluginCall call) { - Log.d(TAG, "Delegating getRebootRecoveryStatus to RecoveryManager"); - recoveryManager.getRebootRecoveryStatus(call); - } - - // ============================================================================ - // EXACT ALARM MANAGEMENT METHODS - Delegation to ExactAlarmManager - // ============================================================================ - - @PluginMethod - public void getExactAlarmStatus(PluginCall call) { - Log.d(TAG, "Delegating getExactAlarmStatus to ExactAlarmManager"); - exactAlarmManager.getExactAlarmStatus(call); - } - - @PluginMethod - public void requestExactAlarmPermission(PluginCall call) { - Log.d(TAG, "Delegating requestExactAlarmPermission to ExactAlarmManager"); - exactAlarmManager.requestExactAlarmPermission(call); - } - - // ============================================================================ - // TIMESAFARI INTEGRATION METHODS - Delegation to TimeSafariIntegrationManager - // ============================================================================ - - @PluginMethod - public void setActiveDidFromHost(PluginCall call) { - Log.d(TAG, "Delegating setActiveDidFromHost to TimeSafariIntegrationManager"); - timeSafariIntegrationManager.setActiveDidFromHost(call); - } - - @PluginMethod - public void refreshAuthenticationForNewIdentity(PluginCall call) { - Log.d(TAG, "Delegating refreshAuthenticationForNewIdentity to TimeSafariIntegrationManager"); - timeSafariIntegrationManager.refreshAuthenticationForNewIdentity(call); - } - - @PluginMethod - public void clearCacheForNewIdentity(PluginCall call) { - Log.d(TAG, "Delegating clearCacheForNewIdentity to TimeSafariIntegrationManager"); - timeSafariIntegrationManager.clearCacheForNewIdentity(call); - } - - @PluginMethod - public void updateBackgroundTaskIdentity(PluginCall call) { - Log.d(TAG, "Delegating updateBackgroundTaskIdentity to TimeSafariIntegrationManager"); - timeSafariIntegrationManager.updateBackgroundTaskIdentity(call); - } - - @PluginMethod - public void testJWTGeneration(PluginCall call) { - Log.d(TAG, "Delegating testJWTGeneration to TimeSafariIntegrationManager"); - timeSafariIntegrationManager.testJWTGeneration(call); - } - - @PluginMethod - public void testEndorserAPI(PluginCall call) { - Log.d(TAG, "Delegating testEndorserAPI to TimeSafariIntegrationManager"); - timeSafariIntegrationManager.testEndorserAPI(call); - } - - // ============================================================================ - // TASK COORDINATION METHODS - Delegation to TaskCoordinationManager - // ============================================================================ - - @PluginMethod - public void coordinateBackgroundTasks(PluginCall call) { - Log.d(TAG, "Delegating coordinateBackgroundTasks to TaskCoordinationManager"); - taskCoordinationManager.coordinateBackgroundTasks(call); - } - - @PluginMethod - public void handleAppLifecycleEvent(PluginCall call) { - Log.d(TAG, "Delegating handleAppLifecycleEvent to TaskCoordinationManager"); - taskCoordinationManager.handleAppLifecycleEvent(call); - } - - @PluginMethod - public void getCoordinationStatus(PluginCall call) { - Log.d(TAG, "Delegating getCoordinationStatus to TaskCoordinationManager"); - taskCoordinationManager.getCoordinationStatus(call); - } - - // ============================================================================ - // REMINDER MANAGEMENT METHODS - Delegation to ReminderManager - // ============================================================================ - - @PluginMethod - public void scheduleDailyReminder(PluginCall call) { - Log.d(TAG, "Delegating scheduleDailyReminder to ReminderManager"); - reminderManager.scheduleDailyReminder(call); - } - - @PluginMethod - public void cancelDailyReminder(PluginCall call) { - Log.d(TAG, "Delegating cancelDailyReminder to ReminderManager"); - reminderManager.cancelDailyReminder(call); - } - - @PluginMethod - public void getScheduledReminders(PluginCall call) { - Log.d(TAG, "Delegating getScheduledReminders to ReminderManager"); - reminderManager.getScheduledReminders(call); - } - - @PluginMethod - public void updateDailyReminder(PluginCall call) { - Log.d(TAG, "Delegating updateDailyReminder to ReminderManager"); - reminderManager.updateDailyReminder(call); - } -} diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationReceiver.java b/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationReceiver.java index 0ac8e53..ed2bd6c 100644 --- a/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationReceiver.java +++ b/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationReceiver.java @@ -16,9 +16,13 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.os.Build; +import android.os.Trace; import android.util.Log; import androidx.core.app.NotificationCompat; +import androidx.work.Data; +import androidx.work.OneTimeWorkRequest; +import androidx.work.WorkManager; /** * Broadcast receiver for daily notification alarms @@ -36,28 +40,103 @@ public class DailyNotificationReceiver extends BroadcastReceiver { /** * Handle broadcast intent when alarm triggers * + * Ultra-lightweight receiver that only parses intent and enqueues work. + * All heavy operations (storage, JSON, scheduling) are moved to WorkManager. + * * @param context Application context * @param intent Broadcast intent */ @Override public void onReceive(Context context, Intent intent) { + Trace.beginSection("DN:onReceive"); try { - Log.d(TAG, "Received notification broadcast"); + Log.d(TAG, "DN|RECEIVE_START action=" + intent.getAction()); String action = intent.getAction(); if (action == null) { - Log.w(TAG, "Received intent with null action"); + Log.w(TAG, "DN|RECEIVE_ERR null_action"); return; } if ("com.timesafari.daily.NOTIFICATION".equals(action)) { - handleNotificationIntent(context, intent); + // Parse intent and enqueue work - keep receiver ultra-light + String notificationId = intent.getStringExtra(EXTRA_NOTIFICATION_ID); + if (notificationId == null) { + Log.w(TAG, "DN|RECEIVE_ERR missing_id"); + return; + } + + // Enqueue work immediately - don't block receiver + enqueueNotificationWork(context, notificationId); + Log.d(TAG, "DN|RECEIVE_OK enqueued=" + notificationId); + + } else if ("com.timesafari.daily.DISMISS".equals(action)) { + // Handle dismissal - also lightweight + String notificationId = intent.getStringExtra(EXTRA_NOTIFICATION_ID); + if (notificationId != null) { + enqueueDismissalWork(context, notificationId); + Log.d(TAG, "DN|DISMISS_OK enqueued=" + notificationId); + } } else { - Log.w(TAG, "Unknown action: " + action); + Log.w(TAG, "DN|RECEIVE_ERR unknown_action=" + action); } } catch (Exception e) { - Log.e(TAG, "Error handling broadcast", e); + Log.e(TAG, "DN|RECEIVE_ERR exception=" + e.getMessage(), e); + } finally { + Trace.endSection(); + } + } + + /** + * Enqueue notification processing work to WorkManager + * + * @param context Application context + * @param notificationId ID of notification to process + */ + private void enqueueNotificationWork(Context context, String notificationId) { + try { + Data inputData = new Data.Builder() + .putString("notification_id", notificationId) + .putString("action", "display") + .build(); + + OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(DailyNotificationWorker.class) + .setInputData(inputData) + .addTag("daily_notification_display") + .build(); + + WorkManager.getInstance(context).enqueue(workRequest); + Log.d(TAG, "DN|WORK_ENQUEUE display=" + notificationId); + + } catch (Exception e) { + Log.e(TAG, "DN|WORK_ENQUEUE_ERR display=" + notificationId + " err=" + e.getMessage(), e); + } + } + + /** + * Enqueue notification dismissal work to WorkManager + * + * @param context Application context + * @param notificationId ID of notification to dismiss + */ + private void enqueueDismissalWork(Context context, String notificationId) { + try { + Data inputData = new Data.Builder() + .putString("notification_id", notificationId) + .putString("action", "dismiss") + .build(); + + OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(DailyNotificationWorker.class) + .setInputData(inputData) + .addTag("daily_notification_dismiss") + .build(); + + WorkManager.getInstance(context).enqueue(workRequest); + Log.d(TAG, "DN|WORK_ENQUEUE dismiss=" + notificationId); + + } catch (Exception e) { + Log.e(TAG, "DN|WORK_ENQUEUE_ERR dismiss=" + notificationId + " err=" + e.getMessage(), e); } } diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationStorage.java b/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationStorage.java index d3e6fb5..ff10972 100644 --- a/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationStorage.java +++ b/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationStorage.java @@ -44,6 +44,9 @@ public class DailyNotificationStorage { private static final int MAX_CACHE_SIZE = 100; // Maximum notifications to keep in memory private static final long CACHE_CLEANUP_INTERVAL = 24 * 60 * 60 * 1000; // 24 hours + private static final int MAX_STORAGE_ENTRIES = 100; // Maximum total storage entries + private static final long RETENTION_PERIOD_MS = 14 * 24 * 60 * 60 * 1000; // 14 days + private static final int BATCH_CLEANUP_SIZE = 50; // Clean up in batches private final Context context; private final SharedPreferences prefs; @@ -77,7 +80,7 @@ public class DailyNotificationStorage { */ public void saveNotificationContent(NotificationContent content) { try { - Log.d(TAG, "Saving notification: " + content.getId()); + Log.d(TAG, "DN|STORAGE_SAVE_START id=" + content.getId()); // Add to cache notificationCache.put(content.getId(), content); @@ -88,12 +91,15 @@ public class DailyNotificationStorage { notificationList.add(content); Collections.sort(notificationList, Comparator.comparingLong(NotificationContent::getScheduledTime)); + + // Apply storage cap and retention policy + enforceStorageLimits(); } // Persist to SharedPreferences saveNotificationsToStorage(); - Log.d(TAG, "Notification saved successfully"); + Log.d(TAG, "DN|STORAGE_SAVE_OK id=" + content.getId() + " total=" + notificationList.size()); } catch (Exception e) { Log.e(TAG, "Error saving notification content", e); @@ -477,4 +483,90 @@ public class DailyNotificationStorage { notificationCache.size(), getLastFetchTime()); } + + /** + * Enforce storage limits and retention policy + * + * This method implements both storage capping (max entries) and retention policy + * (remove old entries) to prevent unbounded growth. + */ + private void enforceStorageLimits() { + try { + long currentTime = System.currentTimeMillis(); + int initialSize = notificationList.size(); + int removedCount = 0; + + // First, remove expired entries (older than retention period) + notificationList.removeIf(notification -> { + long age = currentTime - notification.getScheduledTime(); + return age > RETENTION_PERIOD_MS; + }); + + removedCount = initialSize - notificationList.size(); + if (removedCount > 0) { + Log.d(TAG, "DN|RETENTION_CLEANUP removed=" + removedCount + " expired_entries"); + } + + // Then, enforce storage cap by removing oldest entries if over limit + while (notificationList.size() > MAX_STORAGE_ENTRIES) { + NotificationContent oldest = notificationList.remove(0); + notificationCache.remove(oldest.getId()); + removedCount++; + } + + if (removedCount > 0) { + Log.i(TAG, "DN|STORAGE_LIMITS_ENFORCED removed=" + removedCount + + " total=" + notificationList.size() + + " max=" + MAX_STORAGE_ENTRIES); + } + + } catch (Exception e) { + Log.e(TAG, "DN|STORAGE_LIMITS_ERR err=" + e.getMessage(), e); + } + } + + /** + * Perform batch cleanup of old notifications + * + * This method can be called periodically to clean up old notifications + * in batches to avoid blocking the main thread. + * + * @return Number of notifications removed + */ + public int performBatchCleanup() { + try { + long currentTime = System.currentTimeMillis(); + int removedCount = 0; + int batchSize = 0; + + synchronized (notificationList) { + java.util.Iterator iterator = notificationList.iterator(); + + while (iterator.hasNext() && batchSize < BATCH_CLEANUP_SIZE) { + NotificationContent notification = iterator.next(); + long age = currentTime - notification.getScheduledTime(); + + if (age > RETENTION_PERIOD_MS) { + iterator.remove(); + notificationCache.remove(notification.getId()); + removedCount++; + batchSize++; + } + } + } + + if (removedCount > 0) { + saveNotificationsToStorage(); + Log.i(TAG, "DN|BATCH_CLEANUP_OK removed=" + removedCount + + " batch_size=" + batchSize + + " remaining=" + notificationList.size()); + } + + return removedCount; + + } catch (Exception e) { + Log.e(TAG, "DN|BATCH_CLEANUP_ERR err=" + e.getMessage(), e); + return 0; + } + } } diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationStorageOptimized.java b/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationStorageOptimized.java deleted file mode 100644 index d711893..0000000 --- a/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationStorageOptimized.java +++ /dev/null @@ -1,548 +0,0 @@ -/** - * DailyNotificationStorageOptimized.java - * - * Optimized storage management with Room hot path optimizations and JSON cleanup - * Implements efficient caching, batch operations, and reduced JSON serialization - * - * @author Matthew Raymer - * @version 2.0.0 - Optimized Architecture - */ - -package com.timesafari.dailynotification; - -import android.content.Context; -import android.content.SharedPreferences; -import android.util.Log; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.reflect.TypeToken; - -import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.locks.ReadWriteLock; -import java.util.concurrent.locks.ReentrantReadWriteLock; - -/** - * Optimized storage manager with Room hot path optimizations - * - * Optimizations: - * - Read-write locks for thread safety - * - Batch operations to reduce JSON serialization - * - Lazy loading and caching strategies - * - Reduced memory allocations - * - Optimized JSON handling - */ -public class DailyNotificationStorageOptimized { - - private static final String TAG = "DailyNotificationStorageOptimized"; - private static final String PREFS_NAME = "DailyNotificationPrefs"; - private static final String KEY_NOTIFICATIONS = "notifications"; - private static final String KEY_SETTINGS = "settings"; - private static final String KEY_LAST_FETCH = "last_fetch"; - private static final String KEY_ADAPTIVE_SCHEDULING = "adaptive_scheduling"; - - // Optimization constants - private static final int MAX_CACHE_SIZE = 100; - private static final long CACHE_CLEANUP_INTERVAL = 24 * 60 * 60 * 1000; - private static final int BATCH_SIZE = 10; // Batch operations for efficiency - private static final boolean ENABLE_LAZY_LOADING = true; - - private final Context context; - private final SharedPreferences prefs; - private final Gson gson; - - // Thread-safe collections with read-write locks - private final ConcurrentHashMap notificationCache; - private final List notificationList; - private final ReadWriteLock cacheLock = new ReentrantReadWriteLock(); - - // Optimization flags - private boolean cacheDirty = false; - private long lastCacheUpdate = 0; - private boolean lazyLoadingEnabled = ENABLE_LAZY_LOADING; - - /** - * Constructor with optimized initialization - * - * @param context Application context - */ - public DailyNotificationStorageOptimized(Context context) { - this.context = context; - this.prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); - - // Optimized Gson configuration - this.gson = createOptimizedGson(); - - // Initialize collections - this.notificationCache = new ConcurrentHashMap<>(MAX_CACHE_SIZE); - this.notificationList = Collections.synchronizedList(new ArrayList<>()); - - // Load data with optimization - loadNotificationsOptimized(); - - Log.d(TAG, "Optimized storage initialized"); - } - - /** - * Create optimized Gson instance with reduced overhead - */ - private Gson createOptimizedGson() { - GsonBuilder builder = new GsonBuilder(); - - // Disable HTML escaping for better performance - builder.disableHtmlEscaping(); - - // Use custom deserializer for NotificationContent - builder.registerTypeAdapter(NotificationContent.class, - new NotificationContent.NotificationContentDeserializer()); - - // Configure for performance - builder.setLenient(); - - return builder.create(); - } - - /** - * Optimized notification loading with lazy loading support - */ - private void loadNotificationsOptimized() { - cacheLock.writeLock().lock(); - try { - if (lazyLoadingEnabled) { - // Load only essential data first - loadEssentialData(); - } else { - // Load all data - loadAllNotifications(); - } - } finally { - cacheLock.writeLock().unlock(); - } - } - - /** - * Load only essential notification data - */ - private void loadEssentialData() { - try { - String notificationsJson = prefs.getString(KEY_NOTIFICATIONS, "[]"); - - if (notificationsJson.length() > 1000) { // Large dataset - // Load only IDs and scheduled times for large datasets - loadNotificationMetadata(notificationsJson); - } else { - // Load full data for small datasets - loadAllNotifications(); - } - - } catch (Exception e) { - Log.e(TAG, "Error loading essential data", e); - } - } - - /** - * Load notification metadata only (IDs and scheduled times) - */ - private void loadNotificationMetadata(String notificationsJson) { - try { - Type type = new TypeToken>(){}.getType(); - List notifications = gson.fromJson(notificationsJson, type); - - if (notifications != null) { - for (NotificationContent notification : notifications) { - // Store only essential data in cache - NotificationContent metadata = new NotificationContent(); - metadata.setId(notification.getId()); - metadata.setScheduledTime(notification.getScheduledTime()); - metadata.setFetchedAt(notification.getFetchedAt()); - - notificationCache.put(notification.getId(), metadata); - notificationList.add(metadata); - } - - // Sort by scheduled time - Collections.sort(notificationList, - Comparator.comparingLong(NotificationContent::getScheduledTime)); - - Log.d(TAG, "Loaded " + notifications.size() + " notification metadata"); - } - - } catch (Exception e) { - Log.e(TAG, "Error loading notification metadata", e); - } - } - - /** - * Load all notification data - */ - private void loadAllNotifications() { - try { - String notificationsJson = prefs.getString(KEY_NOTIFICATIONS, "[]"); - Type type = new TypeToken>(){}.getType(); - List notifications = gson.fromJson(notificationsJson, type); - - if (notifications != null) { - for (NotificationContent notification : notifications) { - notificationCache.put(notification.getId(), notification); - notificationList.add(notification); - } - - // Sort by scheduled time - Collections.sort(notificationList, - Comparator.comparingLong(NotificationContent::getScheduledTime)); - - Log.d(TAG, "Loaded " + notifications.size() + " notifications"); - } - - } catch (Exception e) { - Log.e(TAG, "Error loading all notifications", e); - } - } - - /** - * Optimized save with batch operations - * - * @param content Notification content to save - */ - public void saveNotificationContent(NotificationContent content) { - cacheLock.writeLock().lock(); - try { - Log.d(TAG, "Saving notification: " + content.getId()); - - // Add to cache - notificationCache.put(content.getId(), content); - - // Add to list and maintain sort order - notificationList.removeIf(n -> n.getId().equals(content.getId())); - notificationList.add(content); - Collections.sort(notificationList, - Comparator.comparingLong(NotificationContent::getScheduledTime)); - - // Mark cache as dirty - cacheDirty = true; - - // Batch save if needed - if (shouldBatchSave()) { - saveNotificationsBatch(); - } - - Log.d(TAG, "Notification saved successfully"); - - } finally { - cacheLock.writeLock().unlock(); - } - } - - /** - * Optimized get with read lock - * - * @param id Notification ID - * @return Notification content or null if not found - */ - public NotificationContent getNotificationContent(String id) { - cacheLock.readLock().lock(); - try { - NotificationContent content = notificationCache.get(id); - - // Lazy load full content if only metadata is cached - if (content != null && lazyLoadingEnabled && isMetadataOnly(content)) { - content = loadFullContent(id); - } - - return content; - } finally { - cacheLock.readLock().unlock(); - } - } - - /** - * Check if content is metadata only - */ - private boolean isMetadataOnly(NotificationContent content) { - return content.getTitle() == null || content.getTitle().isEmpty(); - } - - /** - * Load full content for metadata-only entries - */ - private NotificationContent loadFullContent(String id) { - // This would load full content from persistent storage - // For now, return the cached content - return notificationCache.get(id); - } - - /** - * Optimized get all notifications with read lock - * - * @return List of all notifications - */ - public List getAllNotifications() { - cacheLock.readLock().lock(); - try { - return new ArrayList<>(notificationList); - } finally { - cacheLock.readLock().unlock(); - } - } - - /** - * Optimized get next notification - * - * @return Next notification or null if none scheduled - */ - public NotificationContent getNextNotification() { - cacheLock.readLock().lock(); - try { - long currentTime = System.currentTimeMillis(); - - for (NotificationContent notification : notificationList) { - if (notification.getScheduledTime() > currentTime) { - return notification; - } - } - - return null; - } finally { - cacheLock.readLock().unlock(); - } - } - - /** - * Optimized remove with batch operations - * - * @param id Notification ID to remove - */ - public void removeNotification(String id) { - cacheLock.writeLock().lock(); - try { - Log.d(TAG, "Removing notification: " + id); - - notificationCache.remove(id); - notificationList.removeIf(n -> n.getId().equals(id)); - - // Mark cache as dirty - cacheDirty = true; - - // Batch save if needed - if (shouldBatchSave()) { - saveNotificationsBatch(); - } - - Log.d(TAG, "Notification removed successfully"); - - } finally { - cacheLock.writeLock().unlock(); - } - } - - /** - * Optimized clear all with batch operations - */ - public void clearAllNotifications() { - cacheLock.writeLock().lock(); - try { - Log.d(TAG, "Clearing all notifications"); - - notificationCache.clear(); - notificationList.clear(); - - // Mark cache as dirty - cacheDirty = true; - - // Immediate save for clear operation - saveNotificationsBatch(); - - Log.d(TAG, "All notifications cleared successfully"); - - } finally { - cacheLock.writeLock().unlock(); - } - } - - /** - * Check if batch save is needed - */ - private boolean shouldBatchSave() { - return cacheDirty && (System.currentTimeMillis() - lastCacheUpdate > 1000); - } - - /** - * Batch save notifications to reduce JSON serialization overhead - */ - private void saveNotificationsBatch() { - try { - String notificationsJson = gson.toJson(notificationList); - - SharedPreferences.Editor editor = prefs.edit(); - editor.putString(KEY_NOTIFICATIONS, notificationsJson); - editor.apply(); - - cacheDirty = false; - lastCacheUpdate = System.currentTimeMillis(); - - Log.d(TAG, "Batch save completed: " + notificationList.size() + " notifications"); - - } catch (Exception e) { - Log.e(TAG, "Error in batch save", e); - } - } - - /** - * Force save all pending changes - */ - public void flush() { - cacheLock.writeLock().lock(); - try { - if (cacheDirty) { - saveNotificationsBatch(); - } - } finally { - cacheLock.writeLock().unlock(); - } - } - - /** - * Optimized settings management with reduced JSON operations - */ - - // Settings cache to reduce SharedPreferences access - private final ConcurrentHashMap settingsCache = new ConcurrentHashMap<>(); - private boolean settingsCacheDirty = false; - - /** - * Set setting with caching - * - * @param key Setting key - * @param value Setting value - */ - public void setSetting(String key, String value) { - settingsCache.put(key, value); - settingsCacheDirty = true; - - // Batch save settings - if (shouldBatchSaveSettings()) { - saveSettingsBatch(); - } - } - - /** - * Get setting with caching - * - * @param key Setting key - * @param defaultValue Default value - * @return Setting value - */ - public String getSetting(String key, String defaultValue) { - Object cached = settingsCache.get(key); - if (cached != null) { - return cached.toString(); - } - - // Load from SharedPreferences and cache - String value = prefs.getString(key, defaultValue); - settingsCache.put(key, value); - return value; - } - - /** - * Check if batch save settings is needed - */ - private boolean shouldBatchSaveSettings() { - return settingsCacheDirty; - } - - /** - * Batch save settings to reduce SharedPreferences operations - */ - private void saveSettingsBatch() { - try { - SharedPreferences.Editor editor = prefs.edit(); - - for (String key : settingsCache.keySet()) { - Object value = settingsCache.get(key); - if (value instanceof String) { - editor.putString(key, (String) value); - } else if (value instanceof Boolean) { - editor.putBoolean(key, (Boolean) value); - } else if (value instanceof Long) { - editor.putLong(key, (Long) value); - } else if (value instanceof Integer) { - editor.putInt(key, (Integer) value); - } - } - - editor.apply(); - settingsCacheDirty = false; - - Log.d(TAG, "Settings batch save completed: " + settingsCache.size() + " settings"); - - } catch (Exception e) { - Log.e(TAG, "Error in settings batch save", e); - } - } - - /** - * Get notification count (optimized) - * - * @return Number of notifications - */ - public int getNotificationCount() { - cacheLock.readLock().lock(); - try { - return notificationCache.size(); - } finally { - cacheLock.readLock().unlock(); - } - } - - /** - * Check if storage is empty (optimized) - * - * @return true if no notifications exist - */ - public boolean isEmpty() { - cacheLock.readLock().lock(); - try { - return notificationCache.isEmpty(); - } finally { - cacheLock.readLock().unlock(); - } - } - - /** - * Get scheduled notifications count (optimized) - * - * @return Number of scheduled notifications - */ - public int getScheduledNotificationsCount() { - cacheLock.readLock().lock(); - try { - long currentTime = System.currentTimeMillis(); - int count = 0; - - for (NotificationContent notification : notificationList) { - if (notification.getScheduledTime() > currentTime) { - count++; - } - } - - return count; - } finally { - cacheLock.readLock().unlock(); - } - } - - /** - * Delete notification content by ID - * - * @param id Notification ID - */ - public void deleteNotificationContent(String id) { - removeNotification(id); - } -} diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationWorker.java b/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationWorker.java new file mode 100644 index 0000000..7df5a07 --- /dev/null +++ b/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationWorker.java @@ -0,0 +1,430 @@ +/** + * DailyNotificationWorker.java + * + * WorkManager worker for handling notification processing + * Moves heavy operations (storage, JSON, scheduling) out of BroadcastReceiver + * + * @author Matthew Raymer + * @version 1.0.0 + */ + +package com.timesafari.dailynotification; + +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.os.Trace; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.core.app.NotificationCompat; +import androidx.work.Data; +import androidx.work.Worker; +import androidx.work.WorkerParameters; + +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + +/** + * WorkManager worker for processing daily notifications + * + * This worker handles the heavy operations that were previously done in + * the BroadcastReceiver, ensuring the receiver stays ultra-lightweight. + */ +public class DailyNotificationWorker extends Worker { + + private static final String TAG = "DailyNotificationWorker"; + private static final String CHANNEL_ID = "timesafari.daily"; + + public DailyNotificationWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { + super(context, workerParams); + } + + @NonNull + @Override + public Result doWork() { + Trace.beginSection("DN:Worker"); + try { + Data inputData = getInputData(); + String notificationId = inputData.getString("notification_id"); + String action = inputData.getString("action"); + + if (notificationId == null || action == null) { + Log.e(TAG, "DN|WORK_ERR missing_params id=" + notificationId + " action=" + action); + return Result.failure(); + } + + Log.d(TAG, "DN|WORK_START id=" + notificationId + " action=" + action); + + if ("display".equals(action)) { + return handleDisplayNotification(notificationId); + } else if ("dismiss".equals(action)) { + return handleDismissNotification(notificationId); + } else { + Log.e(TAG, "DN|WORK_ERR unknown_action=" + action); + return Result.failure(); + } + + } catch (Exception e) { + Log.e(TAG, "DN|WORK_ERR exception=" + e.getMessage(), e); + return Result.retry(); + } finally { + Trace.endSection(); + } + } + + /** + * Handle notification display + * + * @param notificationId ID of notification to display + * @return Work result + */ + private Result handleDisplayNotification(String notificationId) { + Trace.beginSection("DN:display"); + try { + Log.d(TAG, "DN|DISPLAY_START id=" + notificationId); + + // Get notification content from storage + DailyNotificationStorage storage = new DailyNotificationStorage(getApplicationContext()); + NotificationContent content = storage.getNotificationContent(notificationId); + + if (content == null) { + Log.w(TAG, "DN|DISPLAY_ERR content_not_found id=" + notificationId); + return Result.failure(); + } + + // Check if notification is ready to display + if (!content.isReadyToDisplay()) { + Log.d(TAG, "DN|DISPLAY_SKIP not_ready id=" + notificationId); + return Result.success(); + } + + // JIT Freshness Re-check (Soft TTL) + content = performJITFreshnessCheck(content); + + // Display the notification + boolean displayed = displayNotification(content); + + if (displayed) { + // Schedule next notification if this is a recurring daily notification + scheduleNextNotification(content); + + Log.i(TAG, "DN|DISPLAY_OK id=" + notificationId); + return Result.success(); + } else { + Log.e(TAG, "DN|DISPLAY_ERR display_failed id=" + notificationId); + return Result.retry(); + } + + } catch (Exception e) { + Log.e(TAG, "DN|DISPLAY_ERR exception id=" + notificationId + " err=" + e.getMessage(), e); + return Result.retry(); + } finally { + Trace.endSection(); + } + } + + /** + * Handle notification dismissal + * + * @param notificationId ID of notification to dismiss + * @return Work result + */ + private Result handleDismissNotification(String notificationId) { + Trace.beginSection("DN:dismiss"); + try { + Log.d(TAG, "DN|DISMISS_START id=" + notificationId); + + // Remove from storage + DailyNotificationStorage storage = new DailyNotificationStorage(getApplicationContext()); + storage.removeNotification(notificationId); + + // Cancel any pending alarms + DailyNotificationScheduler scheduler = new DailyNotificationScheduler( + getApplicationContext(), + (android.app.AlarmManager) getApplicationContext().getSystemService(Context.ALARM_SERVICE) + ); + scheduler.cancelNotification(notificationId); + + Log.i(TAG, "DN|DISMISS_OK id=" + notificationId); + return Result.success(); + + } catch (Exception e) { + Log.e(TAG, "DN|DISMISS_ERR exception id=" + notificationId + " err=" + e.getMessage(), e); + return Result.retry(); + } finally { + Trace.endSection(); + } + } + + /** + * Perform JIT (Just-In-Time) freshness re-check for notification content + * + * @param content Original notification content + * @return Updated content if refresh succeeded, original content otherwise + */ + private NotificationContent performJITFreshnessCheck(NotificationContent content) { + Trace.beginSection("DN:jitCheck"); + try { + // Check if content is stale (older than 6 hours for JIT check) + long currentTime = System.currentTimeMillis(); + long age = currentTime - content.getFetchedAt(); + long staleThreshold = 6 * 60 * 60 * 1000; // 6 hours in milliseconds + int ageMinutes = (int) (age / 1000 / 60); + + if (age < staleThreshold) { + Log.d(TAG, "DN|JIT_FRESH skip=true ageMin=" + ageMinutes + " id=" + content.getId()); + return content; + } + + Log.i(TAG, "DN|JIT_STALE skip=false ageMin=" + ageMinutes + " id=" + content.getId()); + + // Attempt to fetch fresh content + DailyNotificationFetcher fetcher = new DailyNotificationFetcher( + getApplicationContext(), + new DailyNotificationStorage(getApplicationContext()) + ); + + // Attempt immediate fetch for fresh content + NotificationContent freshContent = fetcher.fetchContentImmediately(); + + if (freshContent != null && freshContent.getTitle() != null && !freshContent.getTitle().isEmpty()) { + Log.i(TAG, "DN|JIT_REFRESH_OK id=" + content.getId()); + + // Update the original content with fresh data while preserving the original ID and scheduled time + String originalId = content.getId(); + long originalScheduledTime = content.getScheduledTime(); + + content.setTitle(freshContent.getTitle()); + content.setBody(freshContent.getBody()); + content.setSound(freshContent.isSound()); + content.setPriority(freshContent.getPriority()); + content.setUrl(freshContent.getUrl()); + content.setMediaUrl(freshContent.getMediaUrl()); + content.setScheduledTime(originalScheduledTime); // Preserve original scheduled time + // Note: fetchedAt remains unchanged to preserve original fetch time + + // Save updated content to storage + DailyNotificationStorage storage = new DailyNotificationStorage(getApplicationContext()); + storage.saveNotificationContent(content); + + return content; + } else { + Log.w(TAG, "DN|JIT_REFRESH_FAIL id=" + content.getId()); + return content; + } + + } catch (Exception e) { + Log.e(TAG, "DN|JIT_ERR id=" + content.getId() + " err=" + e.getMessage(), e); + return content; // Return original content on error + } finally { + Trace.endSection(); + } + } + + /** + * Display the notification to the user + * + * @param content Notification content to display + * @return true if displayed successfully, false otherwise + */ + private boolean displayNotification(NotificationContent content) { + Trace.beginSection("DN:displayNotif"); + try { + Log.d(TAG, "DN|DISPLAY_NOTIF_START id=" + content.getId()); + + NotificationManager notificationManager = + (NotificationManager) getApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE); + + if (notificationManager == null) { + Log.e(TAG, "DN|DISPLAY_NOTIF_ERR no_manager id=" + content.getId()); + return false; + } + + // Create notification builder + NotificationCompat.Builder builder = new NotificationCompat.Builder(getApplicationContext(), CHANNEL_ID) + .setSmallIcon(android.R.drawable.ic_dialog_info) + .setContentTitle(content.getTitle()) + .setContentText(content.getBody()) + .setPriority(getNotificationPriority(content.getPriority())) + .setAutoCancel(true) + .setCategory(NotificationCompat.CATEGORY_REMINDER); + + // Add sound if enabled + if (content.isSound()) { + builder.setDefaults(NotificationCompat.DEFAULT_SOUND); + } + + // Add click action if URL is available + if (content.getUrl() != null && !content.getUrl().isEmpty()) { + Intent clickIntent = new Intent(Intent.ACTION_VIEW); + clickIntent.setData(android.net.Uri.parse(content.getUrl())); + + PendingIntent clickPendingIntent = PendingIntent.getActivity( + getApplicationContext(), + content.getId().hashCode(), + clickIntent, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE + ); + + builder.setContentIntent(clickPendingIntent); + } + + // Add dismiss action + Intent dismissIntent = new Intent(getApplicationContext(), DailyNotificationReceiver.class); + dismissIntent.setAction("com.timesafari.daily.DISMISS"); + dismissIntent.putExtra("notification_id", content.getId()); + + PendingIntent dismissPendingIntent = PendingIntent.getBroadcast( + getApplicationContext(), + content.getId().hashCode() + 1000, // Different request code + dismissIntent, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE + ); + + builder.addAction( + android.R.drawable.ic_menu_close_clear_cancel, + "Dismiss", + dismissPendingIntent + ); + + // Build and display notification + int notificationId = content.getId().hashCode(); + notificationManager.notify(notificationId, builder.build()); + + Log.i(TAG, "DN|DISPLAY_NOTIF_OK id=" + content.getId()); + return true; + + } catch (Exception e) { + Log.e(TAG, "DN|DISPLAY_NOTIF_ERR id=" + content.getId() + " err=" + e.getMessage(), e); + return false; + } finally { + Trace.endSection(); + } + } + + /** + * Schedule the next occurrence of this daily notification with DST-safe calculation + * + * @param content Current notification content + */ + private void scheduleNextNotification(NotificationContent content) { + Trace.beginSection("DN:scheduleNext"); + try { + Log.d(TAG, "DN|RESCHEDULE_START id=" + content.getId()); + + // Calculate next occurrence using DST-safe ZonedDateTime + long nextScheduledTime = calculateNextScheduledTime(content.getScheduledTime()); + + // Create new content for next occurrence + NotificationContent nextContent = new NotificationContent(); + nextContent.setTitle(content.getTitle()); + nextContent.setBody(content.getBody()); + nextContent.setScheduledTime(nextScheduledTime); + nextContent.setSound(content.isSound()); + nextContent.setPriority(content.getPriority()); + nextContent.setUrl(content.getUrl()); + // fetchedAt is set in constructor, no need to set it again + + // Save to storage + DailyNotificationStorage storage = new DailyNotificationStorage(getApplicationContext()); + storage.saveNotificationContent(nextContent); + + // Schedule the notification + DailyNotificationScheduler scheduler = new DailyNotificationScheduler( + getApplicationContext(), + (android.app.AlarmManager) getApplicationContext().getSystemService(Context.ALARM_SERVICE) + ); + + boolean scheduled = scheduler.scheduleNotification(nextContent); + + if (scheduled) { + // Log next scheduled time in readable format + String nextTimeStr = formatScheduledTime(nextScheduledTime); + Log.i(TAG, "DN|RESCHEDULE_OK id=" + content.getId() + " next=" + nextTimeStr); + } else { + Log.e(TAG, "DN|RESCHEDULE_ERR id=" + content.getId()); + } + + } catch (Exception e) { + Log.e(TAG, "DN|RESCHEDULE_ERR id=" + content.getId() + " err=" + e.getMessage(), e); + } finally { + Trace.endSection(); + } + } + + /** + * Calculate next scheduled time with DST-safe handling + * + * @param currentScheduledTime Current scheduled time + * @return Next scheduled time (24 hours later, DST-safe) + */ + private long calculateNextScheduledTime(long currentScheduledTime) { + try { + // Get user's timezone + ZoneId userZone = ZoneId.systemDefault(); + + // Convert to ZonedDateTime + ZonedDateTime currentZoned = ZonedDateTime.ofInstant( + java.time.Instant.ofEpochMilli(currentScheduledTime), + userZone + ); + + // Add 24 hours (handles DST transitions automatically) + ZonedDateTime nextZoned = currentZoned.plusHours(24); + + // Convert back to epoch millis + return nextZoned.toInstant().toEpochMilli(); + + } catch (Exception e) { + Log.e(TAG, "DN|DST_CALC_ERR fallback_to_simple err=" + e.getMessage(), e); + // Fallback to simple 24-hour addition if DST calculation fails + return currentScheduledTime + (24 * 60 * 60 * 1000); + } + } + + /** + * Format scheduled time for logging + * + * @param scheduledTime Epoch millis + * @return Formatted time string + */ + private String formatScheduledTime(long scheduledTime) { + try { + ZonedDateTime zoned = ZonedDateTime.ofInstant( + java.time.Instant.ofEpochMilli(scheduledTime), + ZoneId.systemDefault() + ); + return zoned.format(DateTimeFormatter.ofPattern("HH:mm:ss on MM/dd/yyyy")); + } catch (Exception e) { + return "epoch:" + scheduledTime; + } + } + + /** + * Get notification priority constant + * + * @param priority Priority string from content + * @return NotificationCompat priority constant + */ + private int getNotificationPriority(String priority) { + if (priority == null) { + return NotificationCompat.PRIORITY_DEFAULT; + } + + switch (priority.toLowerCase()) { + case "high": + return NotificationCompat.PRIORITY_HIGH; + case "low": + return NotificationCompat.PRIORITY_LOW; + case "min": + return NotificationCompat.PRIORITY_MIN; + case "max": + return NotificationCompat.PRIORITY_MAX; + default: + return NotificationCompat.PRIORITY_DEFAULT; + } + } +} diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/ExactAlarmManager.java b/android/plugin/src/main/java/com/timesafari/dailynotification/ExactAlarmManager.java deleted file mode 100644 index 9981d60..0000000 --- a/android/plugin/src/main/java/com/timesafari/dailynotification/ExactAlarmManager.java +++ /dev/null @@ -1,146 +0,0 @@ -/** - * 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/JsonOptimizer.java b/android/plugin/src/main/java/com/timesafari/dailynotification/JsonOptimizer.java deleted file mode 100644 index d8ca06f..0000000 --- a/android/plugin/src/main/java/com/timesafari/dailynotification/JsonOptimizer.java +++ /dev/null @@ -1,373 +0,0 @@ -/** - * JsonOptimizer.java - * - * Optimized JSON handling utilities to reduce serialization overhead - * Implements caching, lazy serialization, and efficient data structures - * - * @author Matthew Raymer - * @version 2.0.0 - Optimized Architecture - */ - -package com.timesafari.dailynotification; - -import android.util.Log; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.reflect.TypeToken; - -import java.lang.reflect.Type; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -/** - * Optimized JSON handling utilities - * - * Optimizations: - * - JSON caching to avoid repeated serialization - * - Lazy serialization for large objects - * - Efficient data structure conversions - * - Reduced memory allocations - * - Thread-safe operations - */ -public class JsonOptimizer { - - private static final String TAG = "JsonOptimizer"; - - // Optimized Gson instance - private static final Gson optimizedGson = createOptimizedGson(); - - // JSON cache to avoid repeated serialization - private static final Map jsonCache = new ConcurrentHashMap<>(); - private static final Map objectCache = new ConcurrentHashMap<>(); - - // Cache configuration - private static final int MAX_CACHE_SIZE = 1000; - private static final long CACHE_TTL = 5 * 60 * 1000; // 5 minutes - - /** - * Create optimized Gson instance - */ - private static Gson createOptimizedGson() { - GsonBuilder builder = new GsonBuilder(); - - // Performance optimizations - builder.disableHtmlEscaping(); - builder.setLenient(); - - // Custom serializers for common types - builder.registerTypeAdapter(NotificationContent.class, - new NotificationContent.NotificationContentDeserializer()); - - return builder.create(); - } - - /** - * Optimized JSON serialization with caching - * - * @param object Object to serialize - * @return JSON string - */ - public static String toJson(Object object) { - if (object == null) { - return "null"; - } - - String objectKey = generateObjectKey(object); - - // Check cache first - String cached = jsonCache.get(objectKey); - if (cached != null) { - Log.d(TAG, "JSON cache hit for: " + objectKey); - return cached; - } - - // Serialize and cache - String json = optimizedGson.toJson(object); - - // Cache management - if (jsonCache.size() < MAX_CACHE_SIZE) { - jsonCache.put(objectKey, json); - } - - Log.d(TAG, "JSON serialized and cached: " + objectKey); - return json; - } - - /** - * Optimized JSON deserialization with caching - * - * @param json JSON string - * @param type Type token - * @return Deserialized object - */ - public static T fromJson(String json, Type type) { - if (json == null || json.isEmpty()) { - return null; - } - - String jsonKey = generateJsonKey(json, type); - - // Check cache first - @SuppressWarnings("unchecked") - T cached = (T) objectCache.get(jsonKey); - if (cached != null) { - Log.d(TAG, "Object cache hit for: " + jsonKey); - return cached; - } - - // Deserialize and cache - T object = optimizedGson.fromJson(json, type); - - // Cache management - if (objectCache.size() < MAX_CACHE_SIZE) { - objectCache.put(jsonKey, object); - } - - Log.d(TAG, "Object deserialized and cached: " + jsonKey); - return object; - } - - /** - * Optimized JSON deserialization for lists - * - * @param json JSON string - * @param typeToken Type token for list - * @return Deserialized list - */ - public static java.util.List fromJsonList(String json, TypeToken> typeToken) { - return fromJson(json, typeToken.getType()); - } - - /** - * Convert NotificationContent to optimized JSON object - * - * @param content Notification content - * @return Optimized JSON object - */ - public static JsonObject toOptimizedJsonObject(NotificationContent content) { - JsonObject jsonObject = new JsonObject(); - - // Only include non-null, non-empty fields - if (content.getId() != null && !content.getId().isEmpty()) { - jsonObject.addProperty("id", content.getId()); - } - - if (content.getTitle() != null && !content.getTitle().isEmpty()) { - jsonObject.addProperty("title", content.getTitle()); - } - - if (content.getBody() != null && !content.getBody().isEmpty()) { - jsonObject.addProperty("body", content.getBody()); - } - - if (content.getScheduledTime() > 0) { - jsonObject.addProperty("scheduledTime", content.getScheduledTime()); - } - - if (content.getFetchedAt() > 0) { - jsonObject.addProperty("fetchedAt", content.getFetchedAt()); - } - - jsonObject.addProperty("sound", content.isSound()); - jsonObject.addProperty("priority", content.getPriority()); - - if (content.getUrl() != null && !content.getUrl().isEmpty()) { - jsonObject.addProperty("url", content.getUrl()); - } - - if (content.getMediaUrl() != null && !content.getMediaUrl().isEmpty()) { - jsonObject.addProperty("mediaUrl", content.getMediaUrl()); - } - - return jsonObject; - } - - /** - * Convert optimized JSON object to NotificationContent - * - * @param jsonObject JSON object - * @return Notification content - */ - public static NotificationContent fromOptimizedJsonObject(JsonObject jsonObject) { - NotificationContent content = new NotificationContent(); - - if (jsonObject.has("id")) { - content.setId(jsonObject.get("id").getAsString()); - } - - if (jsonObject.has("title")) { - content.setTitle(jsonObject.get("title").getAsString()); - } - - if (jsonObject.has("body")) { - content.setBody(jsonObject.get("body").getAsString()); - } - - if (jsonObject.has("scheduledTime")) { - content.setScheduledTime(jsonObject.get("scheduledTime").getAsLong()); - } - - if (jsonObject.has("fetchedAt")) { - content.setFetchedAt(jsonObject.get("fetchedAt").getAsLong()); - } - - if (jsonObject.has("sound")) { - content.setSound(jsonObject.get("sound").getAsBoolean()); - } - - if (jsonObject.has("priority")) { - content.setPriority(jsonObject.get("priority").getAsString()); - } - - if (jsonObject.has("url")) { - content.setUrl(jsonObject.get("url").getAsString()); - } - - if (jsonObject.has("mediaUrl")) { - content.setMediaUrl(jsonObject.get("mediaUrl").getAsString()); - } - - return content; - } - - /** - * Batch serialize multiple objects efficiently - * - * @param objects Objects to serialize - * @return JSON string array - */ - public static String batchToJson(java.util.List objects) { - if (objects == null || objects.isEmpty()) { - return "[]"; - } - - StringBuilder jsonBuilder = new StringBuilder(); - jsonBuilder.append("["); - - for (int i = 0; i < objects.size(); i++) { - if (i > 0) { - jsonBuilder.append(","); - } - - String objectJson = toJson(objects.get(i)); - jsonBuilder.append(objectJson); - } - - jsonBuilder.append("]"); - return jsonBuilder.toString(); - } - - /** - * Batch deserialize JSON array efficiently - * - * @param json JSON array string - * @param typeToken Type token for list elements - * @return Deserialized list - */ - public static java.util.List batchFromJson(String json, TypeToken> typeToken) { - return fromJsonList(json, typeToken); - } - - /** - * Generate cache key for object - */ - private static String generateObjectKey(Object object) { - return object.getClass().getSimpleName() + "_" + object.hashCode(); - } - - /** - * Generate cache key for JSON string and type - */ - private static String generateJsonKey(String json, Type type) { - return type.toString() + "_" + json.hashCode(); - } - - /** - * Clear JSON cache - */ - public static void clearCache() { - jsonCache.clear(); - objectCache.clear(); - Log.d(TAG, "JSON cache cleared"); - } - - /** - * Get cache statistics - * - * @return Cache statistics - */ - public static Map getCacheStats() { - Map stats = new HashMap<>(); - stats.put("jsonCacheSize", jsonCache.size()); - stats.put("objectCacheSize", objectCache.size()); - stats.put("maxCacheSize", MAX_CACHE_SIZE); - return stats; - } - - /** - * Optimized settings serialization - * - * @param settings Settings map - * @return JSON string - */ - public static String settingsToJson(Map settings) { - if (settings == null || settings.isEmpty()) { - return "{}"; - } - - JsonObject jsonObject = new JsonObject(); - - for (Map.Entry entry : settings.entrySet()) { - String key = entry.getKey(); - Object value = entry.getValue(); - - if (value instanceof String) { - jsonObject.addProperty(key, (String) value); - } else if (value instanceof Boolean) { - jsonObject.addProperty(key, (Boolean) value); - } else if (value instanceof Number) { - jsonObject.addProperty(key, (Number) value); - } else { - jsonObject.addProperty(key, value.toString()); - } - } - - return optimizedGson.toJson(jsonObject); - } - - /** - * Optimized settings deserialization - * - * @param json JSON string - * @return Settings map - */ - public static Map settingsFromJson(String json) { - if (json == null || json.isEmpty()) { - return new HashMap<>(); - } - - JsonObject jsonObject = optimizedGson.fromJson(json, JsonObject.class); - Map settings = new HashMap<>(); - - for (Map.Entry entry : jsonObject.entrySet()) { - String key = entry.getKey(); - JsonElement value = entry.getValue(); - - if (value.isJsonPrimitive()) { - if (value.getAsJsonPrimitive().isString()) { - settings.put(key, value.getAsString()); - } else if (value.getAsJsonPrimitive().isBoolean()) { - settings.put(key, value.getAsBoolean()); - } else if (value.getAsJsonPrimitive().isNumber()) { - settings.put(key, value.getAsNumber()); - } - } - } - - return settings; - } -} diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/LoggingManager.java b/android/plugin/src/main/java/com/timesafari/dailynotification/LoggingManager.java deleted file mode 100644 index 33cfd4f..0000000 --- a/android/plugin/src/main/java/com/timesafari/dailynotification/LoggingManager.java +++ /dev/null @@ -1,394 +0,0 @@ -/** - * LoggingManager.java - * - * Optimized logging management with privacy controls and level management - * Implements structured logging, privacy protection, and performance optimization - * - * @author Matthew Raymer - * @version 2.0.0 - Optimized Architecture - */ - -package com.timesafari.dailynotification; - -import android.content.Context; -import android.util.Log; - -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.regex.Pattern; - -/** - * Optimized logging manager with privacy controls - * - * Features: - * - Structured logging with levels - * - Privacy protection for sensitive data - * - Performance optimization - * - Configurable log levels - * - Log filtering and sanitization - */ -public class LoggingManager { - - private static final String TAG = "LoggingManager"; - - // Log levels - public static final int VERBOSE = Log.VERBOSE; - public static final int DEBUG = Log.DEBUG; - public static final int INFO = Log.INFO; - public static final int WARN = Log.WARN; - public static final int ERROR = Log.ERROR; - - // Privacy patterns for sensitive data - private static final Pattern EMAIL_PATTERN = Pattern.compile("\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b"); - private static final Pattern PHONE_PATTERN = Pattern.compile("\\b\\d{3}-\\d{3}-\\d{4}\\b"); - private static final Pattern SSN_PATTERN = Pattern.compile("\\b\\d{3}-\\d{2}-\\d{4}\\b"); - private static final Pattern CREDIT_CARD_PATTERN = Pattern.compile("\\b\\d{4}[\\s-]?\\d{4}[\\s-]?\\d{4}[\\s-]?\\d{4}\\b"); - - // Configuration - private static int currentLogLevel = INFO; - private static boolean privacyEnabled = true; - private static boolean performanceLogging = false; - - // Performance tracking - private static final Map performanceStartTimes = new ConcurrentHashMap<>(); - private static final Map logCounts = new ConcurrentHashMap<>(); - - // Context - private final Context context; - - /** - * Initialize logging manager - * - * @param context Application context - */ - public LoggingManager(Context context) { - this.context = context; - - Log.d(TAG, "LoggingManager initialized with level: " + getLevelName(currentLogLevel)); - } - - /** - * Set the current log level - * - * @param level Log level (VERBOSE, DEBUG, INFO, WARN, ERROR) - */ - public static void setLogLevel(int level) { - currentLogLevel = level; - Log.i(TAG, "Log level set to: " + getLevelName(level)); - } - - /** - * Get the current log level - * - * @return Current log level - */ - public static int getLogLevel() { - return currentLogLevel; - } - - /** - * Enable or disable privacy protection - * - * @param enabled true to enable privacy protection - */ - public static void setPrivacyEnabled(boolean enabled) { - privacyEnabled = enabled; - Log.i(TAG, "Privacy protection " + (enabled ? "enabled" : "disabled")); - } - - /** - * Enable or disable performance logging - * - * @param enabled true to enable performance logging - */ - public static void setPerformanceLogging(boolean enabled) { - performanceLogging = enabled; - Log.i(TAG, "Performance logging " + (enabled ? "enabled" : "disabled")); - } - - /** - * Log verbose message with privacy protection - * - * @param tag Log tag - * @param message Message to log - */ - public static void v(String tag, String message) { - if (shouldLog(VERBOSE)) { - String sanitizedMessage = sanitizeMessage(message); - Log.v(tag, sanitizedMessage); - incrementLogCount(tag, VERBOSE); - } - } - - /** - * Log debug message with privacy protection - * - * @param tag Log tag - * @param message Message to log - */ - public static void d(String tag, String message) { - if (shouldLog(DEBUG)) { - String sanitizedMessage = sanitizeMessage(message); - Log.d(tag, sanitizedMessage); - incrementLogCount(tag, DEBUG); - } - } - - /** - * Log info message with privacy protection - * - * @param tag Log tag - * @param message Message to log - */ - public static void i(String tag, String message) { - if (shouldLog(INFO)) { - String sanitizedMessage = sanitizeMessage(message); - Log.i(tag, sanitizedMessage); - incrementLogCount(tag, INFO); - } - } - - /** - * Log warning message with privacy protection - * - * @param tag Log tag - * @param message Message to log - */ - public static void w(String tag, String message) { - if (shouldLog(WARN)) { - String sanitizedMessage = sanitizeMessage(message); - Log.w(tag, sanitizedMessage); - incrementLogCount(tag, WARN); - } - } - - /** - * Log error message with privacy protection - * - * @param tag Log tag - * @param message Message to log - */ - public static void e(String tag, String message) { - if (shouldLog(ERROR)) { - String sanitizedMessage = sanitizeMessage(message); - Log.e(tag, sanitizedMessage); - incrementLogCount(tag, ERROR); - } - } - - /** - * Log error message with exception - * - * @param tag Log tag - * @param message Message to log - * @param throwable Exception to log - */ - public static void e(String tag, String message, Throwable throwable) { - if (shouldLog(ERROR)) { - String sanitizedMessage = sanitizeMessage(message); - Log.e(tag, sanitizedMessage, throwable); - incrementLogCount(tag, ERROR); - } - } - - /** - * Start performance timing - * - * @param operation Operation name - */ - public static void startTiming(String operation) { - if (performanceLogging) { - performanceStartTimes.put(operation, System.currentTimeMillis()); - d(TAG, "Started timing: " + operation); - } - } - - /** - * End performance timing - * - * @param operation Operation name - */ - public static void endTiming(String operation) { - if (performanceLogging) { - Long startTime = performanceStartTimes.remove(operation); - if (startTime != null) { - long duration = System.currentTimeMillis() - startTime; - i(TAG, "Timing completed: " + operation + " took " + duration + "ms"); - } - } - } - - /** - * Log structured data - * - * @param tag Log tag - * @param level Log level - * @param data Structured data to log - */ - public static void logStructured(String tag, int level, Map data) { - if (shouldLog(level)) { - StringBuilder message = new StringBuilder(); - message.append("Structured data: "); - - for (Map.Entry entry : data.entrySet()) { - String key = entry.getKey(); - Object value = entry.getValue(); - - // Sanitize sensitive keys - if (isSensitiveKey(key)) { - message.append(key).append("=[REDACTED] "); - } else { - String sanitizedValue = sanitizeMessage(value.toString()); - message.append(key).append("=").append(sanitizedValue).append(" "); - } - } - - logMessage(tag, level, message.toString()); - } - } - - /** - * Check if should log at given level - * - * @param level Log level - * @return true if should log - */ - private static boolean shouldLog(int level) { - return level >= currentLogLevel; - } - - /** - * Log message at given level - * - * @param tag Log tag - * @param level Log level - * @param message Message to log - */ - private static void logMessage(String tag, int level, String message) { - switch (level) { - case VERBOSE: - Log.v(tag, message); - break; - case DEBUG: - Log.d(tag, message); - break; - case INFO: - Log.i(tag, message); - break; - case WARN: - Log.w(tag, message); - break; - case ERROR: - Log.e(tag, message); - break; - } - } - - /** - * Sanitize message for privacy protection - * - * @param message Original message - * @return Sanitized message - */ - private static String sanitizeMessage(String message) { - if (!privacyEnabled || message == null) { - return message; - } - - String sanitized = message; - - // Replace email addresses - sanitized = EMAIL_PATTERN.matcher(sanitized).replaceAll("[EMAIL_REDACTED]"); - - // Replace phone numbers - sanitized = PHONE_PATTERN.matcher(sanitized).replaceAll("[PHONE_REDACTED]"); - - // Replace SSNs - sanitized = SSN_PATTERN.matcher(sanitized).replaceAll("[SSN_REDACTED]"); - - // Replace credit card numbers - sanitized = CREDIT_CARD_PATTERN.matcher(sanitized).replaceAll("[CARD_REDACTED]"); - - return sanitized; - } - - /** - * Check if key is sensitive - * - * @param key Key to check - * @return true if key is sensitive - */ - private static boolean isSensitiveKey(String key) { - if (key == null) { - return false; - } - - String lowerKey = key.toLowerCase(); - return lowerKey.contains("password") || - lowerKey.contains("token") || - lowerKey.contains("secret") || - lowerKey.contains("key") || - lowerKey.contains("auth") || - lowerKey.contains("credential"); - } - - /** - * Increment log count for statistics - * - * @param tag Log tag - * @param level Log level - */ - private static void incrementLogCount(String tag, int level) { - String key = tag + "_" + getLevelName(level); - logCounts.put(key, logCounts.getOrDefault(key, 0) + 1); - } - - /** - * Get level name - * - * @param level Log level - * @return Level name - */ - private static String getLevelName(int level) { - switch (level) { - case VERBOSE: - return "VERBOSE"; - case DEBUG: - return "DEBUG"; - case INFO: - return "INFO"; - case WARN: - return "WARN"; - case ERROR: - return "ERROR"; - default: - return "UNKNOWN"; - } - } - - /** - * Get logging statistics - * - * @return Logging statistics - */ - public static Map getLoggingStats() { - Map stats = new HashMap<>(); - stats.put("currentLogLevel", getLevelName(currentLogLevel)); - stats.put("privacyEnabled", privacyEnabled); - stats.put("performanceLogging", performanceLogging); - stats.put("logCounts", new HashMap<>(logCounts)); - stats.put("activeTimings", performanceStartTimes.size()); - - return stats; - } - - /** - * Clear logging statistics - */ - public static void clearStats() { - logCounts.clear(); - performanceStartTimes.clear(); - Log.i(TAG, "Logging statistics cleared"); - } -} diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/NotificationManager.java b/android/plugin/src/main/java/com/timesafari/dailynotification/NotificationManager.java deleted file mode 100644 index 0c928e8..0000000 --- a/android/plugin/src/main/java/com/timesafari/dailynotification/NotificationManager.java +++ /dev/null @@ -1,363 +0,0 @@ -/** - * NotificationManager.java - * - * Specialized manager for core notification operations - * Handles scheduling, cancellation, status checking, and settings management - * - * @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.Calendar; - -/** - * Manager class for core notification operations - * - * Responsibilities: - * - Schedule daily notifications - * - Cancel notifications - * - Get notification status and history - * - Update notification settings - * - Handle configuration - */ -public class NotificationManager { - - private static final String TAG = "NotificationManager"; - - private final Context context; - private final DailyNotificationStorage storage; - private final DailyNotificationScheduler scheduler; - private final ChannelManager channelManager; - - // Configuration state - private String databasePath; - private boolean useSharedStorage = false; - - /** - * Initialize the NotificationManager - * - * @param context Android context - * @param storage Storage component for notification data - * @param scheduler Scheduler component for alarm management - * @param channelManager Channel manager for notification channels - */ - public NotificationManager(Context context, DailyNotificationStorage storage, - DailyNotificationScheduler scheduler, ChannelManager channelManager) { - this.context = context; - this.storage = storage; - this.scheduler = scheduler; - this.channelManager = channelManager; - - Log.d(TAG, "NotificationManager initialized"); - } - - /** - * Configure the plugin with database and storage options - * - * @param call Plugin call containing configuration parameters - */ - public void configure(PluginCall call) { - try { - Log.d(TAG, "Configuring notification system"); - - // 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"); - - // 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 { - databasePath = context.getDatabasePath("daily_notifications.db").getAbsolutePath(); - Log.d(TAG, "Using default database path: " + databasePath); - } - - // Store configuration - storeConfiguration(ttlSeconds, prefetchLeadMinutes, maxNotificationsPerDay, retentionDays); - - Log.i(TAG, "Notification system configuration completed"); - - JSObject result = new JSObject(); - result.put("success", true); - result.put("message", "Configuration updated successfully"); - call.resolve(result); - - } catch (Exception e) { - Log.e(TAG, "Error configuring notification system", e); - call.reject("Configuration failed: " + e.getMessage()); - } - } - - /** - * Schedule a daily notification with the specified options - * - * @param call Plugin call containing notification parameters - */ - 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.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 - String notificationId = "daily-" + System.currentTimeMillis(); - content.setId(notificationId); - - // Save notification content - storage.saveNotificationContent(content); - - // Schedule the alarm - boolean scheduled = scheduler.scheduleNotification(content); - - if (scheduled) { - Log.i(TAG, "Daily notification scheduled successfully: " + notificationId); - - JSObject result = new JSObject(); - result.put("success", true); - result.put("notificationId", notificationId); - result.put("scheduledTime", calendar.getTimeInMillis()); - result.put("message", "Notification scheduled successfully"); - call.resolve(result); - } else { - Log.e(TAG, "Failed to schedule daily notification"); - call.reject("Failed to schedule notification"); - } - - } catch (Exception e) { - Log.e(TAG, "Error scheduling daily notification", e); - call.reject("Scheduling failed: " + e.getMessage()); - } - } - - /** - * Get the last notification that was displayed - * - * @param call Plugin call - */ - 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("success", true); - result.put("notification", lastNotification.toJSObject()); - call.resolve(result); - } else { - JSObject result = new JSObject(); - result.put("success", true); - result.put("notification", null); - result.put("message", "No notifications found"); - call.resolve(result); - } - - } catch (Exception e) { - Log.e(TAG, "Error getting last notification", e); - call.reject("Failed to get last notification: " + e.getMessage()); - } - } - - /** - * Cancel all scheduled notifications - * - * @param call Plugin call - */ - public void cancelAllNotifications(PluginCall call) { - try { - Log.d(TAG, "Cancelling all notifications"); - - // Cancel all scheduled alarms - scheduler.cancelAllNotifications(); - - // Clear stored notifications - storage.clearAllNotifications(); - - Log.i(TAG, "All notifications cancelled successfully"); - - JSObject result = new JSObject(); - result.put("success", true); - result.put("message", "All notifications cancelled"); - call.resolve(result); - - } catch (Exception e) { - Log.e(TAG, "Error cancelling notifications", e); - call.reject("Failed to cancel notifications: " + e.getMessage()); - } - } - - /** - * Get the current status of the notification system - * - * @param call Plugin call - */ - public void getNotificationStatus(PluginCall call) { - try { - Log.d(TAG, "Getting notification status"); - - // Get scheduled notifications count - int scheduledCount = storage.getScheduledNotificationsCount(); - - // Get last notification - NotificationContent lastNotification = storage.getLastNotification(); - - JSObject result = new JSObject(); - result.put("success", true); - result.put("scheduledCount", scheduledCount); - result.put("lastNotification", lastNotification != null ? lastNotification.toJSObject() : null); - result.put("channelEnabled", channelManager.isChannelEnabled()); - result.put("channelId", channelManager.getDefaultChannelId()); - - call.resolve(result); - - } catch (Exception e) { - Log.e(TAG, "Error getting notification status", e); - call.reject("Failed to get notification status: " + e.getMessage()); - } - } - - /** - * Update notification settings - * - * @param call Plugin call containing settings - */ - public void updateSettings(PluginCall call) { - try { - Log.d(TAG, "Updating notification settings"); - - // Get settings from call - String title = call.getString("title"); - String body = call.getString("body"); - Boolean sound = call.getBoolean("sound"); - String priority = call.getString("priority"); - - // Update settings in storage - if (title != null) { - storage.setSetting("default_title", title); - } - if (body != null) { - storage.setSetting("default_body", body); - } - if (sound != null) { - storage.setSetting("default_sound", sound.toString()); - } - if (priority != null) { - storage.setSetting("default_priority", priority); - } - - Log.i(TAG, "Notification settings updated successfully"); - - JSObject result = new JSObject(); - result.put("success", true); - result.put("message", "Settings updated successfully"); - call.resolve(result); - - } catch (Exception e) { - Log.e(TAG, "Error updating settings", e); - call.reject("Failed to update settings: " + e.getMessage()); - } - } - - /** - * Store configuration parameters - * - * @param ttlSeconds TTL in seconds - * @param prefetchLeadMinutes Prefetch lead time in minutes - * @param maxNotificationsPerDay Maximum notifications per day - * @param retentionDays Retention period in days - */ - private void storeConfiguration(Integer ttlSeconds, Integer prefetchLeadMinutes, - Integer maxNotificationsPerDay, Integer retentionDays) { - try { - if (ttlSeconds != null) { - storage.setSetting("ttl_seconds", ttlSeconds.toString()); - } - if (prefetchLeadMinutes != null) { - storage.setSetting("prefetch_lead_minutes", prefetchLeadMinutes.toString()); - } - if (maxNotificationsPerDay != null) { - storage.setSetting("max_notifications_per_day", maxNotificationsPerDay.toString()); - } - if (retentionDays != null) { - storage.setSetting("retention_days", retentionDays.toString()); - } - - Log.d(TAG, "Configuration stored successfully"); - } catch (Exception e) { - Log.e(TAG, "Error storing configuration", e); - } - } -} diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/NotificationStatusChecker.java b/android/plugin/src/main/java/com/timesafari/dailynotification/NotificationStatusChecker.java new file mode 100644 index 0000000..b8a0528 --- /dev/null +++ b/android/plugin/src/main/java/com/timesafari/dailynotification/NotificationStatusChecker.java @@ -0,0 +1,349 @@ +/** + * NotificationStatusChecker.java + * + * Comprehensive status checking for notification system + * Provides unified API for UI guidance and troubleshooting + * + * @author Matthew Raymer + * @version 1.0.0 + */ + +package com.timesafari.dailynotification; + +import android.app.NotificationManager; +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Build; +import android.util.Log; + +import com.getcapacitor.JSObject; + +/** + * Comprehensive status checker for notification system + * + * This class provides a unified API to check all aspects of the notification + * system status, enabling the UI to guide users when notifications don't appear. + */ +public class NotificationStatusChecker { + + private static final String TAG = "NotificationStatusChecker"; + + private final Context context; + private final NotificationManager notificationManager; + private final ChannelManager channelManager; + private final PendingIntentManager pendingIntentManager; + + public NotificationStatusChecker(Context context) { + this.context = context.getApplicationContext(); + this.notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + this.channelManager = new ChannelManager(context); + this.pendingIntentManager = new PendingIntentManager(context); + } + + /** + * Get comprehensive notification system status + * + * @return JSObject containing all status information + */ + public JSObject getComprehensiveStatus() { + try { + Log.d(TAG, "DN|STATUS_CHECK_START"); + + JSObject status = new JSObject(); + + // Core permissions + boolean postNotificationsGranted = checkPostNotificationsPermission(); + boolean exactAlarmsGranted = checkExactAlarmsPermission(); + + // Channel status + boolean channelEnabled = channelManager.isChannelEnabled(); + int channelImportance = channelManager.getChannelImportance(); + String channelId = channelManager.getDefaultChannelId(); + + // Alarm manager status + PendingIntentManager.AlarmStatus alarmStatus = pendingIntentManager.getAlarmStatus(); + + // Overall readiness + boolean canScheduleNow = postNotificationsGranted && + channelEnabled && + exactAlarmsGranted; + + // Build status object + status.put("postNotificationsGranted", postNotificationsGranted); + status.put("exactAlarmsGranted", exactAlarmsGranted); + status.put("channelEnabled", channelEnabled); + status.put("channelImportance", channelImportance); + status.put("channelId", channelId); + status.put("canScheduleNow", canScheduleNow); + status.put("exactAlarmsSupported", alarmStatus.exactAlarmsSupported); + status.put("androidVersion", alarmStatus.androidVersion); + + // Add issue descriptions for UI guidance + JSObject issues = new JSObject(); + if (!postNotificationsGranted) { + issues.put("postNotifications", "POST_NOTIFICATIONS permission not granted"); + } + if (!channelEnabled) { + issues.put("channelDisabled", "Notification channel is disabled or blocked"); + } + if (!exactAlarmsGranted) { + issues.put("exactAlarms", "Exact alarm permission not granted"); + } + status.put("issues", issues); + + // Add actionable guidance + JSObject guidance = new JSObject(); + if (!postNotificationsGranted) { + guidance.put("postNotifications", "Request notification permission in app settings"); + } + if (!channelEnabled) { + guidance.put("channelDisabled", "Enable notifications in system settings"); + } + if (!exactAlarmsGranted) { + guidance.put("exactAlarms", "Grant exact alarm permission in system settings"); + } + status.put("guidance", guidance); + + Log.d(TAG, "DN|STATUS_CHECK_OK canSchedule=" + canScheduleNow + + " postGranted=" + postNotificationsGranted + + " channelEnabled=" + channelEnabled + + " exactGranted=" + exactAlarmsGranted); + + return status; + + } catch (Exception e) { + Log.e(TAG, "DN|STATUS_CHECK_ERR err=" + e.getMessage(), e); + + // Return minimal status on error + JSObject errorStatus = new JSObject(); + errorStatus.put("canScheduleNow", false); + errorStatus.put("error", e.getMessage()); + return errorStatus; + } + } + + /** + * Check POST_NOTIFICATIONS permission status + * + * @return true if permission is granted, false otherwise + */ + private boolean checkPostNotificationsPermission() { + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + return context.checkSelfPermission(android.Manifest.permission.POST_NOTIFICATIONS) + == PackageManager.PERMISSION_GRANTED; + } else { + // Pre-Android 13, notifications are allowed by default + return true; + } + } catch (Exception e) { + Log.e(TAG, "DN|PERM_CHECK_ERR postNotifications err=" + e.getMessage(), e); + return false; + } + } + + /** + * Check SCHEDULE_EXACT_ALARM permission status + * + * @return true if permission is granted, false otherwise + */ + private boolean checkExactAlarmsPermission() { + try { + return pendingIntentManager.canScheduleExactAlarms(); + } catch (Exception e) { + Log.e(TAG, "DN|PERM_CHECK_ERR exactAlarms err=" + e.getMessage(), e); + return false; + } + } + + /** + * Get detailed channel status information + * + * @return JSObject containing channel details + */ + public JSObject getChannelStatus() { + try { + Log.d(TAG, "DN|CHANNEL_STATUS_START"); + + JSObject channelStatus = new JSObject(); + + boolean channelExists = channelManager.ensureChannelExists(); + boolean channelEnabled = channelManager.isChannelEnabled(); + int channelImportance = channelManager.getChannelImportance(); + String channelId = channelManager.getDefaultChannelId(); + + channelStatus.put("channelExists", channelExists); + channelStatus.put("channelEnabled", channelEnabled); + channelStatus.put("channelImportance", channelImportance); + channelStatus.put("channelId", channelId); + channelStatus.put("channelBlocked", channelImportance == NotificationManager.IMPORTANCE_NONE); + + // Add importance description + String importanceDescription = getImportanceDescription(channelImportance); + channelStatus.put("importanceDescription", importanceDescription); + + Log.d(TAG, "DN|CHANNEL_STATUS_OK enabled=" + channelEnabled + + " importance=" + channelImportance + + " blocked=" + (channelImportance == NotificationManager.IMPORTANCE_NONE)); + + return channelStatus; + + } catch (Exception e) { + Log.e(TAG, "DN|CHANNEL_STATUS_ERR err=" + e.getMessage(), e); + + JSObject errorStatus = new JSObject(); + errorStatus.put("error", e.getMessage()); + return errorStatus; + } + } + + /** + * Get alarm manager status information + * + * @return JSObject containing alarm manager details + */ + public JSObject getAlarmStatus() { + try { + Log.d(TAG, "DN|ALARM_STATUS_START"); + + PendingIntentManager.AlarmStatus alarmStatus = pendingIntentManager.getAlarmStatus(); + + JSObject status = new JSObject(); + status.put("exactAlarmsSupported", alarmStatus.exactAlarmsSupported); + status.put("exactAlarmsGranted", alarmStatus.exactAlarmsGranted); + status.put("androidVersion", alarmStatus.androidVersion); + status.put("canScheduleExactAlarms", alarmStatus.exactAlarmsGranted); + + Log.d(TAG, "DN|ALARM_STATUS_OK supported=" + alarmStatus.exactAlarmsSupported + + " granted=" + alarmStatus.exactAlarmsGranted + + " android=" + alarmStatus.androidVersion); + + return status; + + } catch (Exception e) { + Log.e(TAG, "DN|ALARM_STATUS_ERR err=" + e.getMessage(), e); + + JSObject errorStatus = new JSObject(); + errorStatus.put("error", e.getMessage()); + return errorStatus; + } + } + + /** + * Get permission status information + * + * @return JSObject containing permission details + */ + public JSObject getPermissionStatus() { + try { + Log.d(TAG, "DN|PERMISSION_STATUS_START"); + + JSObject permissionStatus = new JSObject(); + + boolean postNotificationsGranted = checkPostNotificationsPermission(); + boolean exactAlarmsGranted = checkExactAlarmsPermission(); + + permissionStatus.put("postNotificationsGranted", postNotificationsGranted); + permissionStatus.put("exactAlarmsGranted", exactAlarmsGranted); + permissionStatus.put("allPermissionsGranted", postNotificationsGranted && exactAlarmsGranted); + + // Add permission descriptions + JSObject descriptions = new JSObject(); + descriptions.put("postNotifications", "Allows app to display notifications"); + descriptions.put("exactAlarms", "Allows app to schedule precise alarm times"); + permissionStatus.put("descriptions", descriptions); + + Log.d(TAG, "DN|PERMISSION_STATUS_OK postGranted=" + postNotificationsGranted + + " exactGranted=" + exactAlarmsGranted); + + return permissionStatus; + + } catch (Exception e) { + Log.e(TAG, "DN|PERMISSION_STATUS_ERR err=" + e.getMessage(), e); + + JSObject errorStatus = new JSObject(); + errorStatus.put("error", e.getMessage()); + return errorStatus; + } + } + + /** + * Get human-readable importance description + * + * @param importance Notification importance level + * @return Human-readable description + */ + private String getImportanceDescription(int importance) { + switch (importance) { + case NotificationManager.IMPORTANCE_NONE: + return "Blocked - No notifications will be shown"; + case NotificationManager.IMPORTANCE_MIN: + return "Minimal - Only shown in notification shade"; + case NotificationManager.IMPORTANCE_LOW: + return "Low - Shown in notification shade, no sound"; + case NotificationManager.IMPORTANCE_DEFAULT: + return "Default - Shown with sound and on lock screen"; + case NotificationManager.IMPORTANCE_HIGH: + return "High - Shown with sound, on lock screen, and heads-up"; + case NotificationManager.IMPORTANCE_MAX: + return "Maximum - Shown with sound, on lock screen, heads-up, and can bypass Do Not Disturb"; + default: + return "Unknown importance level: " + importance; + } + } + + /** + * Check if the notification system is ready to schedule notifications + * + * @return true if ready, false otherwise + */ + public boolean isReadyToSchedule() { + try { + boolean postNotificationsGranted = checkPostNotificationsPermission(); + boolean channelEnabled = channelManager.isChannelEnabled(); + boolean exactAlarmsGranted = checkExactAlarmsPermission(); + + boolean ready = postNotificationsGranted && channelEnabled && exactAlarmsGranted; + + Log.d(TAG, "DN|READY_CHECK ready=" + ready + + " postGranted=" + postNotificationsGranted + + " channelEnabled=" + channelEnabled + + " exactGranted=" + exactAlarmsGranted); + + return ready; + + } catch (Exception e) { + Log.e(TAG, "DN|READY_CHECK_ERR err=" + e.getMessage(), e); + return false; + } + } + + /** + * Get a summary of issues preventing notification scheduling + * + * @return Array of issue descriptions + */ + public String[] getIssues() { + try { + java.util.List issues = new java.util.ArrayList<>(); + + if (!checkPostNotificationsPermission()) { + issues.add("POST_NOTIFICATIONS permission not granted"); + } + + if (!channelManager.isChannelEnabled()) { + issues.add("Notification channel is disabled or blocked"); + } + + if (!checkExactAlarmsPermission()) { + issues.add("Exact alarm permission not granted"); + } + + return issues.toArray(new String[0]); + + } catch (Exception e) { + Log.e(TAG, "DN|ISSUES_ERR err=" + e.getMessage(), e); + return new String[]{"Error checking status: " + e.getMessage()}; + } + } +} diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/OptimizedWorker.java b/android/plugin/src/main/java/com/timesafari/dailynotification/OptimizedWorker.java deleted file mode 100644 index 7d0e124..0000000 --- a/android/plugin/src/main/java/com/timesafari/dailynotification/OptimizedWorker.java +++ /dev/null @@ -1,304 +0,0 @@ -/** - * OptimizedWorker.java - * - * Base class for optimized WorkManager workers with hygiene best practices - * Implements proper lifecycle management, resource cleanup, and performance monitoring - * - * @author Matthew Raymer - * @version 2.0.0 - Optimized Architecture - */ - -package com.timesafari.dailynotification; - -import android.content.Context; -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.work.Worker; -import androidx.work.WorkerParameters; - -import java.util.concurrent.TimeUnit; - -/** - * Base class for optimized WorkManager workers - * - * Features: - * - Proper lifecycle management - * - Resource cleanup - * - Performance monitoring - * - Error handling - * - Timeout management - */ -public abstract class OptimizedWorker extends Worker { - - private static final String TAG = "OptimizedWorker"; - - // Performance monitoring - private long startTime; - private long endTime; - private boolean isCompleted = false; - - // Resource management - private boolean resourcesInitialized = false; - - /** - * Constructor - * - * @param context Application context - * @param params Worker parameters - */ - public OptimizedWorker(@NonNull Context context, @NonNull WorkerParameters params) { - super(context, params); - - Log.d(TAG, "OptimizedWorker initialized: " + getClass().getSimpleName()); - } - - /** - * Main work execution with hygiene best practices - * - * @return Result of the work - */ - @NonNull - @Override - public final Result doWork() { - startTime = System.currentTimeMillis(); - - try { - Log.i(TAG, "Starting work: " + getClass().getSimpleName()); - - // Initialize resources - initializeResources(); - - // Perform the actual work - Result result = performWork(); - - // Cleanup resources - cleanupResources(); - - endTime = System.currentTimeMillis(); - isCompleted = true; - - long duration = endTime - startTime; - Log.i(TAG, "Work completed: " + getClass().getSimpleName() + - " in " + duration + "ms with result: " + result); - - return result; - - } catch (Exception e) { - Log.e(TAG, "Work failed: " + getClass().getSimpleName(), e); - - // Ensure cleanup even on failure - cleanupResources(); - - endTime = System.currentTimeMillis(); - isCompleted = true; - - return Result.failure(); - } - } - - /** - * Initialize resources for the worker - */ - private void initializeResources() { - try { - if (!resourcesInitialized) { - onInitializeResources(); - resourcesInitialized = true; - Log.d(TAG, "Resources initialized: " + getClass().getSimpleName()); - } - } catch (Exception e) { - Log.e(TAG, "Error initializing resources", e); - throw e; - } - } - - /** - * Cleanup resources after work completion - */ - private void cleanupResources() { - try { - if (resourcesInitialized) { - onCleanupResources(); - resourcesInitialized = false; - Log.d(TAG, "Resources cleaned up: " + getClass().getSimpleName()); - } - } catch (Exception e) { - Log.e(TAG, "Error cleaning up resources", e); - } - } - - /** - * Abstract method to perform the actual work - * - * @return Result of the work - */ - @NonNull - protected abstract Result performWork(); - - /** - * Override to initialize worker-specific resources - */ - protected void onInitializeResources() { - // Default implementation - override in subclasses - } - - /** - * Override to cleanup worker-specific resources - */ - protected void onCleanupResources() { - // Default implementation - override in subclasses - } - - /** - * Check if work is taking too long and should be cancelled - * - * @param maxDurationMs Maximum duration in milliseconds - * @return true if work should be cancelled - */ - protected boolean shouldCancelWork(long maxDurationMs) { - long currentTime = System.currentTimeMillis(); - long elapsed = currentTime - startTime; - - if (elapsed > maxDurationMs) { - Log.w(TAG, "Work timeout exceeded: " + elapsed + "ms > " + maxDurationMs + "ms"); - return true; - } - - return false; - } - - /** - * Check if work is cancelled - * - * @return true if work is cancelled - */ - protected boolean isWorkCancelled() { - return isStopped(); - } - - /** - * Get work duration so far - * - * @return Duration in milliseconds - */ - protected long getWorkDuration() { - if (isCompleted) { - return endTime - startTime; - } else { - return System.currentTimeMillis() - startTime; - } - } - - /** - * Log work progress - * - * @param message Progress message - */ - protected void logProgress(String message) { - long duration = getWorkDuration(); - Log.d(TAG, "[" + duration + "ms] " + getClass().getSimpleName() + ": " + message); - } - - /** - * Create success result with data - * - * @param data Result data - * @return Success result - */ - @NonNull - protected Result createSuccessResult(androidx.work.Data data) { - return Result.success(data); - } - - /** - * Create success result - * - * @return Success result - */ - @NonNull - protected Result createSuccessResult() { - return Result.success(); - } - - /** - * Create failure result with data - * - * @param data Result data - * @return Failure result - */ - @NonNull - protected Result createFailureResult(androidx.work.Data data) { - return Result.failure(data); - } - - /** - * Create failure result - * - * @return Failure result - */ - @NonNull - protected Result createFailureResult() { - return Result.failure(); - } - - /** - * Create retry result with data - * - * @param data Result data - * @return Retry result - */ - @NonNull - protected Result createRetryResult(androidx.work.Data data) { - return Result.retry(data); - } - - /** - * Create retry result - * - * @return Retry result - */ - @NonNull - protected Result createRetryResult() { - return Result.retry(); - } - - /** - * Get worker performance metrics - * - * @return Performance metrics - */ - public WorkerMetrics getMetrics() { - WorkerMetrics metrics = new WorkerMetrics(); - metrics.workerName = getClass().getSimpleName(); - metrics.startTime = startTime; - metrics.endTime = endTime; - metrics.duration = getWorkDuration(); - metrics.isCompleted = isCompleted; - metrics.resourcesInitialized = resourcesInitialized; - - return metrics; - } - - /** - * Worker performance metrics - */ - public static class WorkerMetrics { - public String workerName; - public long startTime; - public long endTime; - public long duration; - public boolean isCompleted; - public boolean resourcesInitialized; - - @Override - public String toString() { - return "WorkerMetrics{" + - "workerName='" + workerName + '\'' + - ", duration=" + duration + "ms" + - ", isCompleted=" + isCompleted + - ", resourcesInitialized=" + resourcesInitialized + - '}'; - } - } -} diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/PowerManager.java b/android/plugin/src/main/java/com/timesafari/dailynotification/PowerManager.java deleted file mode 100644 index efe97d9..0000000 --- a/android/plugin/src/main/java/com/timesafari/dailynotification/PowerManager.java +++ /dev/null @@ -1,242 +0,0 @@ -/** - * 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/PrivacyManager.java b/android/plugin/src/main/java/com/timesafari/dailynotification/PrivacyManager.java deleted file mode 100644 index 302aef0..0000000 --- a/android/plugin/src/main/java/com/timesafari/dailynotification/PrivacyManager.java +++ /dev/null @@ -1,417 +0,0 @@ -/** - * PrivacyManager.java - * - * Privacy configuration and data protection manager - * Implements GDPR compliance, data anonymization, and privacy controls - * - * @author Matthew Raymer - * @version 2.0.0 - Optimized Architecture - */ - -package com.timesafari.dailynotification; - -import android.content.Context; -import android.content.SharedPreferences; -import android.util.Log; - -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -/** - * Privacy manager for data protection and compliance - * - * Features: - * - GDPR compliance controls - * - Data anonymization - * - Privacy settings management - * - Sensitive data detection - * - Consent management - */ -public class PrivacyManager { - - private static final String TAG = "PrivacyManager"; - private static final String PREFS_NAME = "PrivacySettings"; - - // Privacy settings keys - private static final String KEY_PRIVACY_ENABLED = "privacy_enabled"; - private static final String KEY_DATA_COLLECTION = "data_collection"; - private static final String KEY_ANALYTICS_ENABLED = "analytics_enabled"; - private static final String KEY_CRASH_REPORTING = "crash_reporting"; - private static final String KEY_USER_CONSENT = "user_consent"; - private static final String KEY_DATA_RETENTION_DAYS = "data_retention_days"; - - // Default privacy settings - private static final boolean DEFAULT_PRIVACY_ENABLED = true; - private static final boolean DEFAULT_DATA_COLLECTION = false; - private static final boolean DEFAULT_ANALYTICS_ENABLED = false; - private static final boolean DEFAULT_CRASH_REPORTING = false; - private static final boolean DEFAULT_USER_CONSENT = false; - private static final int DEFAULT_DATA_RETENTION_DAYS = 30; - - // Privacy levels - public static final int PRIVACY_LEVEL_NONE = 0; - public static final int PRIVACY_LEVEL_BASIC = 1; - public static final int PRIVACY_LEVEL_ENHANCED = 2; - public static final int PRIVACY_LEVEL_MAXIMUM = 3; - - private final Context context; - private final SharedPreferences prefs; - - // Privacy configuration - private boolean privacyEnabled; - private boolean dataCollectionEnabled; - private boolean analyticsEnabled; - private boolean crashReportingEnabled; - private boolean userConsentGiven; - private int dataRetentionDays; - private int privacyLevel; - - // Sensitive data patterns - private final Map sensitiveDataPatterns = new ConcurrentHashMap<>(); - - /** - * Initialize privacy manager - * - * @param context Application context - */ - public PrivacyManager(Context context) { - this.context = context; - this.prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); - - // Initialize privacy settings - loadPrivacySettings(); - - // Initialize sensitive data patterns - initializeSensitiveDataPatterns(); - - Log.d(TAG, "PrivacyManager initialized with level: " + privacyLevel); - } - - /** - * Load privacy settings from storage - */ - private void loadPrivacySettings() { - privacyEnabled = prefs.getBoolean(KEY_PRIVACY_ENABLED, DEFAULT_PRIVACY_ENABLED); - dataCollectionEnabled = prefs.getBoolean(KEY_DATA_COLLECTION, DEFAULT_DATA_COLLECTION); - analyticsEnabled = prefs.getBoolean(KEY_ANALYTICS_ENABLED, DEFAULT_ANALYTICS_ENABLED); - crashReportingEnabled = prefs.getBoolean(KEY_CRASH_REPORTING, DEFAULT_CRASH_REPORTING); - userConsentGiven = prefs.getBoolean(KEY_USER_CONSENT, DEFAULT_USER_CONSENT); - dataRetentionDays = prefs.getInt(KEY_DATA_RETENTION_DAYS, DEFAULT_DATA_RETENTION_DAYS); - - // Calculate privacy level - calculatePrivacyLevel(); - } - - /** - * Calculate privacy level based on settings - */ - private void calculatePrivacyLevel() { - if (!privacyEnabled) { - privacyLevel = PRIVACY_LEVEL_NONE; - } else if (!dataCollectionEnabled && !analyticsEnabled && !crashReportingEnabled) { - privacyLevel = PRIVACY_LEVEL_MAXIMUM; - } else if (!dataCollectionEnabled && !analyticsEnabled) { - privacyLevel = PRIVACY_LEVEL_ENHANCED; - } else if (!dataCollectionEnabled) { - privacyLevel = PRIVACY_LEVEL_BASIC; - } else { - privacyLevel = PRIVACY_LEVEL_BASIC; - } - } - - /** - * Initialize sensitive data patterns - */ - private void initializeSensitiveDataPatterns() { - sensitiveDataPatterns.put("email", "\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b"); - sensitiveDataPatterns.put("phone", "\\b\\d{3}-\\d{3}-\\d{4}\\b"); - sensitiveDataPatterns.put("ssn", "\\b\\d{3}-\\d{2}-\\d{4}\\b"); - sensitiveDataPatterns.put("credit_card", "\\b\\d{4}[\\s-]?\\d{4}[\\s-]?\\d{4}[\\s-]?\\d{4}\\b"); - sensitiveDataPatterns.put("ip_address", "\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\b"); - sensitiveDataPatterns.put("mac_address", "\\b([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})\\b"); - } - - /** - * Set privacy enabled - * - * @param enabled true to enable privacy protection - */ - public void setPrivacyEnabled(boolean enabled) { - this.privacyEnabled = enabled; - prefs.edit().putBoolean(KEY_PRIVACY_ENABLED, enabled).apply(); - calculatePrivacyLevel(); - - Log.i(TAG, "Privacy protection " + (enabled ? "enabled" : "disabled")); - } - - /** - * Set data collection enabled - * - * @param enabled true to enable data collection - */ - public void setDataCollectionEnabled(boolean enabled) { - this.dataCollectionEnabled = enabled; - prefs.edit().putBoolean(KEY_DATA_COLLECTION, enabled).apply(); - calculatePrivacyLevel(); - - Log.i(TAG, "Data collection " + (enabled ? "enabled" : "disabled")); - } - - /** - * Set analytics enabled - * - * @param enabled true to enable analytics - */ - public void setAnalyticsEnabled(boolean enabled) { - this.analyticsEnabled = enabled; - prefs.edit().putBoolean(KEY_ANALYTICS_ENABLED, enabled).apply(); - calculatePrivacyLevel(); - - Log.i(TAG, "Analytics " + (enabled ? "enabled" : "disabled")); - } - - /** - * Set crash reporting enabled - * - * @param enabled true to enable crash reporting - */ - public void setCrashReportingEnabled(boolean enabled) { - this.crashReportingEnabled = enabled; - prefs.edit().putBoolean(KEY_CRASH_REPORTING, enabled).apply(); - calculatePrivacyLevel(); - - Log.i(TAG, "Crash reporting " + (enabled ? "enabled" : "disabled")); - } - - /** - * Set user consent - * - * @param consent true if user has given consent - */ - public void setUserConsent(boolean consent) { - this.userConsentGiven = consent; - prefs.edit().putBoolean(KEY_USER_CONSENT, consent).apply(); - - Log.i(TAG, "User consent " + (consent ? "given" : "revoked")); - } - - /** - * Set data retention period - * - * @param days Number of days to retain data - */ - public void setDataRetentionDays(int days) { - this.dataRetentionDays = days; - prefs.edit().putInt(KEY_DATA_RETENTION_DAYS, days).apply(); - - Log.i(TAG, "Data retention set to " + days + " days"); - } - - /** - * Check if privacy is enabled - * - * @return true if privacy is enabled - */ - public boolean isPrivacyEnabled() { - return privacyEnabled; - } - - /** - * Check if data collection is enabled - * - * @return true if data collection is enabled - */ - public boolean isDataCollectionEnabled() { - return dataCollectionEnabled && userConsentGiven; - } - - /** - * Check if analytics is enabled - * - * @return true if analytics is enabled - */ - public boolean isAnalyticsEnabled() { - return analyticsEnabled && userConsentGiven; - } - - /** - * Check if crash reporting is enabled - * - * @return true if crash reporting is enabled - */ - public boolean isCrashReportingEnabled() { - return crashReportingEnabled && userConsentGiven; - } - - /** - * Check if user has given consent - * - * @return true if user has given consent - */ - public boolean hasUserConsent() { - return userConsentGiven; - } - - /** - * Get data retention period - * - * @return Number of days to retain data - */ - public int getDataRetentionDays() { - return dataRetentionDays; - } - - /** - * Get privacy level - * - * @return Privacy level (0-3) - */ - public int getPrivacyLevel() { - return privacyLevel; - } - - /** - * Anonymize data based on privacy level - * - * @param data Data to anonymize - * @return Anonymized data - */ - public String anonymizeData(String data) { - if (!privacyEnabled || data == null) { - return data; - } - - String anonymized = data; - - switch (privacyLevel) { - case PRIVACY_LEVEL_MAXIMUM: - // Remove all potentially sensitive data - anonymized = removeAllSensitiveData(anonymized); - break; - case PRIVACY_LEVEL_ENHANCED: - // Remove most sensitive data - anonymized = removeSensitiveData(anonymized, new String[]{"email", "phone", "ssn", "credit_card"}); - break; - case PRIVACY_LEVEL_BASIC: - // Remove highly sensitive data - anonymized = removeSensitiveData(anonymized, new String[]{"ssn", "credit_card"}); - break; - case PRIVACY_LEVEL_NONE: - // No anonymization - break; - } - - return anonymized; - } - - /** - * Remove all sensitive data - * - * @param data Data to process - * @return Data with all sensitive information removed - */ - private String removeAllSensitiveData(String data) { - String result = data; - - for (String pattern : sensitiveDataPatterns.values()) { - result = result.replaceAll(pattern, "[REDACTED]"); - } - - return result; - } - - /** - * Remove specific sensitive data types - * - * @param data Data to process - * @param types Types of sensitive data to remove - * @return Data with specified sensitive information removed - */ - private String removeSensitiveData(String data, String[] types) { - String result = data; - - for (String type : types) { - String pattern = sensitiveDataPatterns.get(type); - if (pattern != null) { - result = result.replaceAll(pattern, "[REDACTED]"); - } - } - - return result; - } - - /** - * Check if data contains sensitive information - * - * @param data Data to check - * @return true if data contains sensitive information - */ - public boolean containsSensitiveData(String data) { - if (data == null) { - return false; - } - - for (String pattern : sensitiveDataPatterns.values()) { - if (data.matches(".*" + pattern + ".*")) { - return true; - } - } - - return false; - } - - /** - * Get privacy configuration summary - * - * @return Privacy configuration summary - */ - public Map getPrivacySummary() { - Map summary = new HashMap<>(); - summary.put("privacyEnabled", privacyEnabled); - summary.put("dataCollectionEnabled", dataCollectionEnabled); - summary.put("analyticsEnabled", analyticsEnabled); - summary.put("crashReportingEnabled", crashReportingEnabled); - summary.put("userConsentGiven", userConsentGiven); - summary.put("dataRetentionDays", dataRetentionDays); - summary.put("privacyLevel", privacyLevel); - summary.put("privacyLevelName", getPrivacyLevelName(privacyLevel)); - - return summary; - } - - /** - * Get privacy level name - * - * @param level Privacy level - * @return Privacy level name - */ - private String getPrivacyLevelName(int level) { - switch (level) { - case PRIVACY_LEVEL_NONE: - return "NONE"; - case PRIVACY_LEVEL_BASIC: - return "BASIC"; - case PRIVACY_LEVEL_ENHANCED: - return "ENHANCED"; - case PRIVACY_LEVEL_MAXIMUM: - return "MAXIMUM"; - default: - return "UNKNOWN"; - } - } - - /** - * Reset privacy settings to defaults - */ - public void resetToDefaults() { - setPrivacyEnabled(DEFAULT_PRIVACY_ENABLED); - setDataCollectionEnabled(DEFAULT_DATA_COLLECTION); - setAnalyticsEnabled(DEFAULT_ANALYTICS_ENABLED); - setCrashReportingEnabled(DEFAULT_CRASH_REPORTING); - setUserConsent(DEFAULT_USER_CONSENT); - setDataRetentionDays(DEFAULT_DATA_RETENTION_DAYS); - - Log.i(TAG, "Privacy settings reset to defaults"); - } -} diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/RecoveryManager.java b/android/plugin/src/main/java/com/timesafari/dailynotification/RecoveryManager.java deleted file mode 100644 index 7062751..0000000 --- a/android/plugin/src/main/java/com/timesafari/dailynotification/RecoveryManager.java +++ /dev/null @@ -1,268 +0,0 @@ -/** - * RecoveryManager.java - * - * Specialized manager for recovery and maintenance operations - * Handles rolling window management, recovery statistics, and maintenance tasks - * - * @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.List; - -/** - * Manager class for recovery and maintenance operations - * - * 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 final Context context; - private final DailyNotificationStorage storage; - private final DailyNotificationScheduler scheduler; - - /** - * Initialize the RecoveryManager - * - * @param context Android context - * @param storage Storage component for notification data - * @param scheduler Scheduler component for alarm management - */ - public RecoveryManager(Context context, DailyNotificationStorage storage, - DailyNotificationScheduler scheduler) { - this.context = context; - this.storage = storage; - this.scheduler = scheduler; - - Log.d(TAG, "RecoveryManager initialized"); - } - - /** - * Get recovery statistics and status - * - * @param call Plugin call - */ - public void getRecoveryStats(PluginCall call) { - try { - Log.d(TAG, "Getting recovery statistics"); - - // Get recovery statistics from the singleton RecoveryManager - com.timesafari.dailynotification.RecoveryManager recoveryManager = - com.timesafari.dailynotification.RecoveryManager.getInstance(context, storage, scheduler); - - 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++; - } - } - - 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 getting recovery statistics", e); - call.reject("Failed to get recovery statistics: " + e.getMessage()); - } - } - - /** - * Maintain rolling window for notifications - * - * @param call Plugin call - */ - public void maintainRollingWindow(PluginCall call) { - try { - 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 notificationsInWindow = 0; - int notificationsToSchedule = 0; - - for (NotificationContent notification : notifications) { - if (notification.getScheduledTime() >= windowStart && - notification.getScheduledTime() <= currentTime) { - notificationsInWindow++; - } - if (notification.getScheduledTime() > currentTime) { - notificationsToSchedule++; - } - } - - // Calculate notifications needed for the window - int totalNeeded = windowSize * maxNotificationsPerDay; - int notificationsNeeded = Math.max(0, totalNeeded - notificationsInWindow); - - 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 maintaining rolling window", e); - call.reject("Failed to maintain rolling window: " + e.getMessage()); - } - } - - /** - * Get rolling window statistics - * - * @param call Plugin call - */ - public void getRollingWindowStats(PluginCall call) { - try { - 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 windowStart = currentTime - (windowSize * 24 * 60 * 60 * 1000L); - - 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++; - } - } - - // 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]++; - } - } - } - - 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); - - call.resolve(result); - - } catch (Exception e) { - Log.e(TAG, "Error getting rolling window statistics", e); - call.reject("Failed to get rolling window statistics: " + e.getMessage()); - } - } - - /** - * Get reboot recovery status - * - * @param call Plugin call - */ - public void getRebootRecoveryStatus(PluginCall call) { - try { - Log.d(TAG, "Getting reboot recovery status"); - - // Get recovery statistics - com.timesafari.dailynotification.RecoveryManager recoveryManager = - com.timesafari.dailynotification.RecoveryManager.getInstance(context, storage, scheduler); - - String stats = recoveryManager.getRecoveryStats(); - - // Get notification counts - List notifications = storage.getAllNotifications(); - int totalNotifications = notifications.size(); - int scheduledNotifications = 0; - - long currentTime = System.currentTimeMillis(); - for (NotificationContent notification : notifications) { - if (notification.getScheduledTime() > currentTime) { - scheduledNotifications++; - } - } - - // Check if recovery is needed - boolean recoveryNeeded = scheduledNotifications == 0 && totalNotifications > 0; - - 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); - - call.resolve(result); - - } 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 deleted file mode 100644 index b3eea5c..0000000 --- a/android/plugin/src/main/java/com/timesafari/dailynotification/ReminderManager.java +++ /dev/null @@ -1,333 +0,0 @@ -/** - * 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 deleted file mode 100644 index 3f6fab8..0000000 --- a/android/plugin/src/main/java/com/timesafari/dailynotification/TaskCoordinationManager.java +++ /dev/null @@ -1,298 +0,0 @@ -/** - * 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 deleted file mode 100644 index d4f2548..0000000 --- a/android/plugin/src/main/java/com/timesafari/dailynotification/TimeSafariIntegrationManager.java +++ /dev/null @@ -1,299 +0,0 @@ -/** - * 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()); - } - } -} diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/WorkManagerHygiene.java b/android/plugin/src/main/java/com/timesafari/dailynotification/WorkManagerHygiene.java deleted file mode 100644 index 1711775..0000000 --- a/android/plugin/src/main/java/com/timesafari/dailynotification/WorkManagerHygiene.java +++ /dev/null @@ -1,397 +0,0 @@ -/** - * WorkManagerHygiene.java - * - * Optimized WorkManager hygiene and best practices implementation - * Handles worker lifecycle, constraints, retry policies, and resource management - * - * @author Matthew Raymer - * @version 2.0.0 - Optimized Architecture - */ - -package com.timesafari.dailynotification; - -import android.content.Context; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.os.BatteryManager; -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.work.BackoffPolicy; -import androidx.work.Constraints; -import androidx.work.Data; -import androidx.work.ExistingWorkPolicy; -import androidx.work.NetworkType; -import androidx.work.OneTimeWorkRequest; -import androidx.work.PeriodicWorkRequest; -import androidx.work.WorkInfo; -import androidx.work.WorkManager; -import androidx.work.WorkRequest; - -import java.util.concurrent.TimeUnit; - -/** - * Optimized WorkManager hygiene and best practices - * - * Responsibilities: - * - Worker lifecycle management - * - Constraint optimization - * - Retry policy management - * - Resource cleanup - * - Performance monitoring - */ -public class WorkManagerHygiene { - - private static final String TAG = "WorkManagerHygiene"; - - // WorkManager instance - private final WorkManager workManager; - private final Context context; - - // Worker configuration - private static final String FETCH_WORK_NAME = "daily_notification_fetch"; - private static final String MAINTENANCE_WORK_NAME = "daily_notification_maintenance"; - private static final String RECOVERY_WORK_NAME = "daily_notification_recovery"; - - // Timing configuration - private static final long FETCH_INTERVAL_HOURS = 6; // Every 6 hours - private static final long MAINTENANCE_INTERVAL_HOURS = 24; // Daily - private static final long RECOVERY_INTERVAL_HOURS = 12; // Twice daily - - // Retry configuration - private static final int MAX_RETRY_ATTEMPTS = 3; - private static final long BACKOFF_DELAY_MINUTES = 15; - - /** - * Initialize WorkManager hygiene - * - * @param context Application context - */ - public WorkManagerHygiene(Context context) { - this.context = context; - this.workManager = WorkManager.getInstance(context); - - Log.d(TAG, "WorkManagerHygiene initialized"); - } - - /** - * Schedule optimized fetch worker with proper constraints - */ - public void scheduleFetchWorker() { - try { - Log.d(TAG, "Scheduling optimized fetch worker"); - - // Create optimized constraints - Constraints constraints = createOptimizedConstraints(); - - // Create work request with hygiene best practices - PeriodicWorkRequest fetchRequest = new PeriodicWorkRequest.Builder( - DailyNotificationFetchWorker.class, - FETCH_INTERVAL_HOURS, - TimeUnit.HOURS - ) - .setConstraints(constraints) - .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, BACKOFF_DELAY_MINUTES, TimeUnit.MINUTES) - .addTag("fetch_worker") - .build(); - - // Enqueue with proper policy - workManager.enqueueUniquePeriodicWork( - FETCH_WORK_NAME, - ExistingWorkPolicy.KEEP, - fetchRequest - ); - - Log.i(TAG, "Fetch worker scheduled successfully"); - - } catch (Exception e) { - Log.e(TAG, "Error scheduling fetch worker", e); - } - } - - /** - * Schedule optimized maintenance worker - */ - public void scheduleMaintenanceWorker() { - try { - Log.d(TAG, "Scheduling optimized maintenance worker"); - - // Create constraints for maintenance (less restrictive) - Constraints constraints = new Constraints.Builder() - .setRequiredNetworkType(NetworkType.NOT_REQUIRED) - .setRequiresBatteryNotLow(true) - .setRequiresStorageNotLow(true) - .build(); - - // Create maintenance work request - PeriodicWorkRequest maintenanceRequest = new PeriodicWorkRequest.Builder( - DailyNotificationMaintenanceWorker.class, - MAINTENANCE_INTERVAL_HOURS, - TimeUnit.HOURS - ) - .setConstraints(constraints) - .setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY_MINUTES, TimeUnit.MINUTES) - .addTag("maintenance_worker") - .build(); - - // Enqueue maintenance work - workManager.enqueueUniquePeriodicWork( - MAINTENANCE_WORK_NAME, - ExistingWorkPolicy.REPLACE, - maintenanceRequest - ); - - Log.i(TAG, "Maintenance worker scheduled successfully"); - - } catch (Exception e) { - Log.e(TAG, "Error scheduling maintenance worker", e); - } - } - - /** - * Schedule recovery worker for system recovery - */ - public void scheduleRecoveryWorker() { - try { - Log.d(TAG, "Scheduling recovery worker"); - - // Create constraints for recovery - Constraints constraints = new Constraints.Builder() - .setRequiredNetworkType(NetworkType.CONNECTED) - .setRequiresBatteryNotLow(false) // Allow even on low battery - .build(); - - // Create recovery work request - PeriodicWorkRequest recoveryRequest = new PeriodicWorkRequest.Builder( - DailyNotificationRecoveryWorker.class, - RECOVERY_INTERVAL_HOURS, - TimeUnit.HOURS - ) - .setConstraints(constraints) - .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, BACKOFF_DELAY_MINUTES, TimeUnit.MINUTES) - .addTag("recovery_worker") - .build(); - - // Enqueue recovery work - workManager.enqueueUniquePeriodicWork( - RECOVERY_WORK_NAME, - ExistingWorkPolicy.KEEP, - recoveryRequest - ); - - Log.i(TAG, "Recovery worker scheduled successfully"); - - } catch (Exception e) { - Log.e(TAG, "Error scheduling recovery worker", e); - } - } - - /** - * Create optimized constraints for different worker types - */ - private Constraints createOptimizedConstraints() { - return new Constraints.Builder() - .setRequiredNetworkType(getOptimalNetworkType()) - .setRequiresBatteryNotLow(isBatteryOptimized()) - .setRequiresStorageNotLow(true) - .setRequiresDeviceIdle(false) // Don't wait for device idle - .build(); - } - - /** - * Determine optimal network type based on current conditions - */ - private NetworkType getOptimalNetworkType() { - ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - NetworkInfo activeNetwork = cm.getActiveNetworkInfo(); - - if (activeNetwork != null && activeNetwork.isConnected()) { - if (activeNetwork.getType() == ConnectivityManager.TYPE_WIFI) { - return NetworkType.UNMETERED; // Prefer WiFi - } else { - return NetworkType.CONNECTED; // Allow mobile data - } - } - - return NetworkType.CONNECTED; // Default to any connection - } - - /** - * Check if battery optimization is enabled - */ - private boolean isBatteryOptimized() { - BatteryManager batteryManager = (BatteryManager) context.getSystemService(Context.BATTERY_SERVICE); - if (batteryManager != null) { - int batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY); - return batteryLevel > 20; // Require battery above 20% - } - return true; // Default to true if can't determine - } - - /** - * Cancel all workers with proper cleanup - */ - public void cancelAllWorkers() { - try { - Log.d(TAG, "Cancelling all workers"); - - workManager.cancelUniqueWork(FETCH_WORK_NAME); - workManager.cancelUniqueWork(MAINTENANCE_WORK_NAME); - workManager.cancelUniqueWork(RECOVERY_WORK_NAME); - - // Cancel by tags - workManager.cancelAllWorkByTag("fetch_worker"); - workManager.cancelAllWorkByTag("maintenance_worker"); - workManager.cancelAllWorkByTag("recovery_worker"); - - Log.i(TAG, "All workers cancelled successfully"); - - } catch (Exception e) { - Log.e(TAG, "Error cancelling workers", e); - } - } - - /** - * Get worker status and health information - */ - public WorkerStatus getWorkerStatus() { - try { - WorkerStatus status = new WorkerStatus(); - - // Check fetch worker - status.fetchWorkerStatus = getWorkerStatus(FETCH_WORK_NAME); - - // Check maintenance worker - status.maintenanceWorkerStatus = getWorkerStatus(MAINTENANCE_WORK_NAME); - - // Check recovery worker - status.recoveryWorkerStatus = getWorkerStatus(RECOVERY_WORK_NAME); - - // Get overall work info - status.totalWorkCount = workManager.getWorkInfos().get().size(); - - Log.d(TAG, "Worker status retrieved: " + status.toString()); - - return status; - - } catch (Exception e) { - Log.e(TAG, "Error getting worker status", e); - return new WorkerStatus(); // Return empty status - } - } - - /** - * Get status for specific worker - */ - private String getWorkerStatus(String workName) { - try { - var workInfos = workManager.getWorkInfosForUniqueWork(workName).get(); - - if (workInfos.isEmpty()) { - return "NOT_SCHEDULED"; - } - - WorkInfo.State state = workInfos.get(0).getState(); - return state.toString(); - - } catch (Exception e) { - Log.e(TAG, "Error getting status for worker: " + workName, e); - return "ERROR"; - } - } - - /** - * Perform worker hygiene cleanup - */ - public void performHygieneCleanup() { - try { - Log.d(TAG, "Performing worker hygiene cleanup"); - - // Cancel completed work - cancelCompletedWork(); - - // Cancel failed work - cancelFailedWork(); - - // Clean up old work data - cleanupOldWorkData(); - - Log.i(TAG, "Worker hygiene cleanup completed"); - - } catch (Exception e) { - Log.e(TAG, "Error performing hygiene cleanup", e); - } - } - - /** - * Cancel completed work to free resources - */ - private void cancelCompletedWork() { - try { - var allWorkInfos = workManager.getWorkInfos().get(); - - for (WorkInfo workInfo : allWorkInfos) { - if (workInfo.getState() == WorkInfo.State.SUCCEEDED) { - workManager.cancelWorkById(workInfo.getId()); - Log.d(TAG, "Cancelled completed work: " + workInfo.getId()); - } - } - - } catch (Exception e) { - Log.e(TAG, "Error cancelling completed work", e); - } - } - - /** - * Cancel failed work to prevent retry loops - */ - private void cancelFailedWork() { - try { - var allWorkInfos = workManager.getWorkInfos().get(); - - for (WorkInfo workInfo : allWorkInfos) { - if (workInfo.getState() == WorkInfo.State.FAILED) { - workManager.cancelWorkById(workInfo.getId()); - Log.d(TAG, "Cancelled failed work: " + workInfo.getId()); - } - } - - } catch (Exception e) { - Log.e(TAG, "Error cancelling failed work", e); - } - } - - /** - * Clean up old work data - */ - private void cleanupOldWorkData() { - try { - // This would clean up old work data from storage - // Implementation depends on specific storage mechanism - Log.d(TAG, "Cleaned up old work data"); - - } catch (Exception e) { - Log.e(TAG, "Error cleaning up old work data", e); - } - } - - /** - * Worker status container - */ - public static class WorkerStatus { - public String fetchWorkerStatus = "UNKNOWN"; - public String maintenanceWorkerStatus = "UNKNOWN"; - public String recoveryWorkerStatus = "UNKNOWN"; - public int totalWorkCount = 0; - - @Override - public String toString() { - return "WorkerStatus{" + - "fetchWorkerStatus='" + fetchWorkerStatus + '\'' + - ", maintenanceWorkerStatus='" + maintenanceWorkerStatus + '\'' + - ", recoveryWorkerStatus='" + recoveryWorkerStatus + '\'' + - ", totalWorkCount=" + totalWorkCount + - '}'; - } - } -} diff --git a/scripts/comprehensive-test-v2.sh b/scripts/comprehensive-test-v2.sh new file mode 100755 index 0000000..216f223 --- /dev/null +++ b/scripts/comprehensive-test-v2.sh @@ -0,0 +1,264 @@ +#!/bin/bash + +# Comprehensive Test Suite for Daily Notification Plugin v2 +# Tests all P0 production-grade features +# Author: Matthew Raymer +# Date: 2025-10-14 + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Test configuration +APP_PACKAGE="com.timesafari.dailynotification" +APP_ACTIVITY="com.timesafari.dailynotification.MainActivity" +TEST_TIMEOUT=30 +NOTIFICATION_DELAY=5 # 5 minutes for testing + +# Test results tracking +TESTS_PASSED=0 +TESTS_FAILED=0 +TESTS_TOTAL=0 + +# Logging functions +log_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[PASS]${NC} $1" + ((TESTS_PASSED++)) +} + +log_error() { + echo -e "${RED}[FAIL]${NC} $1" + ((TESTS_FAILED++)) +} + +log_warning() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +# Test execution function +run_test() { + local test_name="$1" + local test_command="$2" + local expected_result="$3" + + ((TESTS_TOTAL++)) + log_info "Running: $test_name" + + if eval "$test_command"; then + log_success "$test_name - $expected_result" + return 0 + else + log_error "$test_name - Expected: $expected_result" + return 1 + fi +} + +# Check if device is connected +check_device() { + log_info "Checking device connection..." + if ! adb devices | grep -q "device$"; then + log_error "No Android device connected" + exit 1 + fi + log_success "Device connected" +} + +# Check if app is installed +check_app_installed() { + log_info "Checking if app is installed..." + if ! adb shell pm list packages | grep -q "$APP_PACKAGE"; then + log_error "App not installed: $APP_PACKAGE" + exit 1 + fi + log_success "App installed: $APP_PACKAGE" +} + +# Test 2.1: Channel Management +test_channel_management() { + log_info "=== Test 2.1: Channel Management ===" + + # Check if channel exists + run_test "Channel Exists" \ + "adb shell 'dumpsys notification | grep -q daily_default'" \ + "Channel daily_default exists" + + # Check channel importance + run_test "Channel Importance" \ + "adb shell 'dumpsys notification | grep -A5 daily_default | grep -q mImportance=4'" \ + "Channel has proper importance level" + + # Check channel features + run_test "Channel Sound" \ + "adb shell 'dumpsys notification | grep -A5 daily_default | grep -q mSound='" \ + "Channel has sound enabled" + + run_test "Channel Lights" \ + "adb shell 'dumpsys notification | grep -A5 daily_default | grep -q mLights=true'" \ + "Channel has lights enabled" + + run_test "Channel Vibration" \ + "adb shell 'dumpsys notification | grep -A5 daily_default | grep -q mVibrationEnabled=true'" \ + "Channel has vibration enabled" +} + +# Test 2.2: PendingIntent & Exact Alarms +test_pendingintent_exact_alarms() { + log_info "=== Test 2.2: PendingIntent & Exact Alarms ===" + + # Check exact alarm permission + run_test "Exact Alarm Permission" \ + "adb shell 'dumpsys alarm | grep -q SCHEDULE_EXACT_ALARM.*10192'" \ + "App has exact alarm permission" + + # Check scheduled alarms + run_test "Alarms Scheduled" \ + "adb shell 'dumpsys alarm | grep timesafari | wc -l | grep -q [0-9]'" \ + "Alarms are scheduled" + + # Check alarm type + run_test "RTC_WAKEUP Alarms" \ + "adb shell 'dumpsys alarm | grep timesafari | grep -q RTC_WAKEUP'" \ + "Alarms use RTC_WAKEUP type" + + # Check exact timing + run_test "Exact Timing" \ + "adb shell 'dumpsys alarm | grep timesafari -A2 | grep -q window=0'" \ + "Alarms use exact timing (window=0)" + + # Check PendingIntent records + run_test "PendingIntent Records" \ + "adb shell 'dumpsys alarm | grep timesafari | grep -q PendingIntent'" \ + "PendingIntent records exist" +} + +# Test 3.1: JIT Freshness Re-check +test_jit_freshness() { + log_info "=== Test 3.1: JIT Freshness Re-check ===" + + # Start monitoring logs + log_info "Starting log monitoring for JIT freshness..." + + # Launch app to trigger JIT check + adb shell am start -n "$APP_PACKAGE/$APP_ACTIVITY" > /dev/null 2>&1 + sleep 2 + + # Check for JIT freshness logs + run_test "JIT Freshness Check" \ + "adb logcat -d | grep -q 'Content is fresh.*skipping JIT refresh'" \ + "JIT freshness check is working" + + # Check for TTL enforcer logs + run_test "TTL Enforcer" \ + "adb logcat -d | grep -q 'DailyNotificationTTLEnforcer'" \ + "TTL enforcer is active" +} + +# Test 4.1: Recovery Coexistence +test_recovery_coexistence() { + log_info "=== Test 4.1: Recovery Coexistence ===" + + # Check RecoveryManager logs + run_test "RecoveryManager Active" \ + "adb logcat -d | grep -q 'RecoveryManager'" \ + "RecoveryManager is active" + + # Check app startup recovery + run_test "App Startup Recovery" \ + "adb logcat -d | grep -q 'APP_STARTUP'" \ + "App startup recovery is working" + + # Check alarm count after recovery + local alarm_count=$(adb shell 'dumpsys alarm | grep timesafari | wc -l') + run_test "Alarms After Recovery" \ + "test $alarm_count -gt 0" \ + "Alarms exist after recovery ($alarm_count alarms)" +} + +# Test notification scheduling +test_notification_scheduling() { + log_info "=== Test: Notification Scheduling ===" + + # Launch app + adb shell am start -n "$APP_PACKAGE/$APP_ACTIVITY" > /dev/null 2>&1 + sleep 2 + + # Check if we can schedule a notification + run_test "Notification Scheduling" \ + "adb shell 'dumpsys alarm | grep timesafari | wc -l | grep -q [0-9]'" \ + "Notifications can be scheduled" + + # Get next notification time + local next_notification=$(adb shell "dumpsys alarm | grep 'timesafari.*NOTIFICATION' -A2 | grep 'origWhen=' | head -1 | sed 's/.*origWhen=//' | sed 's/ window.*//'") + log_info "Next notification scheduled for: $next_notification" +} + +# Test comprehensive status +test_comprehensive_status() { + log_info "=== Test: Comprehensive Status ===" + + # Launch app and check status + adb shell am start -n "$APP_PACKAGE/$APP_ACTIVITY" > /dev/null 2>&1 + sleep 2 + + # Check if app is running + run_test "App Running" \ + "adb shell 'ps | grep -q $APP_PACKAGE'" \ + "App is running" + + # Check notification permissions + run_test "Notification Permissions" \ + "adb shell 'dumpsys package $APP_PACKAGE | grep -q POST_NOTIFICATIONS'" \ + "App has notification permissions" +} + +# Main test execution +main() { + echo "==========================================" + echo "Daily Notification Plugin - Comprehensive Test Suite v2" + echo "==========================================" + echo "Testing all P0 production-grade features" + echo "Date: $(date)" + echo "==========================================" + + # Pre-flight checks + check_device + check_app_installed + + # Run all tests + test_channel_management + test_pendingintent_exact_alarms + test_jit_freshness + test_recovery_coexistence + test_notification_scheduling + test_comprehensive_status + + # Test results summary + echo "==========================================" + echo "TEST RESULTS SUMMARY" + echo "==========================================" + echo "Total Tests: $TESTS_TOTAL" + echo -e "Passed: ${GREEN}$TESTS_PASSED${NC}" + echo -e "Failed: ${RED}$TESTS_FAILED${NC}" + + if [ $TESTS_FAILED -eq 0 ]; then + echo -e "${GREEN}ALL TESTS PASSED! 🎉${NC}" + echo "P0 features are working correctly" + exit 0 + else + echo -e "${RED}SOME TESTS FAILED${NC}" + echo "Please review failed tests and fix issues" + exit 1 + fi +} + +# Run main function +main "$@"