# DIRECTIVE: Explore & Document Alarm / Schedule / Notification Behavior in Capacitor Plugin (Android & iOS) **Author**: Matthew Raymer **Date**: November 2025 **Status**: Active Directive - Exploration Phase ## 0. Scope & Objective We want to **explore, test, and document** how the *current* Capacitor plugin handles: - **Alarms / schedules / reminders** - **Local notifications** - **Persistence and recovery** across: - App kill / swipe from recents - OS process kill - Device reboot - **Force stop** (Android) / hard termination (iOS) - Cross-platform **semantic differences** between Android and iOS The focus is **observation of current behavior**, not yet changing implementation. We want a clear map of **what the plugin actually guarantees** on each platform. --- ## 1. Key Questions to Answer For **each platform (Android, iOS)** and for **each "scheduled thing"** the plugin supports (alarms, reminders, scheduled notifications, repeating schedules, etc.): ### 1.1 How is it implemented under the hood? - **Android**: AlarmManager? WorkManager? JobScheduler? Foreground service? - **iOS**: UNUserNotificationCenter? BGTaskScheduler? background fetch? timers in foreground? ### 1.2 What happens when the app is: - Swiped away from recents? - Killed by OS (memory pressure)? - Device rebooted? - On Android: explicitly **Force Stopped** in system settings? - On iOS: explicitly swiped away, then device rebooted before next trigger? ### 1.3 What is persisted? Are schedules/alarms stored in: - SQLite / Room / shared preferences (Android)? - CoreData / UserDefaults / files (iOS)? - Or are they only in RAM / native scheduler? ### 1.4 What is re-created and when? - On boot? - On app cold start? - On notification tap? - Not at all? ### 1.5 What does the plugin *promise* to the JS/TS layer? - "Will always fire even after reboot"? - "Will fire as long as app hasn't been force-stopped"? - "Best-effort only"? We are trying to align **plugin promises** with **real platform capabilities and limitations.** --- ## 2. Android Exploration ### 2.1 Code-Level Inspection **Source Locations:** - **Plugin Implementation**: `android/src/main/java/com/timesafari/dailynotification/` (Kotlin files may also be present) - **Manifest**: `android/src/main/AndroidManifest.xml` - **Test Applications**: - `test-apps/android-test-app/` - Primary Android test app - `test-apps/daily-notification-test/` - Additional test application **Tasks:** 1. **Locate the Android implementation in the plugin:** - Primary path: `android/src/main/java/com/timesafari/dailynotification/` - Key files to examine: - `DailyNotificationPlugin.kt` - Main plugin class (see `scheduleDailyNotification()` at line 1302) - `DailyNotificationWorker.java` - WorkManager worker (see `doWork()` at line 59) - `DailyNotificationReceiver.java` - BroadcastReceiver for alarms (see `onReceive()` at line 51) - `NotifyReceiver.kt` - AlarmManager scheduling (see `scheduleExactNotification()` at line 92) - `BootReceiver.kt` - Boot recovery (see `onReceive()` at line 24) - `FetchWorker.kt` - WorkManager fetch scheduling (see `scheduleFetch()` at line 31) 2. **Identify the mechanisms used to schedule work:** - **AlarmManager**: Used via `NotifyReceiver.scheduleExactNotification()` (line 92) - `setAlarmClock()` for Android 5.0+ (line 219) - `setExactAndAllowWhileIdle()` for Android 6.0+ (line 223) - `setExact()` for older versions (line 231) - **WorkManager**: Used for background fetching and notification processing - `FetchWorker.scheduleFetch()` (line 31) - Uses `OneTimeWorkRequest` - `DailyNotificationWorker.doWork()` (line 59) - Processes notifications - **No JobScheduler**: Not used in current implementation - **No repeating alarms**: Uses one-time alarms with rescheduling 3. **Inspect how notifications are issued:** - **NotificationCompat**: Used in `DailyNotificationWorker.displayNotification()` and `NotifyReceiver.showNotification()` (line 482) - **setFullScreenIntent**: Not currently used (check `NotifyReceiver.showNotification()` at line 443) - **Notification channels**: Created in `NotifyReceiver.showNotification()` (lines 454-470) - Channel ID: `"timesafari.daily"` (see `DailyNotificationWorker.java` line 46) - Importance based on priority (HIGH/DEFAULT/LOW) - **ChannelManager**: Check for separate channel management class 4. **Check for permissions & receivers:** - Manifest: `android/src/main/AndroidManifest.xml` - Look for: - `RECEIVE_BOOT_COMPLETED` permission - `SCHEDULE_EXACT_ALARM` permission - Any `BroadcastReceiver` declarations for: - `BOOT_COMPLETED` / `LOCKED_BOOT_COMPLETED` - Custom alarm intent actions - Check test app manifests: - `test-apps/android-test-app/app/src/main/AndroidManifest.xml` - `test-apps/daily-notification-test/` (if applicable) 5. **Determine persistence strategy:** - Where are scheduled alarms stored? - SharedPreferences? - SQLite / Room? - Not at all (just in AlarmManager/work queue)? 6. **Check for reschedule-on-boot or reschedule-on-app-launch logic:** - **BootReceiver**: `android/src/main/java/com/timesafari/dailynotification/BootReceiver.kt` - `onReceive()` handles `ACTION_BOOT_COMPLETED` (line 24) - `rescheduleNotifications()` reloads from database and reschedules (line 38+) - Calls `NotifyReceiver.scheduleExactNotification()` for each schedule (line 74) - **Alternative**: `DailyNotificationRebootRecoveryManager.java` has `BootCompletedReceiver` inner class (line 278) - **App launch recovery**: Check `DailyNotificationPlugin.kt` for initialization logic that reschedules on app start --- ### 2.2 Behavior Testing Matrix (Android) **Test Applications:** - **Primary**: `test-apps/android-test-app/` - Use this for comprehensive testing - **Secondary**: `test-apps/daily-notification-test/` - Additional test scenarios if needed **Run these tests on a real device or emulator.** For each scenario, record: - Did the alarm / notification fire? - Did it fire on time? - From what state did the app wake up (cold, warm, already running)? - Any visible logs / errors? **Scenarios:** #### 2.2.1 Base Case - Schedule an alarm/notification 2 minutes in the future. - Leave app in foreground or background. - Confirm it fires. #### 2.2.2 Swipe from Recents - Schedule alarm (2–5 minutes). - Swipe app away from recents. - Wait for trigger time. - Observe: does alarm still fire? #### 2.2.3 OS Kill (simulate memory pressure) - Mainly observational; may be tricky to force, but: - Open many other apps or use `adb shell am kill ` (not force-stop). - Confirm whether scheduled alarm still fires. #### 2.2.4 Device Reboot - Schedule alarm (e.g. 10 minutes in the future). - Reboot device. - Do **not** reopen app. - Wait past scheduled time: - Does plugin reschedule and fire automatically? - Or does nothing happen until user opens the app? Then: - After device reboot, manually open the app. - Does plugin detect missed alarms and: - Fire "missed" behavior? - Reschedule future alarms? - Or silently forget them? #### 2.2.5 Android Force Stop - Schedule alarm. - Go to Settings → Apps → [Your App] → Force stop. - Wait for trigger time. - Observe: it should **not** fire (OS-level rule). - Then open app again and see if plugin automatically: - Detects missed alarms and recovers, or - Treats them as lost. **Goal:** build a clear empirical table of plugin behavior vs Android's known rules. --- ## 3. iOS Exploration ### 3.1 Code-Level Inspection **Source Locations:** - **Plugin Implementation**: `ios/Plugin/` - Swift plugin files - **Alternative Branch**: Check `ios-2` branch for additional iOS implementations or variations - **Test Applications**: - `test-apps/ios-test-app/` - Primary iOS test app - `test-apps/daily-notification-test/` - Additional test application (if iOS support exists) **Tasks:** 1. **Locate the iOS implementation:** - Primary path: `ios/Plugin/` - Key files to examine: - `DailyNotificationPlugin.swift` - Main plugin class (see `scheduleUserNotification()` at line 506) - `DailyNotificationScheduler.swift` - Notification scheduling (see `scheduleNotification()` at line 133) - `DailyNotificationBackgroundTasks.swift` - BGTaskScheduler handlers - **Also check**: `ios-2` branch for alternative implementations or newer iOS code ```bash git checkout ios-2 # Compare ios/Plugin/DailyNotificationPlugin.swift ``` 2. **Identify the scheduling mechanism:** - **UNUserNotificationCenter**: Primary mechanism - `DailyNotificationScheduler.scheduleNotification()` uses `UNCalendarNotificationTrigger` (line 172) - `DailyNotificationPlugin.scheduleUserNotification()` uses `UNTimeIntervalNotificationTrigger` (line 514) - No `UNLocationNotificationTrigger` found - **BGTaskScheduler**: Used for background fetch - `DailyNotificationPlugin.scheduleBackgroundFetch()` (line 495) - Uses `BGAppRefreshTaskRequest` (line 496) - **No timers**: No plain Timer usage found (would die with app) 3. **Determine what's persisted:** - Does the plugin store alarms in: - `UserDefaults`? - Files / CoreData? - Or only within UNUserNotificationCenter's pending notification requests (no parallel app-side storage)? 4. **Check for re-scheduling behavior on app launch:** - On app start (cold or warm), does plugin: - Query `UNUserNotificationCenter` for pending notifications? - Compare against its own store? - Attempt to rebuild schedules? - Or does it rely solely on UNUserNotificationCenter to manage everything? 5. **Determine capabilities / limitations:** - Can the plugin run arbitrary code *when the notification fires*? - Only via notification actions / `didReceive response` callbacks. - Does it support repeating notifications (daily/weekly)? --- ### 3.2 Behavior Testing Matrix (iOS) **Test Applications:** - **Primary**: `test-apps/ios-test-app/` - Use this for comprehensive testing - **Secondary**: `test-apps/daily-notification-test/` - Additional test scenarios if needed - **Note**: Compare behavior between main branch and `ios-2` branch implementations if they differ As with Android, test: #### 3.2.1 Base Case - Schedule local notification 2–5 minutes in the future; leave app backgrounded. - Confirm it fires on time with app in background. #### 3.2.2 Swipe App Away - Schedule notification, then swipe app away from app switcher. - Confirm notification still fires (iOS local notification center should handle this). #### 3.2.3 Device Reboot - Schedule notification for a future time. - Reboot device. - Do **not** open app. - Test whether: - Notification still fires (iOS usually persists scheduled local notifications across reboot), or - Behavior depends on trigger type (time vs calendar, etc.). #### 3.2.4 Hard Termination & Relaunch - Schedule some repeating notification(s). - Terminate app via Xcode / app switcher. - Allow some triggers to occur. - Reopen app and inspect whether plugin: - Notices anything about missed events, or - Simply trusts that UNUserNotificationCenter handled user-visible parts. **Goal:** map what your *plugin* adds on top of native behavior vs what is entirely delegated to the OS. --- ## 4. Cross-Platform Behavior & Promise Alignment After exploration, produce a summary: ### 4.1 What the plugin actually guarantees to JS callers - "Scheduled reminders will still fire after app swipe / kill" - "On Android, reminders may not survive device reboot unless app is opened" - "After Force Stop (Android), nothing runs until user opens app" - "On iOS, local notifications themselves persist across reboot, but no extra app code runs at fire time unless user interacts" ### 4.2 Where semantics differ - Android may require explicit rescheduling on boot; iOS may not. - Android **force stop** is a hard wall; iOS has no exact equivalent in user-facing settings. - Plugin may currently: - Over-promise on reliability, or - Under-document platform limitations. ### 4.3 Where we need to add warnings / notes in the public API - E.g. "This schedule is best-effort; on Android, device reboot may cancel it unless you open the app again," etc. --- ## 5. Deliverables from This Exploration ### 5.1 Doc: `ALARMS_BEHAVIOR_MATRIX.md` A table of scenarios (per platform) vs observed behavior. Includes: - App state - OS event (reboot, force stop, etc.) - What fired, what didn't - Log snippets where useful ### 5.2 Doc: `PLUGIN_ALARM_LIMITATIONS.md` Plain-language explanation of: - Android hard limits (Force Stop, reboot behavior) - iOS behavior (local notifications vs app code execution) - Clear note on what the plugin promises. ### 5.3 Annotated code pointers Commented locations in Android/iOS code where: - Scheduling is performed - Persistence (if any) is implemented - Rescheduling (if any) is implemented ### 5.4 Open Questions / TODOs Gaps uncovered: - No reschedule-on-boot? - No persistence of schedules? - No handling of "missed" alarms on reactivation? - Potential next-step directives (separate document) to improve behavior. --- ## 6. One-Liner Summary > This directive is to **investigate, not change**: we want a precise, tested understanding of what our Capacitor plugin *currently* does with alarms/schedules/notifications on Android and iOS, especially across kills, reboots, and force stops, and where that behavior does or does not match what we think we're promising to app developers. --- ## Related Documentation - [Android Alarm Persistence Directive](./android-alarm-persistence-directive.md) - General Android alarm capabilities and limitations - [Boot Receiver Testing Guide](./boot-receiver-testing-guide.md) - Testing boot receiver behavior - [App Startup Recovery Solution](./app-startup-recovery-solution.md) - Recovery mechanisms on app launch - [Reboot Testing Procedure](./reboot-testing-procedure.md) - Step-by-step reboot testing --- ## Source Code Structure Reference ### Android Source Files **Primary Plugin Code:** - `android/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.kt` (or `.java`) - `android/src/main/java/com/timesafari/dailynotification/DailyNotificationWorker.java` - `android/src/main/java/com/timesafari/dailynotification/DailyNotificationReceiver.java` - `android/src/main/java/com/timesafari/dailynotification/ChannelManager.java` - `android/src/main/AndroidManifest.xml` **Test Applications:** - `test-apps/android-test-app/app/src/main/` - Test app source - `test-apps/android-test-app/app/src/main/assets/public/index.html` - Test UI - `test-apps/daily-notification-test/` - Additional test app (if present) ### iOS Source Files **Primary Plugin Code:** - `ios/Plugin/DailyNotificationPlugin.swift` (or similar) - `ios/Plugin/` - All Swift plugin files - **Also check**: `ios-2` branch for alternative implementations **Test Applications:** - `test-apps/ios-test-app/` - Test app source - `test-apps/daily-notification-test/` - Additional test app (if iOS support exists) --- ## Detailed Code References (File Locations, Functions, Line Numbers) ### Android Implementation Details #### Alarm Scheduling **File**: `android/src/main/java/com/timesafari/dailynotification/NotifyReceiver.kt` - **Function**: `scheduleExactNotification()` - **Lines 92-247** - Schedules exact alarms using AlarmManager - Uses `setAlarmClock()` for Android 5.0+ (API 21+) - **Line 219** - Falls back to `setExactAndAllowWhileIdle()` for Android 6.0+ (API 23+) - **Line 223** - Falls back to `setExact()` for older versions - **Line 231** - Called from: - `DailyNotificationPlugin.kt` - `scheduleDailyNotification()` - **Line 1385** - `DailyNotificationPlugin.kt` - `scheduleDailyReminder()` - **Line 809** - `DailyNotificationPlugin.kt` - `scheduleDualNotification()` - **Line 1685** - `BootReceiver.kt` - `rescheduleNotifications()` - **Line 74** **File**: `android/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.kt` - **Function**: `scheduleDailyNotification()` - **Lines 1302-1417** - Main plugin method for scheduling notifications - Checks exact alarm permission - **Line 1309** - Opens settings if permission not granted - **Lines 1314-1324** - Calls `NotifyReceiver.scheduleExactNotification()` - **Line 1385** - Schedules prefetch 2 minutes before notification - **Line 1395** - **Function**: `scheduleDailyReminder()` - **Lines 777-833** - Schedules static reminders (no content dependency) - Calls `NotifyReceiver.scheduleExactNotification()` - **Line 809** - **Function**: `canScheduleExactAlarms()` - **Lines 835-860** - Checks if exact alarm permission is granted (Android 12+) **File**: `android/src/main/java/com/timesafari/dailynotification/PendingIntentManager.java` - **Function**: `scheduleExactAlarm()` - **Lines 127-158** - Uses `setExactAndAllowWhileIdle()` - **Line 135** - Falls back to `setExact()` - **Line 141** **File**: `android/src/main/java/com/timesafari/dailynotification/DailyNotificationExactAlarmManager.java` - **Function**: `scheduleExactAlarm()` - **Lines 186-201** - Uses `setExactAndAllowWhileIdle()` - **Line 189** - Falls back to `setExact()` - **Line 193** **File**: `android/src/main/java/com/timesafari/dailynotification/DailyNotificationScheduler.java` - **Function**: `scheduleExactAlarm()` - **Lines 237-272** - Uses `setExactAndAllowWhileIdle()` - **Line 242** - Falls back to `setExact()` - **Line 251** #### WorkManager Usage **File**: `android/src/main/java/com/timesafari/dailynotification/FetchWorker.kt` - **Function**: `scheduleFetch()` - **Lines 31-59** - Schedules WorkManager one-time work request - Uses `OneTimeWorkRequestBuilder` - **Line 36** - Enqueues with `WorkManager.getInstance().enqueueUniqueWork()` - **Lines 53-58** - **Function**: `scheduleDelayedPrefetch()` - **Lines 62-131** - Schedules delayed prefetch work - Uses `setInitialDelay()` - **Line 104** **File**: `android/src/main/java/com/timesafari/dailynotification/DailyNotificationWorker.java` - **Function**: `doWork()` - **Lines 59-915** - Main WorkManager worker execution - Handles notification display - **Line 91** - Calls `displayNotification()` - **Line 200+** - **Function**: `displayNotification()` - **Lines 200-400+** - Displays notification using NotificationCompat - Ensures notification channel exists - Uses `NotificationCompat.Builder` - **Line 200+** **File**: `android/src/main/java/com/timesafari/dailynotification/DailyNotificationFetcher.java` - **Function**: `scheduleFetch()` - **Lines 78-140** - Schedules WorkManager fetch work - Uses `OneTimeWorkRequest.Builder` - **Line 106** - Enqueues with `workManager.enqueueUniqueWork()` - **Lines 115-119** #### Boot Recovery **File**: `android/src/main/java/com/timesafari/dailynotification/BootReceiver.kt` - **Class**: `BootReceiver` - **Lines 18-100+** - BroadcastReceiver for BOOT_COMPLETED - `onReceive()` - **Line 24** - Handles boot intent - `rescheduleNotifications()` - **Line 38+** - Reschedules all notifications from database - Calls `NotifyReceiver.scheduleExactNotification()` - **Line 74** **File**: `android/src/main/java/com/timesafari/dailynotification/DailyNotificationRebootRecoveryManager.java` - **Class**: `BootCompletedReceiver` - **Lines 278-297** - Inner BroadcastReceiver for boot events - `onReceive()` - **Line 280** - Handles BOOT_COMPLETED action - Calls `handleSystemReboot()` - **Line 290** #### Notification Display **File**: `android/src/main/java/com/timesafari/dailynotification/DailyNotificationReceiver.java` - **Function**: `onReceive()` - **Lines 51-485** - Lightweight BroadcastReceiver triggered by AlarmManager - Enqueues WorkManager work for heavy operations - **Line 100+** - Extracts notification ID and action from intent **File**: `android/src/main/java/com/timesafari/dailynotification/NotifyReceiver.kt` - **Function**: `showNotification()` - **Lines 443-500** - Displays notification using NotificationCompat - Creates notification channel if needed - **Lines 454-470** - Uses `NotificationCompat.Builder` - **Line 482** #### Persistence **File**: `android/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.kt` - Database operations use Room database: - `getDatabase()` - Returns DailyNotificationDatabase instance - Schedule storage in `scheduleDailyNotification()` - **Lines 1393-1410** - Schedule storage in `scheduleDailyReminder()` - **Lines 813-821** #### Permissions & Manifest **File**: `android/src/main/AndroidManifest.xml` - **Note**: Plugin manifest is minimal; receivers declared in consuming app manifest - Check test app manifest: `test-apps/android-test-app/app/src/main/AndroidManifest.xml` - Look for `RECEIVE_BOOT_COMPLETED` permission - Look for `SCHEDULE_EXACT_ALARM` permission - Look for `BootReceiver` registration - Look for `DailyNotificationReceiver` registration ### iOS Implementation Details #### Notification Scheduling **File**: `ios/Plugin/DailyNotificationPlugin.swift` - **Class**: `DailyNotificationPlugin` - **Lines 24-532** - Main Capacitor plugin class - Uses `UNUserNotificationCenter.current()` - **Line 26** - Uses `BGTaskScheduler.shared` - **Line 27** - **Function**: `scheduleUserNotification()` - **Lines 506-529** - Schedules notification using UNUserNotificationCenter - Creates `UNMutableNotificationContent` - **Line 507** - Creates `UNTimeIntervalNotificationTrigger` - **Line 514** - Adds request via `notificationCenter.add()` - **Line 522** - **Function**: `scheduleBackgroundFetch()` - **Lines 495-504** - Schedules BGTaskScheduler background fetch - Creates `BGAppRefreshTaskRequest` - **Line 496** - Submits via `backgroundTaskScheduler.submit()` - **Line 502** **File**: `ios/Plugin/DailyNotificationScheduler.swift` - **Class**: `DailyNotificationScheduler` - **Lines 20-236+** - Manages UNUserNotificationCenter scheduling - **Function**: `scheduleNotification()` - **Lines 133-198** - Schedules notification with calendar trigger - Creates `UNCalendarNotificationTrigger` - **Lines 172-175** - Creates `UNNotificationRequest` - **Lines 178-182** - Adds via `notificationCenter.add()` - **Line 185** - **Function**: `cancelNotification()` - **Lines 205-213** - Cancels notification by ID - Uses `notificationCenter.removePendingNotificationRequests()` - **Line 206** #### Background Tasks **File**: `ios/Plugin/DailyNotificationBackgroundTasks.swift` - Background task handling for BGTaskScheduler - Register background task identifiers - Handle background fetch execution #### Persistence **File**: `ios/Plugin/DailyNotificationPlugin.swift** - **Note**: Check for UserDefaults, CoreData, or file-based storage - Storage component: `var storage: DailyNotificationStorage?` - **Line 35** - Scheduler component: `var scheduler: DailyNotificationScheduler?` - **Line 36** #### iOS-2 Branch - **Note**: Check `ios-2` branch for alternative implementations: ```bash git checkout ios-2 # Compare ios/Plugin/ implementations ``` --- ## Testing Tools & Commands ### Android Testing ```bash # Check scheduled alarms adb shell dumpsys alarm | grep -i timesafari # Force kill (not force-stop) - adjust package name based on test app adb shell am kill com.timesafari.dailynotification # Or for test apps: # adb shell am kill com.timesafari.androidtestapp # adb shell am kill # View logs adb logcat | grep -i "DN\|DailyNotification" # Check WorkManager tasks adb shell dumpsys jobscheduler | grep -i timesafari # Build and install test app cd test-apps/android-test-app ./gradlew installDebug ``` ### iOS Testing ```bash # View device logs (requires Xcode) xcrun simctl spawn booted log stream --predicate 'processImagePath contains "DailyNotification"' # List pending notifications (requires app code) # Use UNUserNotificationCenter.getPendingNotificationRequests() # Build test app (from test-apps/ios-test-app) # Use Xcode or: cd test-apps/ios-test-app # Follow build instructions in test app README # Check ios-2 branch for alternative implementations git checkout ios-2 # Compare ios/Plugin/ implementations between branches ``` --- ## Next Steps After Exploration Once this exploration is complete: 1. **Document findings** in the deliverables listed above 2. **Identify gaps** between current behavior and desired behavior 3. **Create implementation directives** to address gaps (if needed) 4. **Update plugin documentation** to accurately reflect platform limitations 5. **Update API documentation** with appropriate warnings and caveats