- 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
27 KiB
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.
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 = falseAND 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:
- Plugin's
load()method is called - Plugin initializes database/storage
- Plugin queries for missed alarms/notifications:
- Find alarms with
scheduled_time < nowanddelivery_status != 'delivered' - Find notifications that should have fired but didn't
- Find alarms with
- 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"
- Reschedule future alarms/notifications that are still valid
- 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:
- Plugin's
load()method may be called (or app resumes) - Plugin checks for missed alarms/notifications (same as cold start)
- Plugin verifies that active alarms are still scheduled correctly
- 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:
- App launches (this is the only way to recover from force stop)
- Plugin's
load()method is called - Plugin detects that alarms were cancelled (all alarms have
scheduled_time < nowor are missing from AlarmManager) - Plugin queries database for all enabled alarms
- For each alarm:
- If
scheduled_time < now: Mark as missed, generate missed alarm event, reschedule if repeating - If
scheduled_time >= now: Reschedule the alarm
- If
- 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:
- Does the alarm/notification fire? (OS behavior)
- Does the plugin detect missed alarms? (Plugin behavior)
- Does the plugin recover/reschedule? (Plugin behavior)
- 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
- Line 64:
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
- Query:
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:
- Plugin
load()method called - Query database for missed alarms:
scheduled_time < now AND delivery_status != 'delivered' - For each missed alarm:
- Generate missed alarm event/notification
- Reschedule if repeating
- Update delivery status
- Reschedule all future alarms from database
- Verify active alarms match stored alarms
Warm Start Recovery:
- Plugin checks for missed alarms (same as cold start)
- Verify active alarms are still scheduled
- Reschedule if any were cancelled
Force Stop Recovery:
- Detect that all alarms were cancelled (force stop scenario)
- Query database for ALL enabled alarms
- For each alarm:
- If
scheduled_time < now: Mark as missed, generate event, reschedule if repeating - If
scheduled_time >= now: Reschedule immediately
- If
- 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:
- Add recovery logic to
load()method or createReactivationManager - Implement missed alarm detection using
getNotificationsReadyForDelivery()query - Implement force stop detection (all alarms cancelled)
- Implement rescheduling of future alarms from database
1.4 Persistence Completeness
Status: ✅ IMPLEMENTED
Findings:
- Room database used for persistence
Scheduleentity stores: id, kind, cron, clockTime, enabled, nextRunAtNotificationContentEntitystores: id, title, body, scheduledTime, priority, etc.ContentCachestores: 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:
- Detect that alarms were cancelled (check AlarmManager for scheduled alarms)
- Compare with database: if database has alarms but AlarmManager has none → force stop occurred
- Query database for ALL enabled alarms
- 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
- If
- 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:
- Implement app launch recovery (which will handle force stop as a special case)
- Add force stop detection: compare AlarmManager scheduled alarms with database
- 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
DailyNotificationStoragefor 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:
- Query plugin storage (CoreData) for all scheduled notifications
- Query
UNUserNotificationCenter.pendingNotificationRequests()for future notifications - Query
UNUserNotificationCenter.getDeliveredNotifications()for already-fired notifications - Find CoreData entries where:
scheduled_time < now(should have fired)- NOT in
deliveredNotificationslist (didn't fire) - NOT in
pendingNotificationRequestslist (not scheduled for future)
- Generate "missed notification" events for each detected miss
- Reschedule repeating notifications
- 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:
- Add notification recovery logic in
load()orsetupBackgroundTasks() - Implement three-way comparison: CoreData vs pending vs delivered notifications
- Add missed notification detection using the architecture above
- 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.
5. Next Steps
- Verify findings through testing using Plugin Behavior Exploration Template
- Test boot receiver on actual device reboot
- Test app launch recovery on cold/warm start
- Test force stop recovery on Android (with cross-check mechanism)
- Test missed notification detection on iOS (with three-way comparison)
- Inspect
UNUserNotificationCenter.getPendingNotificationRequests()vs CoreData to detect "lost" iOS notifications - Update Plugin Requirements document with verified gaps
- 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):
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<Schedule>,
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:
// 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):
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 - Use this for testing
- Plugin Requirements & Implementation - Requirements based on findings
- Platform Capability Reference - 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)
- iOS Behavior → Platform Reference (see Platform Capability Reference)
- Plugin Requirements → Independent document (see Plugin Requirements & Implementation)
- 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
- iOS missed notification detection requires three-way comparison: CoreData vs pending vs delivered
- Android force stop detection requires cross-check: AlarmManager vs database