Files
daily-notification-plugin/docs/explore-alarm-behavior-directive.md
Matthew Raymer 6aa9140f67 docs: add comprehensive alarm/notification behavior documentation
- 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
2025-11-21 07:30:25 +00:00

24 KiB
Raw Blame History

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 (25 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.

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
      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 25 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.



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:
    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:

  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