# Plugin Behavior Exploration - Initial Findings **Author**: Matthew Raymer **Date**: November 2025 **Status**: Initial Code Review - In Progress ## Purpose This document contains initial findings from code-level inspection of the plugin. These findings should be verified through actual testing using [Plugin Behavior Exploration Template](./plugin-behavior-exploration-template.md). --- ## 0. Behavior Definitions & Investigation Scope Before examining the code, we need to clearly define what behaviors we're investigating and what each scenario means. ### 0.1 App State Scenarios #### Swipe from Recents (Recent Apps List) **What it is**: User swipes the app away from the Android recent apps list (app switcher) or iOS app switcher. **What happens**: - **Android**: The app's UI is removed from the recent apps list, but: - The app process may still be running in the background - The app may be killed by the OS later due to memory pressure - **AlarmManager alarms remain scheduled** and will fire even if the process is killed - The OS will recreate the app process when the alarm fires - **iOS**: The app is terminated, but: - **UNUserNotificationCenter notifications remain scheduled** and will fire - **Calendar/time-based triggers persist across reboot** - **TimeInterval triggers also persist across reboot** (UNLESS they were scheduled with `repeats = false` AND the reboot occurs before the elapsed interval) - The app does not run in the background (unless it has active background tasks) - Notifications fire even though the app is not running - **No plugin code runs when notification fires** unless the user interacts with the notification **Key Point**: Swiping from recents does **not** cancel scheduled alarms/notifications. The OS maintains them separately from the app process. **Android Nuance - Swipe vs Kill**: - **"Swipe away" DOES NOT kill your process**; the OS may kill it later due to memory pressure - **AlarmManager remains unaffected** by swipe - alarms stay scheduled - **WorkManager tasks remain scheduled** regardless of swipe - The app process may continue running in the background after swipe - Only Force Stop actually cancels alarms and prevents execution **Investigation Goal**: Verify that alarms/notifications still fire after the app is swiped away. --- #### Force Stop (Android Only) **What it is**: User goes to Settings → Apps → [Your App] → Force Stop. This is a **hard kill** that is different from swiping from recents. **What happens**: - **All alarms are immediately cancelled** by the OS - **All WorkManager tasks are cancelled** - **All broadcast receivers are blocked** (including BOOT_COMPLETED) - **All JobScheduler jobs are cancelled** - **The app cannot run** until the user manually opens it again - **No background execution** is possible **Key Point**: Force Stop is a **hard boundary** that cannot be bypassed. It's more severe than swiping from recents. **Investigation Goal**: Verify that alarms do NOT fire after force stop, and that the plugin can detect and recover when the app is opened again. **Difference from Swipe**: - **Swipe**: Alarms remain scheduled, app may still run in background - **Force Stop**: Alarms are cancelled, app cannot run until manually opened --- #### App Still Functioning When Not Visible **Android**: - When an app is swiped from recents but not force-stopped: - The app process may continue running in the background - Background services can continue - WorkManager tasks continue - AlarmManager alarms remain scheduled - The app is just not visible in the recent apps list - The OS may kill the process later due to memory pressure, but alarms remain scheduled **iOS**: - When an app is swiped from the app switcher: - The app process is terminated - Background tasks (BGTaskScheduler) may still execute (system-controlled, but **opportunistic, not exact**) - UNUserNotificationCenter notifications remain scheduled - The app does not run in the foreground or background (unless it has active background tasks) - **No persistent background execution** after user swipe - **No alarm-like wake** for plugins (unlike Android AlarmManager) - **No background execution at notification time** unless user interacts **iOS Limitations**: - No background execution at notification fire time unless user interacts - No alarm-style wakeups exist on iOS - Background execution (BGTaskScheduler) cannot be used for precise timing - Notifications survive reboot but plugin code does not run automatically **Investigation Goal**: Understand that "not visible" does not mean "not functioning" for alarms/notifications, but also understand iOS limitations on background execution. --- ### 0.2 App Launch Recovery - How It Should Work App Launch Recovery is the mechanism by which the plugin detects and handles missed alarms/notifications when the app starts. #### Recovery Scenarios ##### Cold Start **What it is**: App is launched from a completely terminated state (process was killed or never started). **Recovery Process**: 1. Plugin's `load()` method is called 2. Plugin initializes database/storage 3. Plugin queries for missed alarms/notifications: - Find alarms with `scheduled_time < now` and `delivery_status != 'delivered'` - Find notifications that should have fired but didn't 4. For each missed alarm/notification: - Generate a "missed alarm" event or notification - If repeating, reschedule the next occurrence - Update delivery status to "missed" or "delivered" 5. Reschedule future alarms/notifications that are still valid 6. Verify active alarms match stored alarms **Investigation Goal**: Verify that the plugin detects missed alarms on cold start and handles them appropriately. **Android Force Stop Detection**: - On cold start, query AlarmManager for active alarms - Query plugin DB schedules - If `(DB.count > 0 && AlarmManager.count == 0)`: **Force Stop detected** - Recovery: Mark all past schedules as missed, reschedule all future schedules, emit missed notifications --- ##### Warm Start **What it is**: App is returning from background (app was paused but process still running). **Recovery Process**: 1. Plugin's `load()` method may be called (or app resumes) 2. Plugin checks for missed alarms/notifications (same as cold start) 3. Plugin verifies that active alarms are still scheduled correctly 4. Plugin reschedules if any alarms were cancelled (shouldn't happen, but verify) **Investigation Goal**: Verify that the plugin checks for missed alarms on warm start and verifies active alarms. --- ##### Force Stop Recovery (Android) **What it is**: App was force-stopped and user manually opens it again. **Recovery Process**: 1. App launches (this is the only way to recover from force stop) 2. Plugin's `load()` method is called 3. Plugin detects that alarms were cancelled (all alarms have `scheduled_time < now` or are missing from AlarmManager) 4. Plugin queries database for all enabled alarms 5. For each alarm: - If `scheduled_time < now`: Mark as missed, generate missed alarm event, reschedule if repeating - If `scheduled_time >= now`: Reschedule the alarm 6. Plugin reschedules all future alarms **Investigation Goal**: Verify that the plugin can detect force stop scenario and fully recover all alarms. **Key Difference**: Force stop recovery is more comprehensive than normal app launch recovery because ALL alarms were cancelled, not just missed ones. --- ### 0.3 What We're Investigating For each scenario, we want to know: 1. **Does the alarm/notification fire?** (OS behavior) 2. **Does the plugin detect missed alarms?** (Plugin behavior) 3. **Does the plugin recover/reschedule?** (Plugin behavior) 4. **What happens on next app launch?** (Recovery behavior) **Expected Behaviors**: | Scenario | Alarm Fires? | Plugin Detects Missed? | Plugin Recovers? | | -------- | ------------ | ---------------------- | ---------------- | | Swipe from recents | ✅ Yes (OS) | N/A (fired) | N/A | | Force stop | ❌ No (OS cancels) | ✅ Should detect | ✅ Should recover | | Device reboot (Android) | ❌ No (OS cancels) | ✅ Should detect | ✅ Should recover | | Device reboot (iOS) | ✅ Yes (OS persists) | ⚠️ May detect | ⚠️ May recover | | Cold start | N/A | ✅ Should detect | ✅ Should recover | | Warm start | N/A | ✅ Should detect | ✅ Should verify | --- ## 1. Android Findings ### 1.1 Boot Receiver Implementation **Status**: ✅ **IMPLEMENTED** **Location**: `android/src/main/java/com/timesafari/dailynotification/BootReceiver.kt` **Findings**: - Boot receiver exists and handles `ACTION_BOOT_COMPLETED` (line 24) - Reschedules alarms from database (line 38+) - Loads enabled schedules from Room database (line 40) - Reschedules both "fetch" and "notify" schedules (lines 46-81) **Gap Identified**: - **Missed Alarm Handling**: Boot receiver only reschedules FUTURE alarms - Line 64: `if (nextRunTime > System.currentTimeMillis())` - This means if an alarm was scheduled for before the reboot time, it won't be rescheduled - **No missed alarm detection or notification** **Recommendation**: Add missed alarm detection in `rescheduleNotifications()` method --- ### 1.2 Missed Alarm Detection **Status**: ⚠️ **PARTIAL** **Location**: `android/src/main/java/com/timesafari/dailynotification/dao/NotificationContentDao.java` **Findings**: - DAO has query for missed alarms: `getNotificationsReadyForDelivery()` (line 98) - Query: `SELECT * FROM notification_content WHERE scheduled_time <= :currentTime AND delivery_status != 'delivered'` - This can identify notifications that should have fired but haven't **Gap Identified**: - **Not called on app launch**: The `DailyNotificationPlugin.load()` method (line 91) only initializes the database - No recovery logic in `load()` method - Query exists but may not be used for missed alarm detection **Recommendation**: Add missed alarm detection in `load()` method or create separate recovery method --- ### 1.3 App Launch Recovery **Status**: ❌ **NOT IMPLEMENTED** **Location**: `android/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.kt` **Expected Behavior** (as defined in Section 0.2): **Cold Start Recovery**: 1. Plugin `load()` method called 2. Query database for missed alarms: `scheduled_time < now AND delivery_status != 'delivered'` 3. For each missed alarm: - Generate missed alarm event/notification - Reschedule if repeating - Update delivery status 4. Reschedule all future alarms from database 5. Verify active alarms match stored alarms **Warm Start Recovery**: 1. Plugin checks for missed alarms (same as cold start) 2. Verify active alarms are still scheduled 3. Reschedule if any were cancelled **Force Stop Recovery**: 1. Detect that all alarms were cancelled (force stop scenario) 2. Query database for ALL enabled alarms 3. For each alarm: - If `scheduled_time < now`: Mark as missed, generate event, reschedule if repeating - If `scheduled_time >= now`: Reschedule immediately 4. Fully restore alarm state **Current Implementation**: - `load()` method (line 91) only initializes database - No recovery logic on app launch - No check for missed alarms - No rescheduling of future alarms - No distinction between cold/warm/force-stop scenarios **Gap Identified**: - Plugin does not recover on app cold/warm start - Plugin does not recover from force stop - Only boot receiver handles recovery (and only for future alarms) - No missed alarm detection on app launch **Recommendation**: 1. Add recovery logic to `load()` method or create `ReactivationManager` 2. Implement missed alarm detection using `getNotificationsReadyForDelivery()` query 3. Implement force stop detection (all alarms cancelled) 4. Implement rescheduling of future alarms from database --- ### 1.4 Persistence Completeness **Status**: ✅ **IMPLEMENTED** **Findings**: - Room database used for persistence - `Schedule` entity stores: id, kind, cron, clockTime, enabled, nextRunAt - `NotificationContentEntity` stores: id, title, body, scheduledTime, priority, etc. - `ContentCache` stores: fetched content with TTL **All Required Fields Present**: - ✅ alarm_id (Schedule.id, NotificationContentEntity.id) - ✅ trigger_time (Schedule.nextRunAt, NotificationContentEntity.scheduledTime) - ✅ repeat_rule (Schedule.cron, Schedule.clockTime) - ✅ channel_id (NotificationContentEntity - implicit) - ✅ priority (NotificationContentEntity.priority) - ✅ title, body (NotificationContentEntity) - ✅ sound_enabled, vibration_enabled (NotificationContentEntity) - ✅ created_at, updated_at (NotificationContentEntity) - ✅ enabled (Schedule.enabled) --- ### 1.5 Force Stop Recovery **Status**: ❌ **NOT IMPLEMENTED** **Expected Behavior** (as defined in Section 0.1 and 0.2): **Force Stop Scenario**: - User goes to Settings → Apps → [App] → Force Stop - All alarms are immediately cancelled by the OS - App cannot run until user manually opens it - When app is opened, it's a cold start scenario **Force Stop Recovery**: 1. Detect that alarms were cancelled (check AlarmManager for scheduled alarms) 2. Compare with database: if database has alarms but AlarmManager has none → force stop occurred 3. Query database for ALL enabled alarms 4. For each alarm: - If `scheduled_time < now`: This alarm was missed during force stop - Generate missed alarm event/notification - Reschedule next occurrence if repeating - Update delivery status - If `scheduled_time >= now`: This alarm is still in the future - Reschedule immediately 5. Fully restore alarm state **Current Implementation**: - No specific force stop detection - No recovery logic for force stop scenario - App launch recovery (if implemented) would handle this, but app launch recovery is not implemented - Cannot distinguish between normal app launch and force stop recovery **Gap Identified**: - Plugin cannot detect force stop scenario - Plugin cannot distinguish between normal app launch and force stop recovery - No special handling for force stop scenario - All alarms remain cancelled until user opens app, then plugin should recover them **Recommendation**: 1. Implement app launch recovery (which will handle force stop as a special case) 2. Add force stop detection: compare AlarmManager scheduled alarms with database 3. If force stop detected, recover ALL alarms (not just missed ones) --- ## 2. iOS Findings ### 2.1 Notification Persistence **Status**: ✅ **IMPLEMENTED** **Location**: `ios/Plugin/DailyNotificationStorage.swift` **Findings**: - Plugin uses `DailyNotificationStorage` for separate persistence - Uses UserDefaults for quick access (line 40) - Uses CoreData for structured data (line 41) - Stores notifications separately from UNUserNotificationCenter **Storage Components**: - UserDefaults: Settings, last fetch, BGTask tracking - CoreData: NotificationContent, Schedule entities - UNUserNotificationCenter: OS-managed notification scheduling --- ### 2.2 Missed Notification Detection **Status**: ⚠️ **PARTIAL** **Location**: `ios/Plugin/DailyNotificationPlugin.swift` **Findings**: - `checkForMissedBGTask()` method exists (line 421) - Checks for missed background tasks (BGTaskScheduler) - Reschedules missed BGTask if needed **Gap Identified**: - Only checks for missed BGTask, not missed notifications - UNUserNotificationCenter handles notification persistence, but plugin doesn't check for missed notifications - No comparison between plugin storage and UNUserNotificationCenter pending notifications **Recommendation**: Add missed notification detection by comparing plugin storage with UNUserNotificationCenter pending requests --- ### 2.3 App Launch Recovery **Status**: ⚠️ **PARTIAL** **Location**: `ios/Plugin/DailyNotificationPlugin.swift` **Expected Behavior** (iOS Missed Notification Recovery Architecture): **Required Steps for Missed Notification Detection**: 1. Query plugin storage (CoreData) for all scheduled notifications 2. Query `UNUserNotificationCenter.pendingNotificationRequests()` for future notifications 3. Query `UNUserNotificationCenter.getDeliveredNotifications()` for already-fired notifications 4. Find CoreData entries where: - `scheduled_time < now` (should have fired) - NOT in `deliveredNotifications` list (didn't fire) - NOT in `pendingNotificationRequests` list (not scheduled for future) 5. Generate "missed notification" events for each detected miss 6. Reschedule repeating notifications 7. Verify that scheduled notifications in UNUserNotificationCenter align with CoreData schedules **This must be placed in `load()` during cold start.** **Current Implementation**: - `load()` method exists (line 42) - `setupBackgroundTasks()` called (line 318) - `checkForMissedBGTask()` called on setup (line 330) - Only checks for missed BGTask, not missed notifications - No recovery of notification state - No rescheduling of notifications from plugin storage - No comparison between UNUserNotificationCenter and CoreData **Gap Identified**: - Only checks for missed BGTask, not missed notifications - No recovery of notification state - No rescheduling of notifications from plugin storage - No cross-checking between UNUserNotificationCenter and CoreData - **iOS cannot detect missed notifications** unless plugin compares storage vs `UNUserNotificationCenter.getDeliveredNotifications()` or infers from plugin timestamps **Recommendation**: 1. Add notification recovery logic in `load()` or `setupBackgroundTasks()` 2. Implement three-way comparison: CoreData vs pending vs delivered notifications 3. Add missed notification detection using the architecture above 4. Note: iOS does NOT allow arbitrary code execution at notification fire time unless user interacts or Notification Service Extensions are used (not currently used) --- ### 2.4 Background Execution Limits **Status**: ✅ **DOCUMENTED IN CODE** **Findings**: - BGTaskScheduler used for background fetch - Time budget limitations understood (30 seconds typical) - System-controlled execution acknowledged - Rescheduling logic handles missed tasks **Code Evidence**: - `checkForMissedBGTask()` handles missed BGTask (line 421) - 15-minute miss window used (line 448) - Reschedules if missed (line 462) --- ## 3. Cross-Platform Gaps Summary | Gap | Android | iOS | Severity | Recommendation | | --- | ------- | --- | -------- | -------------- | | Missed alarm/notification detection | ⚠️ Partial | ⚠️ Partial | **High** | Implement on app launch | | App launch recovery | ❌ Missing | ⚠️ Partial | **High** | **MUST implement for both platforms** | | Force stop recovery | ❌ Missing | N/A | **Medium** | Android: Implement app launch recovery with force stop detection | | Boot recovery missed alarms | ⚠️ Only future | N/A | **Medium** | Android: Add missed alarm handling in boot receiver | | Cross-check mechanism (DB vs OS) | ❌ Missing | ⚠️ Partial | **High** | Android: AlarmManager vs DB; iOS: UNUserNotificationCenter vs CoreData | **Critical Requirement**: App Launch Recovery **must be implemented on BOTH platforms**: - Plugin must execute recovery logic during `load()` OR equivalent - Distinguish cold vs warm start - Use timestamps in storage to verify last known state - Reconcile DB entries with OS scheduling APIs - Android: Cross-check AlarmManager scheduled alarms with DB - iOS: Cross-check UNUserNotificationCenter with CoreData schedules --- ## 4. Test Validation Outputs For each scenario, the exploration should produce explicit outputs: | Scenario | OS Expected | Plugin Expected | Observed Result | Pass/Fail | Notes | | -------- | ----------- | --------------- | --------------- | --------- | ----- | | Swipe from recents | Alarm fires | Alarm fires | ☐ | ☐ | | | Force stop | Alarm does NOT fire | Plugin detects on launch | ☐ | ☐ | | | Device reboot (Android) | Alarm does NOT fire | Plugin reschedules on boot | ☐ | ☐ | | | Device reboot (iOS) | Notification fires | Notification fires | ☐ | ☐ | | | Cold start | N/A | Missed alarms detected | ☐ | ☐ | | | Warm start | N/A | Missed alarms detected | ☐ | ☐ | | | Force stop recovery | N/A | All alarms recovered | ☐ | ☐ | | **This creates alignment with the [Exploration Template](./plugin-behavior-exploration-template.md).** --- ## 5. Next Steps 1. **Verify findings through testing** using [Plugin Behavior Exploration Template](./plugin-behavior-exploration-template.md) 2. **Test boot receiver** on actual device reboot 3. **Test app launch recovery** on cold/warm start 4. **Test force stop recovery** on Android (with cross-check mechanism) 5. **Test missed notification detection** on iOS (with three-way comparison) 6. **Inspect `UNUserNotificationCenter.getPendingNotificationRequests()` vs CoreData** to detect "lost" iOS notifications 7. **Update Plugin Requirements** document with verified gaps 8. **Generate Test Validation Outputs** table with actual test results --- ## 6. Code References for Implementation ### Android - Add Missed Alarm Detection with Force Stop Detection **Location**: `DailyNotificationPlugin.kt` - `load()` method (line 91) **Suggested Implementation** (with Force Stop Detection): ```kotlin override fun load() { super.load() // ... existing initialization ... // Check for missed alarms on app launch CoroutineScope(Dispatchers.IO).launch { detectAndHandleMissedAlarms() } } private suspend fun detectAndHandleMissedAlarms() { val db = DailyNotificationDatabase.getDatabase(context) val currentTime = System.currentTimeMillis() // Cross-check: Query AlarmManager for active alarms val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager val activeAlarmCount = getActiveAlarmCount(alarmManager) // Helper method needed // Query database for all enabled schedules val dbSchedules = db.scheduleDao().getEnabled() // Force Stop Detection: If DB has schedules but AlarmManager has zero val forceStopDetected = dbSchedules.isNotEmpty() && activeAlarmCount == 0 if (forceStopDetected) { Log.i(TAG, "Force stop detected - all alarms were cancelled") // Recover ALL alarms (not just missed ones) recoverAllAlarmsAfterForceStop(db, dbSchedules, currentTime) } else { // Normal recovery: only check for missed alarms val missedNotifications = db.notificationContentDao() .getNotificationsReadyForDelivery(currentTime) missedNotifications.forEach { notification -> // Generate missed alarm event/notification // Reschedule if repeating // Update delivery status } // Reschedule future alarms from database rescheduleFutureAlarms(db, dbSchedules, currentTime) } } private suspend fun recoverAllAlarmsAfterForceStop( db: DailyNotificationDatabase, schedules: List, currentTime: Long ) { schedules.forEach { schedule -> val nextRunTime = calculateNextRunTime(schedule) if (nextRunTime < currentTime) { // Past alarm - mark as missed // Generate missed alarm notification // Reschedule if repeating } else { // Future alarm - reschedule immediately rescheduleAlarm(schedule, nextRunTime) } } } ``` ### Android - Add Missed Alarm Handling in Boot Receiver **Location**: `BootReceiver.kt` - `rescheduleNotifications()` method (line 38) **Suggested Implementation**: ```kotlin // After rescheduling future alarms, check for missed ones val missedNotifications = db.notificationContentDao() .getNotificationsReadyForDelivery(System.currentTimeMillis()) missedNotifications.forEach { notification -> // Generate missed alarm notification // Reschedule if repeating } ``` ### iOS - Add Missed Notification Detection **Location**: `DailyNotificationPlugin.swift` - `setupBackgroundTasks()` or `load()` method **Suggested Implementation** (Three-Way Comparison): ```swift private func checkForMissedNotifications() async { // Step 1: Get pending notifications (future) from UNUserNotificationCenter let pendingRequests = await notificationCenter.pendingNotificationRequests() let pendingIds = Set(pendingRequests.map { $0.identifier }) // Step 2: Get delivered notifications (already fired) from UNUserNotificationCenter let deliveredNotifications = await notificationCenter.getDeliveredNotifications() let deliveredIds = Set(deliveredNotifications.map { $0.request.identifier }) // Step 3: Get notifications from plugin storage (CoreData) let storedNotifications = storage?.getAllNotifications() ?? [] let currentTime = Date().timeIntervalSince1970 // Step 4: Find missed notifications // Missed = scheduled_time < now AND not in delivered AND not in pending for notification in storedNotifications { let scheduledTime = notification.scheduledTime let notificationId = notification.id if scheduledTime < currentTime { // Should have fired by now if !deliveredIds.contains(notificationId) && !pendingIds.contains(notificationId) { // This notification was missed // Generate missed notification event // Reschedule if repeating // Update delivery status } } } // Step 5: Verify alignment - check if CoreData schedules match UNUserNotificationCenter // Reschedule any missing notifications from CoreData } ``` --- ## Related Documentation - [Plugin Behavior Exploration Template](./plugin-behavior-exploration-template.md) - Use this for testing - [Plugin Requirements & Implementation](./plugin-requirements-implementation.md) - Requirements based on findings - [Platform Capability Reference](./platform-capability-reference.md) - OS-level facts --- ## 7. Document Separation Directive After improvements are complete, separate documents by purpose: - **This file** → Exploration Findings (final) - Code inspection and test results - **Android Behavior** → Platform Reference (see [Platform Capability Reference](./platform-capability-reference.md)) - **iOS Behavior** → Platform Reference (see [Platform Capability Reference](./platform-capability-reference.md)) - **Plugin Requirements** → Independent document (see [Plugin Requirements & Implementation](./plugin-requirements-implementation.md)) - **Future Implementation Directive** → Separate document (to be created) This avoids future redundancy and maintains clear separation of concerns. --- ## Notes - These findings are from **code inspection only** - **Actual testing required** to verify behavior - Findings should be updated after testing with [Plugin Behavior Exploration Template](./plugin-behavior-exploration-template.md) - iOS missed notification detection requires three-way comparison: CoreData vs pending vs delivered - Android force stop detection requires cross-check: AlarmManager vs database