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