- Add platform capability reference (Android & iOS OS-level facts) - Add plugin behavior exploration template (executable test matrices) - Add plugin requirements & implementation directive - Add Android-specific implementation directive with detailed test procedures - Add exploration findings from code inspection - Add improvement directive for refining documentation structure - Add Android alarm persistence directive (OS capabilities) All documents include: - File locations, function references, and line numbers - Detailed test procedures with ADB commands - Cross-platform comparisons - Implementation checklists and code examples
24 KiB
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 apptest-apps/daily-notification-test/- Additional test application
Tasks:
-
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 (seescheduleDailyNotification()at line 1302)DailyNotificationWorker.java- WorkManager worker (seedoWork()at line 59)DailyNotificationReceiver.java- BroadcastReceiver for alarms (seeonReceive()at line 51)NotifyReceiver.kt- AlarmManager scheduling (seescheduleExactNotification()at line 92)BootReceiver.kt- Boot recovery (seeonReceive()at line 24)FetchWorker.kt- WorkManager fetch scheduling (seescheduleFetch()at line 31)
- Primary path:
-
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) - UsesOneTimeWorkRequestDailyNotificationWorker.doWork()(line 59) - Processes notifications
- No JobScheduler: Not used in current implementation
- No repeating alarms: Uses one-time alarms with rescheduling
- AlarmManager: Used via
-
Inspect how notifications are issued:
- NotificationCompat: Used in
DailyNotificationWorker.displayNotification()andNotifyReceiver.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"(seeDailyNotificationWorker.javaline 46) - Importance based on priority (HIGH/DEFAULT/LOW)
- Channel ID:
- ChannelManager: Check for separate channel management class
- NotificationCompat: Used in
-
Check for permissions & receivers:
- Manifest:
android/src/main/AndroidManifest.xml - Look for:
RECEIVE_BOOT_COMPLETEDpermissionSCHEDULE_EXACT_ALARMpermission
- Any
BroadcastReceiverdeclarations for:BOOT_COMPLETED/LOCKED_BOOT_COMPLETED- Custom alarm intent actions
- Check test app manifests:
test-apps/android-test-app/app/src/main/AndroidManifest.xmltest-apps/daily-notification-test/(if applicable)
- Manifest:
-
Determine persistence strategy:
- Where are scheduled alarms stored?
- SharedPreferences?
- SQLite / Room?
- Not at all (just in AlarmManager/work queue)?
- Where are scheduled alarms stored?
-
Check for reschedule-on-boot or reschedule-on-app-launch logic:
- BootReceiver:
android/src/main/java/com/timesafari/dailynotification/BootReceiver.ktonReceive()handlesACTION_BOOT_COMPLETED(line 24)rescheduleNotifications()reloads from database and reschedules (line 38+)- Calls
NotifyReceiver.scheduleExactNotification()for each schedule (line 74)
- Alternative:
DailyNotificationRebootRecoveryManager.javahasBootCompletedReceiverinner class (line 278) - App launch recovery: Check
DailyNotificationPlugin.ktfor initialization logic that reschedules on app start
- BootReceiver:
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 <package>(not force-stop). - Confirm whether scheduled alarm still fires.
- Open many other apps or use
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-2branch for additional iOS implementations or variations - Test Applications:
test-apps/ios-test-app/- Primary iOS test apptest-apps/daily-notification-test/- Additional test application (if iOS support exists)
Tasks:
-
Locate the iOS implementation:
- Primary path:
ios/Plugin/ - Key files to examine:
DailyNotificationPlugin.swift- Main plugin class (seescheduleUserNotification()at line 506)DailyNotificationScheduler.swift- Notification scheduling (seescheduleNotification()at line 133)DailyNotificationBackgroundTasks.swift- BGTaskScheduler handlers
- Also check:
ios-2branch for alternative implementations or newer iOS codegit checkout ios-2 # Compare ios/Plugin/DailyNotificationPlugin.swift
- Primary path:
-
Identify the scheduling mechanism:
- UNUserNotificationCenter: Primary mechanism
DailyNotificationScheduler.scheduleNotification()usesUNCalendarNotificationTrigger(line 172)DailyNotificationPlugin.scheduleUserNotification()usesUNTimeIntervalNotificationTrigger(line 514)- No
UNLocationNotificationTriggerfound
- BGTaskScheduler: Used for background fetch
DailyNotificationPlugin.scheduleBackgroundFetch()(line 495)- Uses
BGAppRefreshTaskRequest(line 496)
- No timers: No plain Timer usage found (would die with app)
- UNUserNotificationCenter: Primary mechanism
-
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)?
- Does the plugin store alarms in:
-
Check for re-scheduling behavior on app launch:
- On app start (cold or warm), does plugin:
- Query
UNUserNotificationCenterfor pending notifications? - Compare against its own store?
- Attempt to rebuild schedules?
- Query
- Or does it rely solely on UNUserNotificationCenter to manage everything?
- On app start (cold or warm), does plugin:
-
Determine capabilities / limitations:
- Can the plugin run arbitrary code when the notification fires?
- Only via notification actions /
didReceive responsecallbacks.
- Only via notification actions /
- Does it support repeating notifications (daily/weekly)?
- Can the plugin run arbitrary code when the notification fires?
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-2branch 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 - General Android alarm capabilities and limitations
- Boot Receiver Testing Guide - Testing boot receiver behavior
- App Startup Recovery Solution - Recovery mechanisms on app launch
- Reboot Testing Procedure - 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.javaandroid/src/main/java/com/timesafari/dailynotification/DailyNotificationReceiver.javaandroid/src/main/java/com/timesafari/dailynotification/ChannelManager.javaandroid/src/main/AndroidManifest.xml
Test Applications:
test-apps/android-test-app/app/src/main/- Test app sourcetest-apps/android-test-app/app/src/main/assets/public/index.html- Test UItest-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-2branch for alternative implementations
Test Applications:
test-apps/ios-test-app/- Test app sourcetest-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 1385DailyNotificationPlugin.kt-scheduleDailyReminder()- Line 809DailyNotificationPlugin.kt-scheduleDualNotification()- Line 1685BootReceiver.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
- Uses
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
- Uses
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
- Uses
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 intentrescheduleNotifications()- 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_COMPLETEDpermission - Look for
SCHEDULE_EXACT_ALARMpermission - Look for
BootReceiverregistration - Look for
DailyNotificationReceiverregistration
- Look for
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-2branch for alternative implementations:git checkout ios-2 # Compare ios/Plugin/ implementations
Testing Tools & Commands
Android Testing
# 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 <package-name-from-test-app-manifest>
# 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
# 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:
- Document findings in the deliverables listed above
- Identify gaps between current behavior and desired behavior
- Create implementation directives to address gaps (if needed)
- Update plugin documentation to accurately reflect platform limitations
- Update API documentation with appropriate warnings and caveats