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
This commit is contained in:
Matthew Raymer
2025-11-21 07:30:25 +00:00
parent 53845330f9
commit 6aa9140f67
8 changed files with 4542 additions and 0 deletions

View File

@@ -0,0 +1,244 @@
# Android Alarm Persistence, Recovery, and Limitations
**Author**: Matthew Raymer
**Date**: November 2025
**Status**: Engineering Directive - Active Reference
## Purpose
This document provides a **clean, consolidated, engineering-grade directive** summarizing Android's abilities and limitations for remembering, firing, and restoring alarms across:
- App kills
- Swipes from recents
- Device reboot
- **Force stop**
- User-triggered reactivation
This is the actionable version you can plug directly into your architecture docs.
---
## 1. Core Principle
Android does **not** guarantee persistence of alarms across process death, swipes, or reboot.
It is the app's responsibility to **persist alarm definitions** and **re-schedule them** under allowed system conditions.
The following directives outline **exactly what is possible** and **what is impossible**.
---
## 2. Allowed Behaviors (What *Can* Work)
### 2.1 Alarms survive UI kills (swipe from recents)
`AlarmManager.setExactAndAllowWhileIdle(...)` alarms **will fire** even after:
- App is swiped away
- App process is killed by the OS
The OS recreates your app's process to deliver the `PendingIntent`.
**Directive:**
Use `setExactAndAllowWhileIdle` for alarm execution.
---
### 2.2 Alarms can be preserved across device reboot
Android wipes all alarms on reboot, but **you may recreate them**.
**Directive:**
1. Persist all alarms in storage (Room DB or SharedPreferences).
2. Add a `BOOT_COMPLETED` / `LOCKED_BOOT_COMPLETED` broadcast receiver.
3. On boot, load all enabled alarms and reschedule them using AlarmManager.
**Permissions required:**
- `RECEIVE_BOOT_COMPLETED`
**Conditions:**
- User must have launched your app at least once before reboot to grant boot receiver execution.
---
### 2.3 Alarms can fire full-screen notifications and wake the device
**Directive:**
Implement `setFullScreenIntent(...)`, use an IMPORTANCE_HIGH channel with `CATEGORY_ALARM`.
This allows Clock-appstyle alarms even when the app is not foregrounded.
---
### 2.4 Alarms can be restored after app restart
If the user re-opens the app (direct user action), you may:
- Scan the persistent DB
- Detect "missed" alarms
- Reschedule future alarms
- Fire "missed alarm" notifications
- Reconstruct WorkManager/JobScheduler tasks wiped by OS
**Directive:**
Create a `ReactivationManager` that runs on every app launch and recomputes the correct alarm state.
---
## 3. Forbidden Behaviors (What *Cannot* Work)
### 3.1 You cannot survive "Force Stop"
**Settings → Apps → YourApp → Force Stop** triggers:
- Removal of all alarms
- Removal of WorkManager tasks
- Blocking of all broadcast receivers (including BOOT_COMPLETED)
- Blocking of all JobScheduler jobs
- Blocking of AlarmManager callbacks
- Your app will NOT run until the user manually launches it again
**Directive:**
Accept that FORCE STOP is a hard kill.
No scheduling, alarms, jobs, or receivers may execute afterward.
---
### 3.2 You cannot auto-resume after "Force Stop"
You may only resume tasks when:
- The user opens your app
- The user taps a notification belonging to your app
- The user interacts with a widget/deep link
- Another app explicitly targets your component
**Directive:**
Provide user-facing reactivation pathways (icon, widget, notification).
---
### 3.3 Alarms cannot be preserved solely in RAM
Android can kill your app's RAM state at any time.
**Directive:**
All alarm data must be persisted in durable storage.
---
### 3.4 You cannot bypass Doze or battery optimization restrictions without permission
Doze may defer inexact alarms; exact alarms with `setExactAndAllowWhileIdle` are allowed.
**Directive:**
Request `SCHEDULE_EXACT_ALARM` on Android 12+.
---
## 4. Required Implementation Components
### 4.1 Persistent Storage
Create a table or serialized structure for alarms:
```
id: Int
timeMillis: Long
repeat: NONE | DAILY | WEEKLY | CUSTOM
label: String
enabled: Boolean
```
---
### 4.2 Alarm Scheduling
Use:
```kotlin
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
triggerAtMillis,
pendingIntent
)
```
---
### 4.3 Boot Receiver
Reschedules alarms from storage.
---
### 4.4 Reactivation Manager
Runs on **every app launch** and performs:
- Load pending alarms
- Detect overdue alarms
- Reschedule future alarms
- Trigger notifications for missed alarms
---
### 4.5 Full-Screen Alarm UI
Use a `BroadcastReceiver` → Notification with full-screen intent → Activity.
---
## 5. Summary of Android Alarm Capability Matrix
| Scenario | Will Alarm Fire? | Reason |
| --------------------------------------- | --------------------------------------- | --------------------------------------------------------------- |
| **Swipe from Recents** | ✅ Yes | AlarmManager resurrects the app process |
| **App silently killed by OS** | ✅ Yes | AlarmManager still holds scheduled alarms |
| **Device Reboot** | ❌ No (auto) / ✅ Yes (if you reschedule) | Alarms wiped on reboot |
| **Doze Mode** | ⚠️ Only "exact" alarms | Must use `setExactAndAllowWhileIdle` |
| **Force Stop** | ❌ Never | Android blocks all callbacks + receivers until next user launch |
| **User reopens app** | ✅ You may reschedule & recover | All logic must be implemented by app |
| **PendingIntent from user interaction** | ✅ If triggered by user | User action unlocks the app |
---
## 6. Final Directive
> **Design alarm behavior with the assumption that Android will destroy all scheduled work on reboot or force-stop.
>
> Persist all alarm definitions. On every boot or app reactivation, reconstruct and reschedule alarms.
>
> Never rely on the OS to preserve alarms except across UI process kills.
>
> Accept that "force stop" is a hard stop that cannot be bypassed.**
---
## Related Documentation
- [Boot Receiver Testing Guide](./boot-receiver-testing-guide.md)
- [App Startup Recovery Solution](./app-startup-recovery-solution.md)
- [Reboot Testing Procedure](./reboot-testing-procedure.md)
---
## Future Directives
Potential follow-up directives:
- **How to implement the minimal alarm system**
- **How to implement a Clock-style robust alarm system**
- **How to map this to your own app's architecture**

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,669 @@
# 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

View File

@@ -0,0 +1,670 @@
# 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
```bash
git checkout ios-2
# Compare ios/Plugin/DailyNotificationPlugin.swift
```
2. **Identify the scheduling mechanism:**
- **UNUserNotificationCenter**: Primary mechanism
- `DailyNotificationScheduler.scheduleNotification()` uses `UNCalendarNotificationTrigger` (line 172)
- `DailyNotificationPlugin.scheduleUserNotification()` uses `UNTimeIntervalNotificationTrigger` (line 514)
- No `UNLocationNotificationTrigger` found
- **BGTaskScheduler**: Used for background fetch
- `DailyNotificationPlugin.scheduleBackgroundFetch()` (line 495)
- Uses `BGAppRefreshTaskRequest` (line 496)
- **No timers**: No plain Timer usage found (would die with app)
3. **Determine what's persisted:**
- Does the plugin store alarms in:
- `UserDefaults`?
- Files / CoreData?
- Or only within UNUserNotificationCenter's pending notification requests (no parallel app-side storage)?
4. **Check for re-scheduling behavior on app launch:**
- On app start (cold or warm), does plugin:
- Query `UNUserNotificationCenter` for pending notifications?
- Compare against its own store?
- Attempt to rebuild schedules?
- Or does it rely solely on UNUserNotificationCenter to manage everything?
5. **Determine capabilities / limitations:**
- Can the plugin run arbitrary code *when the notification fires*?
- Only via notification actions / `didReceive response` callbacks.
- Does it support repeating notifications (daily/weekly)?
---
### 3.2 Behavior Testing Matrix (iOS)
**Test Applications:**
- **Primary**: `test-apps/ios-test-app/` - Use this for comprehensive testing
- **Secondary**: `test-apps/daily-notification-test/` - Additional test scenarios if needed
- **Note**: Compare behavior between main branch and `ios-2` branch implementations if they differ
As with Android, test:
#### 3.2.1 Base Case
- Schedule local notification 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.
---
## Related Documentation
- [Android Alarm Persistence Directive](./android-alarm-persistence-directive.md) - General Android alarm capabilities and limitations
- [Boot Receiver Testing Guide](./boot-receiver-testing-guide.md) - Testing boot receiver behavior
- [App Startup Recovery Solution](./app-startup-recovery-solution.md) - Recovery mechanisms on app launch
- [Reboot Testing Procedure](./reboot-testing-procedure.md) - Step-by-step reboot testing
---
## Source Code Structure Reference
### Android Source Files
**Primary Plugin Code:**
- `android/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.kt` (or `.java`)
- `android/src/main/java/com/timesafari/dailynotification/DailyNotificationWorker.java`
- `android/src/main/java/com/timesafari/dailynotification/DailyNotificationReceiver.java`
- `android/src/main/java/com/timesafari/dailynotification/ChannelManager.java`
- `android/src/main/AndroidManifest.xml`
**Test Applications:**
- `test-apps/android-test-app/app/src/main/` - Test app source
- `test-apps/android-test-app/app/src/main/assets/public/index.html` - Test UI
- `test-apps/daily-notification-test/` - Additional test app (if present)
### iOS Source Files
**Primary Plugin Code:**
- `ios/Plugin/DailyNotificationPlugin.swift` (or similar)
- `ios/Plugin/` - All Swift plugin files
- **Also check**: `ios-2` branch for alternative implementations
**Test Applications:**
- `test-apps/ios-test-app/` - Test app source
- `test-apps/daily-notification-test/` - Additional test app (if iOS support exists)
---
## Detailed Code References (File Locations, Functions, Line Numbers)
### Android Implementation Details
#### Alarm Scheduling
**File**: `android/src/main/java/com/timesafari/dailynotification/NotifyReceiver.kt`
- **Function**: `scheduleExactNotification()` - **Lines 92-247**
- Schedules exact alarms using AlarmManager
- Uses `setAlarmClock()` for Android 5.0+ (API 21+) - **Line 219**
- Falls back to `setExactAndAllowWhileIdle()` for Android 6.0+ (API 23+) - **Line 223**
- Falls back to `setExact()` for older versions - **Line 231**
- Called from:
- `DailyNotificationPlugin.kt` - `scheduleDailyNotification()` - **Line 1385**
- `DailyNotificationPlugin.kt` - `scheduleDailyReminder()` - **Line 809**
- `DailyNotificationPlugin.kt` - `scheduleDualNotification()` - **Line 1685**
- `BootReceiver.kt` - `rescheduleNotifications()` - **Line 74**
**File**: `android/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.kt`
- **Function**: `scheduleDailyNotification()` - **Lines 1302-1417**
- Main plugin method for scheduling notifications
- Checks exact alarm permission - **Line 1309**
- Opens settings if permission not granted - **Lines 1314-1324**
- Calls `NotifyReceiver.scheduleExactNotification()` - **Line 1385**
- Schedules prefetch 2 minutes before notification - **Line 1395**
- **Function**: `scheduleDailyReminder()` - **Lines 777-833**
- Schedules static reminders (no content dependency)
- Calls `NotifyReceiver.scheduleExactNotification()` - **Line 809**
- **Function**: `canScheduleExactAlarms()` - **Lines 835-860**
- Checks if exact alarm permission is granted (Android 12+)
**File**: `android/src/main/java/com/timesafari/dailynotification/PendingIntentManager.java`
- **Function**: `scheduleExactAlarm()` - **Lines 127-158**
- Uses `setExactAndAllowWhileIdle()` - **Line 135**
- Falls back to `setExact()` - **Line 141**
**File**: `android/src/main/java/com/timesafari/dailynotification/DailyNotificationExactAlarmManager.java`
- **Function**: `scheduleExactAlarm()` - **Lines 186-201**
- Uses `setExactAndAllowWhileIdle()` - **Line 189**
- Falls back to `setExact()` - **Line 193**
**File**: `android/src/main/java/com/timesafari/dailynotification/DailyNotificationScheduler.java`
- **Function**: `scheduleExactAlarm()` - **Lines 237-272**
- Uses `setExactAndAllowWhileIdle()` - **Line 242**
- Falls back to `setExact()` - **Line 251**
#### WorkManager Usage
**File**: `android/src/main/java/com/timesafari/dailynotification/FetchWorker.kt`
- **Function**: `scheduleFetch()` - **Lines 31-59**
- Schedules WorkManager one-time work request
- Uses `OneTimeWorkRequestBuilder` - **Line 36**
- Enqueues with `WorkManager.getInstance().enqueueUniqueWork()` - **Lines 53-58**
- **Function**: `scheduleDelayedPrefetch()` - **Lines 62-131**
- Schedules delayed prefetch work
- Uses `setInitialDelay()` - **Line 104**
**File**: `android/src/main/java/com/timesafari/dailynotification/DailyNotificationWorker.java`
- **Function**: `doWork()` - **Lines 59-915**
- Main WorkManager worker execution
- Handles notification display - **Line 91**
- Calls `displayNotification()` - **Line 200+**
- **Function**: `displayNotification()` - **Lines 200-400+**
- Displays notification using NotificationCompat
- Ensures notification channel exists
- Uses `NotificationCompat.Builder` - **Line 200+**
**File**: `android/src/main/java/com/timesafari/dailynotification/DailyNotificationFetcher.java`
- **Function**: `scheduleFetch()` - **Lines 78-140**
- Schedules WorkManager fetch work
- Uses `OneTimeWorkRequest.Builder` - **Line 106**
- Enqueues with `workManager.enqueueUniqueWork()` - **Lines 115-119**
#### Boot Recovery
**File**: `android/src/main/java/com/timesafari/dailynotification/BootReceiver.kt`
- **Class**: `BootReceiver` - **Lines 18-100+**
- BroadcastReceiver for BOOT_COMPLETED
- `onReceive()` - **Line 24** - Handles boot intent
- `rescheduleNotifications()` - **Line 38+** - Reschedules all notifications from database
- Calls `NotifyReceiver.scheduleExactNotification()` - **Line 74**
**File**: `android/src/main/java/com/timesafari/dailynotification/DailyNotificationRebootRecoveryManager.java`
- **Class**: `BootCompletedReceiver` - **Lines 278-297**
- Inner BroadcastReceiver for boot events
- `onReceive()` - **Line 280** - Handles BOOT_COMPLETED action
- Calls `handleSystemReboot()` - **Line 290**
#### Notification Display
**File**: `android/src/main/java/com/timesafari/dailynotification/DailyNotificationReceiver.java`
- **Function**: `onReceive()` - **Lines 51-485**
- Lightweight BroadcastReceiver triggered by AlarmManager
- Enqueues WorkManager work for heavy operations - **Line 100+**
- Extracts notification ID and action from intent
**File**: `android/src/main/java/com/timesafari/dailynotification/NotifyReceiver.kt`
- **Function**: `showNotification()` - **Lines 443-500**
- Displays notification using NotificationCompat
- Creates notification channel if needed - **Lines 454-470**
- Uses `NotificationCompat.Builder` - **Line 482**
#### Persistence
**File**: `android/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.kt`
- Database operations use Room database:
- `getDatabase()` - Returns DailyNotificationDatabase instance
- Schedule storage in `scheduleDailyNotification()` - **Lines 1393-1410**
- Schedule storage in `scheduleDailyReminder()` - **Lines 813-821**
#### Permissions & Manifest
**File**: `android/src/main/AndroidManifest.xml`
- **Note**: Plugin manifest is minimal; receivers declared in consuming app manifest
- Check test app manifest: `test-apps/android-test-app/app/src/main/AndroidManifest.xml`
- Look for `RECEIVE_BOOT_COMPLETED` permission
- Look for `SCHEDULE_EXACT_ALARM` permission
- Look for `BootReceiver` registration
- Look for `DailyNotificationReceiver` registration
### iOS Implementation Details
#### Notification Scheduling
**File**: `ios/Plugin/DailyNotificationPlugin.swift`
- **Class**: `DailyNotificationPlugin` - **Lines 24-532**
- Main Capacitor plugin class
- Uses `UNUserNotificationCenter.current()` - **Line 26**
- Uses `BGTaskScheduler.shared` - **Line 27**
- **Function**: `scheduleUserNotification()` - **Lines 506-529**
- Schedules notification using UNUserNotificationCenter
- Creates `UNMutableNotificationContent` - **Line 507**
- Creates `UNTimeIntervalNotificationTrigger` - **Line 514**
- Adds request via `notificationCenter.add()` - **Line 522**
- **Function**: `scheduleBackgroundFetch()` - **Lines 495-504**
- Schedules BGTaskScheduler background fetch
- Creates `BGAppRefreshTaskRequest` - **Line 496**
- Submits via `backgroundTaskScheduler.submit()` - **Line 502**
**File**: `ios/Plugin/DailyNotificationScheduler.swift`
- **Class**: `DailyNotificationScheduler` - **Lines 20-236+**
- Manages UNUserNotificationCenter scheduling
- **Function**: `scheduleNotification()` - **Lines 133-198**
- Schedules notification with calendar trigger
- Creates `UNCalendarNotificationTrigger` - **Lines 172-175**
- Creates `UNNotificationRequest` - **Lines 178-182**
- Adds via `notificationCenter.add()` - **Line 185**
- **Function**: `cancelNotification()` - **Lines 205-213**
- Cancels notification by ID
- Uses `notificationCenter.removePendingNotificationRequests()` - **Line 206**
#### Background Tasks
**File**: `ios/Plugin/DailyNotificationBackgroundTasks.swift`
- Background task handling for BGTaskScheduler
- Register background task identifiers
- Handle background fetch execution
#### Persistence
**File**: `ios/Plugin/DailyNotificationPlugin.swift**
- **Note**: Check for UserDefaults, CoreData, or file-based storage
- Storage component: `var storage: DailyNotificationStorage?` - **Line 35**
- Scheduler component: `var scheduler: DailyNotificationScheduler?` - **Line 36**
#### iOS-2 Branch
- **Note**: Check `ios-2` branch for alternative implementations:
```bash
git checkout ios-2
# Compare ios/Plugin/ implementations
```
---
## Testing Tools & Commands
### Android Testing
```bash
# Check scheduled alarms
adb shell dumpsys alarm | grep -i timesafari
# Force kill (not force-stop) - adjust package name based on test app
adb shell am kill com.timesafari.dailynotification
# Or for test apps:
# adb shell am kill com.timesafari.androidtestapp
# adb shell am kill <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
```bash
# View device logs (requires Xcode)
xcrun simctl spawn booted log stream --predicate 'processImagePath contains "DailyNotification"'
# List pending notifications (requires app code)
# Use UNUserNotificationCenter.getPendingNotificationRequests()
# Build test app (from test-apps/ios-test-app)
# Use Xcode or:
cd test-apps/ios-test-app
# Follow build instructions in test app README
# Check ios-2 branch for alternative implementations
git checkout ios-2
# Compare ios/Plugin/ implementations between branches
```
---
## Next Steps After Exploration
Once this exploration is complete:
1. **Document findings** in the deliverables listed above
2. **Identify gaps** between current behavior and desired behavior
3. **Create implementation directives** to address gaps (if needed)
4. **Update plugin documentation** to accurately reflect platform limitations
5. **Update API documentation** with appropriate warnings and caveats

View File

@@ -0,0 +1,296 @@
# ✅ DIRECTIVE: Improvements to Alarm/Schedule/Notification Directives for Capacitor Plugin
**Author**: Matthew Raymer
**Date**: November 2025
**Status**: Active Improvement Directive
## 0. Goal of This Improvement Directive
Unify, refine, and strengthen the existing alarm-behavior directives so they:
1. **Eliminate duplication** between the Android Alarm Persistence Directive and the Plugin Exploration Directive.
2. **Clarify scope and purpose** (one is exploration/investigation; the other is platform mechanics).
3. **Produce a single cohesive standard** to guide future plugin improvements, testing, and documentation.
4. **Streamline testing expectations** into executable, check-box-style matrices.
5. **Map OS-level limitations directly to plugin-level behavior** so the JS/TS API contract is unambiguous.
6. **Add missing iOS-specific limitations, guarantees, and required recovery patterns**.
7. **Provide an upgrade path from exploration → design decisions → implementation directives**.
---
## 1. Structural Improvements to Apply
### 1.1 Split responsibilities into three documents (clear roles)
Your current directives mix *exploration*, *reference*, and *design rules*.
Improve by creating three clearly separated docs:
#### **Document A — Platform Capability Reference (Android/iOS)**
* Pure OS-level facts (no plugin logic).
* Use the Android Alarm Persistence Directive as the baseline.
* Add an equivalent iOS capability matrix.
* Keep strictly normative, minimal, stable.
#### **Document B — Plugin Behavior Exploration (Android/iOS)**
* Use the uploaded exploration directive as the baseline.
* Remove platform-mechanics explanations (moved to Document A).
* Replace vague descriptions with concrete, line-number-linked tasks.
* Add "expected vs actual" checklists to each test item.
#### **Document C — Plugin Requirements & Improvements**
* Generated after exploration.
* Defines the rules the plugin *must follow* to behave predictably.
* Defines recovery strategy (boot, reboot, missed alarms, force stop behavior).
* Defines JS API caveats and warnings.
**This file you're asking for now (improvement directive) becomes the origin of Document C.**
---
## 2. Improvements Needed to Existing Directives
### 2.1 Reduce duplication in Android section
The exploration directive currently repeats much of the alarm persistence directive.
**Improve by:**
* Referencing the Android alarm document instead of replicating content.
* Summarizing Android limitations in 57 lines in the exploration document.
* Keeping full explanation *only* in the Android alarm persistence reference file.
---
### 2.2 Add missing iOS counterpart to Android's capability matrix
You have a complete matrix for Android, but not iOS.
Add a **parallel iOS matrix**, including:
* Notification survives swipe → yes
* Notification survives reboot → yes (for calendar/time triggers)
* App logic runs in background → no
* Arbitrary code on trigger → no
* Recovery required → only if plugin has its own DB
This fixes asymmetry in current directives.
---
### 2.3 Clarify when plugin behavior depends on OS behavior
The exploration directive needs clearer labeling:
**Label each behavior as:**
* **OS-guaranteed** (iOS will fire pending notifications even when the app is dead)
* **Plugin-guaranteed** (plugin must reschedule alarms from DB)
* **Not allowed** (Android force-stop)
This removes ambiguity for plugin developers.
---
### 2.4 Introduce "Observed Behavior Table" in the exploration doc
Currently tests describe how to test but do not include space for results.
Add a table like:
| Scenario | Expected (OS) | Expected (Plugin) | Actual Result | Notes |
| ------------------ | --------------------------- | ------------------------------ | ------------- | ----- |
| Swipe from recents | Fires | Fires | | |
| Device reboot | Does NOT fire automatically | Plugin must reschedule at boot | | |
This allows the exploration document to be executable.
---
### 2.5 Add "JS/TS API Contract" section to both directives
A critical missing piece.
Define what JavaScript developers can assume:
Examples:
* "Notification will still fire if the app is swiped from recents."
* "Notifications WILL NOT fire after Android Force Stop until the app is opened again."
* "Reboot behavior depends on platform: iOS preserves, Android destroys."
This section makes plugin behavior developer-friendly and predictable.
---
### 2.6 Strengthen direction on persistence strategy
The exploration directive asks "what is persisted?" but does not specify what *should* be persisted.
Add:
#### **Required Persistence Items**
* alarm_id
* trigger time
* repeat rule
* channel/priority
* payload
* time created, last modified
And:
#### **Required Recovery Points**
* Boot event
* App cold start
* App warm start
* App returning from background fetch
* User tapping notification
---
### 2.7 Add iOS Background Execution Limits section
Currently missing.
Add:
* No repeating background execution APIs except BGTaskScheduler
* BGTaskScheduler requires minimum intervals
* Plugin cannot rely on background execution to reconstruct alarms
* Only notification center persists notifications
This is critical for plugin parity.
---
### 2.8 Integrate "missed alarm recovery" requirements
The exploration directive asks whether plugin detects missed alarms.
The improvement directive must assert that it **must**.
Add requirement:
* If alarm time < now, and plugin is activated by reboot or user opening the app → plugin must generate a "missed alarm" event or notification.
---
## 3. Rewrite Testing Protocols into Standardized Formats
### Replace long paragraphs with clear test tables, e.g.:
#### Android Reboot Test
| Step | Action |
| ---- | ------------------------------------------------ |
| 1 | Schedule alarm for 10 minutes later |
| 2 | Reboot device |
| 3 | Do not open app |
| 4 | Does alarm fire? (Expected: NO) |
| 5 | Open app |
| 6 | Does plugin detect missed alarm? (Expected: YES) |
Same for iOS, force-stop, etc.
---
## 4. Add Missing Directive Sections
### 4.1 Policy Section
Define:
* Unsupported features
* Required permissions
* Required manifest entries
* Required notification channels
### 4.2 Versioning Requirements
Each change to alarm behavior is **breaking** and must have:
* MAJOR version bump
* Migration guide
---
## 5. Final Improvement Directive (What to Produce Next)
Here is your actionable deliverable list:
### **Produce Three New Documents**
1. **Platform Reference Document**
* Android alarm rules
* iOS notification rules
* Both in tabular form
* (Rewrites + merges the two uploaded directives)
2. **Exploration Results Template**
* Table format for results
* Expected vs actual
* Direct code references
* Remove platform explanation duplication
3. **Plugin Requirements + Future Implementation Directive**
* Persistence spec
* Recovery spec
* JS/TS API contract
* Parity rules
* Android/iOS caveats
* Required test harness
### **Implement Major Improvements**
* Strengthen separation of concerns
* Add iOS parity
* Integrate plugin-level persistence + recovery
* Add test matrices
* Add clear developer contracts
* Add missed-alarm handling requirements
* Add design rules for exact alarms and background restrictions
---
## 6. One-Sentence Summary
> **Rewrite the existing directives into three clear documents—platform reference, plugin exploration, and plugin implementation requirements—while adding iOS parity, recovery rules, persistence requirements, and standardized testing matrices, removing duplicated Android content, and specifying a clear JS/TS API contract.**
---
## Related Documentation
- [Android Alarm Persistence Directive](./android-alarm-persistence-directive.md) - Source material for Document A
- [Explore Alarm Behavior Directive](./explore-alarm-behavior-directive.md) - Source material for Document B
- [Boot Receiver Testing Guide](./boot-receiver-testing-guide.md) - Testing procedures
- [App Startup Recovery Solution](./app-startup-recovery-solution.md) - Recovery mechanisms
---
## Next Steps
1. **Create Document A**: Platform Capability Reference (Android/iOS)
2. **Create Document B**: Plugin Behavior Exploration Template
3. **Create Document C**: Plugin Requirements & Implementation Directive
4. **Execute exploration** using Document B
5. **Update Document C** with findings from exploration
6. **Implement improvements** based on Document C
---
## Status Tracking
- [ ] Document A created
- [ ] Document B created
- [ ] Document C created
- [ ] Exploration executed
- [ ] Findings documented
- [ ] Improvements implemented

View File

@@ -0,0 +1,301 @@
# Platform Capability Reference: Android & iOS Alarm/Notification Behavior
**Author**: Matthew Raymer
**Date**: November 2025
**Status**: Platform Reference - Stable
## Purpose
This document provides **pure OS-level facts** about alarm and notification capabilities on Android and iOS. It contains no plugin-specific logic—only platform mechanics that affect plugin design.
This is a **reference document** to be consulted when designing plugin behavior, not an implementation guide.
---
## 1. Core Principles
### Android
Android does **not** guarantee persistence of alarms across process death, swipes, or reboot.
It is the app's responsibility to **persist alarm definitions** and **re-schedule them** under allowed system conditions.
### iOS
iOS **does** persist scheduled local notifications across app termination and device reboot, but:
* App code does **not** run when notifications fire (unless user interacts)
* Background execution is severely limited
* Plugin must persist its own state if it needs to track or recover missed notifications
---
## 2. Android Alarm Capability Matrix
| Scenario | Will Alarm Fire? | OS Behavior | App Responsibility |
| --------------------------------------- | --------------------------------------- | -------------------------------------------------------------------- | ----------------------------------------------------- |
| **Swipe from Recents** | ✅ Yes | AlarmManager resurrects the app process | None (OS handles) |
| **App silently killed by OS** | ✅ Yes | AlarmManager still holds scheduled alarms | None (OS handles) |
| **Device Reboot** | ❌ No (auto) / ✅ Yes (if you reschedule) | All alarms wiped on reboot | Must reschedule from persistent storage on boot |
| **Doze Mode** | ⚠️ Only "exact" alarms | Inexact alarms deferred; exact alarms allowed | Must use `setExactAndAllowWhileIdle` |
| **Force Stop** | ❌ Never | Android blocks all callbacks + receivers until next user launch | Cannot bypass; must detect on app restart |
| **User reopens app** | ✅ You may reschedule & recover | App process restarted | Must detect missed alarms and reschedule future ones |
| **PendingIntent from user interaction** | ✅ If triggered by user | User action unlocks the app | None (OS handles) |
### Android Allowed Behaviors
#### 2.1 Alarms survive UI kills (swipe from recents)
`AlarmManager.setExactAndAllowWhileIdle(...)` alarms **will fire** even after:
* App is swiped away
* App process is killed by the OS
The OS recreates your app's process to deliver the `PendingIntent`.
**Required API**: `setExactAndAllowWhileIdle()` or `setAlarmClock()`
#### 2.2 Alarms can be preserved across device reboot
Android wipes all alarms on reboot, but **you may recreate them**.
**Required Components**:
1. Persist all alarms in storage (Room DB or SharedPreferences)
2. Add a `BOOT_COMPLETED` / `LOCKED_BOOT_COMPLETED` broadcast receiver
3. On boot, load all enabled alarms and reschedule them using AlarmManager
**Permissions required**: `RECEIVE_BOOT_COMPLETED`
**Conditions**: User must have launched your app at least once before reboot
#### 2.3 Alarms can fire full-screen notifications and wake the device
**Required API**: `setFullScreenIntent(...)`, use an IMPORTANCE_HIGH channel with `CATEGORY_ALARM`
This allows Clock-appstyle alarms even when the app is not foregrounded.
#### 2.4 Alarms can be restored after app restart
If the user re-opens the app (direct user action), you may:
* Scan the persistent DB
* Detect "missed" alarms
* Reschedule future alarms
* Fire "missed alarm" notifications
* Reconstruct WorkManager/JobScheduler tasks wiped by OS
**Required**: Create a `ReactivationManager` that runs on every app launch
### Android Forbidden Behaviors
#### 3.1 You cannot survive "Force Stop"
**Settings → Apps → YourApp → Force Stop** triggers:
* Removal of all alarms
* Removal of WorkManager tasks
* Blocking of all broadcast receivers (including BOOT_COMPLETED)
* Blocking of all JobScheduler jobs
* Blocking of AlarmManager callbacks
* Your app will NOT run until the user manually launches it again
**Directive**: Accept that FORCE STOP is a hard kill. No scheduling, alarms, jobs, or receivers may execute afterward.
#### 3.2 You cannot auto-resume after "Force Stop"
You may only resume tasks when:
* The user opens your app
* The user taps a notification belonging to your app
* The user interacts with a widget/deep link
* Another app explicitly targets your component
**Directive**: Provide user-facing reactivation pathways (icon, widget, notification).
#### 3.3 Alarms cannot be preserved solely in RAM
Android can kill your app's RAM state at any time.
**Directive**: All alarm data must be persisted in durable storage.
#### 3.4 You cannot bypass Doze or battery optimization restrictions without permission
Doze may defer inexact alarms; exact alarms with `setExactAndAllowWhileIdle` are allowed.
**Required Permission**: `SCHEDULE_EXACT_ALARM` on Android 12+ (API 31+)
---
## 3. iOS Notification Capability Matrix
| Scenario | Will Notification Fire? | OS Behavior | App Responsibility |
| --------------------------------------- | ----------------------- | -------------------------------------------------------------------- | ----------------------------------------------------- |
| **Swipe from App Switcher** | ✅ Yes | UNUserNotificationCenter persists and fires notifications | None (OS handles) |
| **App Terminated by System** | ✅ Yes | UNUserNotificationCenter persists and fires notifications | None (OS handles) |
| **Device Reboot** | ✅ Yes (for calendar/time triggers) | iOS persists scheduled local notifications across reboot | None for notifications; must persist own state if needed |
| **App Force Quit (swipe away)** | ✅ Yes | UNUserNotificationCenter persists and fires notifications | None (OS handles) |
| **Background Execution** | ❌ No arbitrary code | Only BGTaskScheduler with strict limits | Cannot rely on background execution for recovery |
| **Notification Fires** | ✅ Yes | Notification displayed; app code does NOT run unless user interacts | Must handle missed notifications on next app launch |
| **User Taps Notification** | ✅ Yes | App launched; code can run | Can detect and handle missed notifications |
### iOS Allowed Behaviors
#### 3.1 Notifications survive app termination
`UNUserNotificationCenter` scheduled notifications **will fire** even after:
* App is swiped away from app switcher
* App is terminated by system
* Device reboots (for calendar/time-based triggers)
**Required API**: `UNUserNotificationCenter.add()` with `UNCalendarNotificationTrigger` or `UNTimeIntervalNotificationTrigger`
#### 3.2 Notifications persist across device reboot
iOS **automatically** persists scheduled local notifications across reboot.
**No app code required** for basic notification persistence.
**Limitation**: Only calendar and time-based triggers persist. Location-based triggers do not.
#### 3.3 Background tasks for prefetching
**Required API**: `BGTaskScheduler` with `BGAppRefreshTaskRequest`
**Limitations**:
* Minimum interval between tasks (system-controlled, typically hours)
* System decides when to execute (not guaranteed)
* Cannot rely on background execution for alarm recovery
* Must schedule next task immediately after current one completes
### iOS Forbidden Behaviors
#### 4.1 App code does not run when notification fires
When a scheduled notification fires:
* Notification is displayed to user
* **No app code executes** unless user taps the notification
* Cannot run arbitrary code at notification time
**Workaround**: Use notification actions or handle missed notifications on next app launch.
#### 4.2 No repeating background execution
iOS does not provide repeating background execution APIs except:
* `BGTaskScheduler` (system-controlled, not guaranteed)
* Background fetch (deprecated, unreliable)
**Directive**: Plugin cannot rely on background execution to reconstruct alarms. Must persist state and recover on app launch.
#### 4.3 No arbitrary code on notification trigger
Unlike Android's `PendingIntent` which can execute code, iOS notifications only:
* Display to user
* Launch app if user taps
* Execute notification action handlers (if configured)
**Directive**: All recovery logic must run on app launch, not at notification time.
#### 4.4 Background execution limits
**BGTaskScheduler Limitations**:
* Minimum intervals between tasks (system-controlled)
* System may defer or skip tasks
* Tasks have time budgets (typically 30 seconds)
* Cannot guarantee execution timing
**Directive**: Use BGTaskScheduler for prefetching only, not for critical scheduling.
---
## 4. Cross-Platform Comparison
| Feature | Android | iOS |
| -------------------------------- | --------------------------------------- | --------------------------------------------- |
| **Survives swipe/termination** | ✅ Yes (with exact alarms) | ✅ Yes (automatic) |
| **Survives reboot** | ❌ No (must reschedule) | ✅ Yes (automatic for calendar/time triggers) |
| **App code runs on trigger** | ✅ Yes (via PendingIntent) | ❌ No (only if user interacts) |
| **Background execution** | ✅ WorkManager, JobScheduler | ⚠️ Limited (BGTaskScheduler only) |
| **Force stop equivalent** | ✅ Force Stop (hard kill) | ❌ No user-facing equivalent |
| **Boot recovery required** | ✅ Yes (must implement) | ❌ No (OS handles) |
| **Missed alarm detection** | ✅ Must implement on app launch | ✅ Must implement on app launch |
| **Exact timing** | ✅ Yes (with permission) | ⚠️ ±180s tolerance |
| **Repeating notifications** | ✅ Must reschedule each occurrence | ✅ Can use `repeats: true` in trigger |
---
## 5. Required Platform APIs
### Android
**Alarm Scheduling**:
* `AlarmManager.setExactAndAllowWhileIdle()` - Android 6.0+ (API 23+)
* `AlarmManager.setAlarmClock()` - Android 5.0+ (API 21+)
* `AlarmManager.setExact()` - Android 4.4+ (API 19+)
**Permissions**:
* `RECEIVE_BOOT_COMPLETED` - Boot receiver
* `SCHEDULE_EXACT_ALARM` - Android 12+ (API 31+)
**Background Work**:
* `WorkManager` - Deferrable background work
* `JobScheduler` - Alternative (API 21+)
### iOS
**Notification Scheduling**:
* `UNUserNotificationCenter.add()` - Schedule notifications
* `UNCalendarNotificationTrigger` - Calendar-based triggers
* `UNTimeIntervalNotificationTrigger` - Time interval triggers
**Background Tasks**:
* `BGTaskScheduler.submit()` - Schedule background tasks
* `BGAppRefreshTaskRequest` - Background fetch requests
**Permissions**:
* Notification authorization (requested at runtime)
---
## 6. Platform-Specific Constraints Summary
### Android Constraints
1. **Reboot**: All alarms wiped; must reschedule from persistent storage
2. **Force Stop**: Hard kill; cannot bypass until user opens app
3. **Doze**: Inexact alarms deferred; must use exact alarms
4. **Exact Alarm Permission**: Required on Android 12+ for precise timing
5. **Boot Receiver**: Must be registered and handle `BOOT_COMPLETED`
### iOS Constraints
1. **Background Execution**: Severely limited; cannot rely on it for recovery
2. **Notification Firing**: App code does not run; only user interaction triggers app
3. **Timing Tolerance**: ±180 seconds for calendar triggers
4. **BGTaskScheduler**: System-controlled; not guaranteed execution
5. **State Persistence**: Must persist own state if tracking missed notifications
---
## Related Documentation
- [Plugin Behavior Exploration Template](./plugin-behavior-exploration-template.md) - Uses this reference
- [Plugin Requirements & Implementation](./plugin-requirements-implementation.md) - Implementation based on this reference
- [Android Alarm Persistence Directive](./android-alarm-persistence-directive.md) - Original Android reference
- [Improve Alarm Directives](./improve-alarm-directives.md) - Improvement directive
---
## Version History
- **v1.0** (November 2025): Initial platform capability reference
- Android alarm matrix
- iOS notification matrix
- Cross-platform comparison

View File

@@ -0,0 +1,363 @@
# Plugin Behavior Exploration Template
**Author**: Matthew Raymer
**Date**: November 2025
**Status**: Active Exploration Template
## Purpose
This document provides an **executable template** for exploring and documenting the current plugin's alarm/schedule/notification behavior on Android and iOS.
**Use this template to**:
1. Test plugin behavior across different scenarios
2. Document expected vs actual results
3. Identify gaps between current behavior and platform capabilities
4. Generate findings for the Plugin Requirements document
**Reference**: See [Platform Capability Reference](./platform-capability-reference.md) for OS-level facts.
---
## 0. Quick Reference: Platform Capabilities
**Android**: See [Platform Capability Reference - Android Section](./platform-capability-reference.md#2-android-alarm-capability-matrix)
**iOS**: See [Platform Capability Reference - iOS Section](./platform-capability-reference.md#3-ios-notification-capability-matrix)
**Key Differences**:
* Android: Alarms wiped on reboot; must reschedule
* iOS: Notifications persist across reboot automatically
* Android: App code runs when alarm fires
* iOS: App code does NOT run when notification fires (unless user interacts)
---
## 1. Android Exploration
### 1.1 Code-Level Inspection Checklist
**Source Locations**:
- Plugin: `android/src/main/java/com/timesafari/dailynotification/`
- Test App: `test-apps/android-test-app/`
- Manifest: `test-apps/android-test-app/app/src/main/AndroidManifest.xml`
| Task | File/Function | Line | Status | Notes |
| ---- | ------------- | ---- | ------ | ----- |
| Locate main plugin class | `DailyNotificationPlugin.kt` | 1302 | ☐ | `scheduleDailyNotification()` |
| Identify alarm scheduling | `NotifyReceiver.kt` | 92 | ☐ | `scheduleExactNotification()` |
| Check AlarmManager usage | `NotifyReceiver.kt` | 219, 223, 231 | ☐ | `setAlarmClock()`, `setExactAndAllowWhileIdle()`, `setExact()` |
| Check WorkManager usage | `FetchWorker.kt` | 31 | ☐ | `scheduleFetch()` |
| Check notification display | `DailyNotificationWorker.java` | 200+ | ☐ | `displayNotification()` |
| Check boot receiver | `BootReceiver.kt` | 24 | ☐ | `onReceive()` handles `BOOT_COMPLETED` |
| Check persistence | `DailyNotificationPlugin.kt` | 1393+ | ☐ | Room database storage |
| Check exact alarm permission | `DailyNotificationPlugin.kt` | 1309 | ☐ | `canScheduleExactAlarms()` |
| Check manifest permissions | `AndroidManifest.xml` | - | ☐ | `RECEIVE_BOOT_COMPLETED`, `SCHEDULE_EXACT_ALARM` |
### 1.2 Behavior Testing Matrix
#### Test 1: Base Case
| Step | Action | Expected (OS) | Expected (Plugin) | Actual Result | Notes |
| ---- | ------ | ------------- | ------------------ | ------------- | ----- |
| 1 | Schedule alarm 2 minutes in future | - | Alarm scheduled | ☐ | |
| 2 | Leave app in foreground/background | - | - | ☐ | |
| 3 | Wait for trigger time | Alarm fires | Notification displayed | ☐ | |
| 4 | Check logs | - | No errors | ☐ | |
**Code Reference**: `NotifyReceiver.scheduleExactNotification()` line 92
---
#### Test 2: Swipe from Recents
| Step | Action | Expected (OS) | Expected (Plugin) | Actual Result | Notes |
| ---- | ------ | ------------- | ------------------ | ------------- | ----- |
| 1 | Schedule alarm 2-5 minutes in future | - | Alarm scheduled | ☐ | |
| 2 | Swipe app away from recents | - | - | ☐ | |
| 3 | Wait for trigger time | ✅ Alarm fires (OS resurrects process) | ✅ Notification displayed | ☐ | |
| 4 | Check app state on wake | Cold start | App process recreated | ☐ | |
| 5 | Check logs | - | No errors | ☐ | |
**Code Reference**: `NotifyReceiver.scheduleExactNotification()` uses `setAlarmClock()` line 219
**Platform Behavior**: OS-guaranteed (Android AlarmManager)
---
#### Test 3: OS Kill (Memory Pressure)
| Step | Action | Expected (OS) | Expected (Plugin) | Actual Result | Notes |
| ---- | ------ | ------------- | ------------------ | ------------- | ----- |
| 1 | Schedule alarm 2-5 minutes in future | - | Alarm scheduled | ☐ | |
| 2 | Force kill via `adb shell am kill <package>` | - | - | ☐ | |
| 3 | Wait for trigger time | ✅ Alarm fires | ✅ Notification displayed | ☐ | |
| 4 | Check logs | - | No errors | ☐ | |
**Platform Behavior**: OS-guaranteed (Android AlarmManager)
---
#### Test 4: Device Reboot
| Step | Action | Expected (OS) | Expected (Plugin) | Actual Result | Notes |
| ---- | ------ | ------------- | ------------------ | ------------- | ----- |
| 1 | Schedule alarm 10 minutes in future | - | Alarm scheduled | ☐ | |
| 2 | Reboot device | - | - | ☐ | |
| 3 | Do NOT open app | ❌ Alarm does NOT fire | ❌ No notification | ☐ | |
| 4 | Wait past scheduled time | ❌ No automatic firing | ❌ No notification | ☐ | |
| 5 | Open app manually | - | Plugin detects missed alarm | ☐ | |
| 6 | Check missed alarm handling | - | ✅ Missed alarm detected | ☐ | |
| 7 | Check rescheduling | - | ✅ Future alarms rescheduled | ☐ | |
**Code Reference**:
- Boot receiver: `BootReceiver.kt` line 24
- Rescheduling: `BootReceiver.kt` line 38+
**Platform Behavior**: Plugin-guaranteed (must implement boot receiver)
**Expected Plugin Behavior**: Plugin must reschedule from database on boot
---
#### Test 5: Android Force Stop
| Step | Action | Expected (OS) | Expected (Plugin) | Actual Result | Notes |
| ---- | ------ | ------------- | ------------------ | ------------- | ----- |
| 1 | Schedule alarm | - | Alarm scheduled | ☐ | |
| 2 | Go to Settings → Apps → [App] → Force Stop | ❌ All alarms removed | ❌ All alarms removed | ☐ | |
| 3 | Wait for trigger time | ❌ Alarm does NOT fire | ❌ No notification | ☐ | |
| 4 | Open app again | - | Plugin detects missed alarm | ☐ | |
| 5 | Check recovery | - | ✅ Missed alarm detected | ☐ | |
| 6 | Check rescheduling | - | ✅ Future alarms rescheduled | ☐ | |
**Platform Behavior**: Not allowed (Android hard kill)
**Expected Plugin Behavior**: Plugin must detect and recover on app restart
---
#### Test 6: Exact Alarm Permission (Android 12+)
| Step | Action | Expected (OS) | Expected (Plugin) | Actual Result | Notes |
| ---- | ------ | ------------- | ------------------ | ------------- | ----- |
| 1 | Revoke exact alarm permission | - | - | ☐ | |
| 2 | Attempt to schedule alarm | - | Plugin requests permission | ☐ | |
| 3 | Check settings opened | - | ✅ Settings opened | ☐ | |
| 4 | Grant permission | - | - | ☐ | |
| 5 | Schedule alarm | - | ✅ Alarm scheduled | ☐ | |
| 6 | Verify alarm fires | ✅ Alarm fires | ✅ Notification displayed | ☐ | |
**Code Reference**: `DailyNotificationPlugin.kt` line 1309, 1314-1324
---
### 1.3 Persistence Investigation
| Item | Expected | Actual | Code Reference | Notes |
| ---- | -------- | ------ | -------------- | ----- |
| Alarm ID stored | ✅ Yes | ☐ | `DailyNotificationPlugin.kt` line 1393+ | |
| Trigger time stored | ✅ Yes | ☐ | Room database | |
| Repeat rule stored | ✅ Yes | ☐ | Schedule entity | |
| Channel/priority stored | ✅ Yes | ☐ | NotificationContentEntity | |
| Payload stored | ✅ Yes | ☐ | ContentCache | |
| Time created/modified | ✅ Yes | ☐ | Entity timestamps | |
**Storage Location**: Room database (`DailyNotificationDatabase`)
---
### 1.4 Recovery Points Investigation
| Recovery Point | Expected Behavior | Actual Behavior | Code Reference | Notes |
| -------------- | ----------------- | --------------- | -------------- | ----- |
| Boot event | ✅ Reschedule all alarms | ☐ | `BootReceiver.kt` line 24 | |
| App cold start | ✅ Detect missed alarms | ☐ | Check plugin initialization | |
| App warm start | ✅ Verify active alarms | ☐ | Check plugin initialization | |
| Background fetch return | ⚠️ May reschedule | ☐ | `FetchWorker.kt` | |
| User taps notification | ✅ Launch app | ☐ | Notification intent | |
---
## 2. iOS Exploration
### 2.1 Code-Level Inspection Checklist
**Source Locations**:
- Plugin: `ios/Plugin/`
- Test App: `test-apps/ios-test-app/`
- Alternative: Check `ios-2` branch
| Task | File/Function | Line | Status | Notes |
| ---- | ------------- | ---- | ------ | ----- |
| Locate main plugin class | `DailyNotificationPlugin.swift` | 506 | ☐ | `scheduleUserNotification()` |
| Identify notification scheduling | `DailyNotificationScheduler.swift` | 133 | ☐ | `scheduleNotification()` |
| Check UNUserNotificationCenter usage | `DailyNotificationScheduler.swift` | 185 | ☐ | `notificationCenter.add()` |
| Check trigger types | `DailyNotificationScheduler.swift` | 172 | ☐ | `UNCalendarNotificationTrigger` |
| Check BGTaskScheduler usage | `DailyNotificationPlugin.swift` | 495 | ☐ | `scheduleBackgroundFetch()` |
| Check persistence | `DailyNotificationPlugin.swift` | 35 | ☐ | `storage: DailyNotificationStorage?` |
| Check app launch recovery | `DailyNotificationPlugin.swift` | 42 | ☐ | `load()` method |
### 2.2 Behavior Testing Matrix
#### Test 1: Base Case
| Step | Action | Expected (OS) | Expected (Plugin) | Actual Result | Notes |
| ---- | ------ | ------------- | ------------------ | ------------- | ----- |
| 1 | Schedule notification 2-5 minutes in future | - | Notification scheduled | ☐ | |
| 2 | Leave app backgrounded | - | - | ☐ | |
| 3 | Wait for trigger time | ✅ Notification fires | ✅ Notification displayed | ☐ | |
| 4 | Check logs | - | No errors | ☐ | |
**Code Reference**: `DailyNotificationScheduler.scheduleNotification()` line 133
---
#### Test 2: Swipe App Away
| Step | Action | Expected (OS) | Expected (Plugin) | Actual Result | Notes |
| ---- | ------ | ------------- | ------------------ | ------------- | ----- |
| 1 | Schedule notification 2-5 minutes in future | - | Notification scheduled | ☐ | |
| 2 | Swipe app away from app switcher | - | - | ☐ | |
| 3 | Wait for trigger time | ✅ Notification fires (OS handles) | ✅ Notification displayed | ☐ | |
| 4 | Check app state | App terminated | App not running | ☐ | |
**Platform Behavior**: OS-guaranteed (iOS UNUserNotificationCenter)
---
#### Test 3: Device Reboot
| Step | Action | Expected (OS) | Expected (Plugin) | Actual Result | Notes |
| ---- | ------ | ------------- | ------------------ | ------------- | ----- |
| 1 | Schedule notification for future time | - | Notification scheduled | ☐ | |
| 2 | Reboot device | - | - | ☐ | |
| 3 | Do NOT open app | ✅ Notification fires (OS persists) | ✅ Notification displayed | ☐ | |
| 4 | Check notification timing | ✅ On time (±180s tolerance) | ✅ On time | ☐ | |
**Platform Behavior**: OS-guaranteed (iOS persists calendar/time triggers)
**Note**: Only calendar and time-based triggers persist. Location triggers do not.
---
#### Test 4: Hard Termination & Relaunch
| Step | Action | Expected (OS) | Expected (Plugin) | Actual Result | Notes |
| ---- | ------ | ------------- | ------------------ | ------------- | ----- |
| 1 | Schedule repeating notifications | - | Notifications scheduled | ☐ | |
| 2 | Terminate app via Xcode/switcher | - | - | ☐ | |
| 3 | Allow some triggers to occur | ✅ Notifications fire | ✅ Notifications displayed | ☐ | |
| 4 | Reopen app | - | Plugin checks for missed events | ☐ | |
| 5 | Check missed event detection | ⚠️ May detect | ☐ | Plugin-specific |
| 6 | Check state recovery | ⚠️ May recover | ☐ | Plugin-specific |
**Platform Behavior**: OS-guaranteed for notifications; Plugin-guaranteed for missed event detection
---
#### Test 5: Background Execution Limits
| Step | Action | Expected (OS) | Expected (Plugin) | Actual Result | Notes |
| ---- | ------ | ------------- | ------------------ | ------------- | ----- |
| 1 | Schedule BGTaskScheduler task | - | Task scheduled | ☐ | |
| 2 | Wait for system to execute | ⚠️ System-controlled | ⚠️ May not execute | ☐ | |
| 3 | Check execution timing | ⚠️ Not guaranteed | ⚠️ Not guaranteed | ☐ | |
| 4 | Check time budget | ⚠️ ~30 seconds | ⚠️ Limited time | ☐ | |
**Code Reference**: `DailyNotificationPlugin.scheduleBackgroundFetch()` line 495
**Platform Behavior**: System-controlled (not guaranteed)
---
### 2.3 Persistence Investigation
| Item | Expected | Actual | Code Reference | Notes |
| ---- | -------- | ------ | -------------- | ----- |
| Notification ID stored | ✅ Yes (in UNUserNotificationCenter) | ☐ | `UNNotificationRequest` | |
| Plugin-side storage | ⚠️ May not exist | ☐ | `DailyNotificationStorage?` | |
| Trigger time stored | ✅ Yes (in trigger) | ☐ | `UNCalendarNotificationTrigger` | |
| Repeat rule stored | ✅ Yes (in trigger) | ☐ | `repeats: true/false` | |
| Payload stored | ✅ Yes (in userInfo) | ☐ | `notificationContent.userInfo` | |
**Storage Location**:
- Primary: UNUserNotificationCenter (OS-managed)
- Secondary: Plugin storage (if implemented)
---
### 2.4 Recovery Points Investigation
| Recovery Point | Expected Behavior | Actual Behavior | Code Reference | Notes |
| -------------- | ----------------- | --------------- | -------------- | ----- |
| Boot event | ✅ Notifications fire automatically | ☐ | OS handles | |
| App cold start | ⚠️ May detect missed notifications | ☐ | Check `load()` method | |
| App warm start | ⚠️ May verify pending notifications | ☐ | Check plugin initialization | |
| Background fetch | ⚠️ May reschedule | ☐ | `BGTaskScheduler` | |
| User taps notification | ✅ App launched | ☐ | Notification action | |
---
## 3. Cross-Platform Comparison
### 3.1 Observed Behavior Summary
| Scenario | Android (Observed) | iOS (Observed) | Platform Difference |
| -------- | ------------------ | -------------- | ------------------- |
| Swipe/termination | ☐ | ☐ | Both should work |
| Reboot | ☐ | ☐ | iOS auto, Android manual |
| Force stop | ☐ | N/A | Android only |
| App code on trigger | ☐ | ☐ | Android yes, iOS no |
| Background execution | ☐ | ☐ | Android more flexible |
---
## 4. Findings & Gaps
### 4.1 Android Gaps
| Gap | Severity | Description | Recommendation |
| --- | -------- | ----------- | -------------- |
| Boot recovery | ☐ High/Medium/Low | Does plugin reschedule on boot? | Implement if missing |
| Missed alarm detection | ☐ High/Medium/Low | Does plugin detect missed alarms? | Implement if missing |
| Force stop recovery | ☐ High/Medium/Low | Does plugin recover after force stop? | Implement if missing |
| Persistence completeness | ☐ High/Medium/Low | Are all required fields persisted? | Verify and add if missing |
### 4.2 iOS Gaps
| Gap | Severity | Description | Recommendation |
| --- | -------- | ----------- | -------------- |
| Missed notification detection | ☐ High/Medium/Low | Does plugin detect missed notifications? | Implement if missing |
| Plugin-side persistence | ☐ High/Medium/Low | Does plugin persist state separately? | Consider if needed |
| Background task reliability | ☐ High/Medium/Low | Can plugin rely on BGTaskScheduler? | Document limitations |
---
## 5. Deliverables from This Exploration
After completing this exploration, generate:
1. **ALARMS_BEHAVIOR_MATRIX.md** - Completed test results
2. **PLUGIN_ALARM_LIMITATIONS.md** - Documented limitations and gaps
3. **Annotated code pointers** - Code locations with findings
4. **Open Questions / TODOs** - Unresolved issues
---
## Related Documentation
- [Platform Capability Reference](./platform-capability-reference.md) - OS-level facts
- [Plugin Requirements & Implementation](./plugin-requirements-implementation.md) - Requirements based on findings
- [Improve Alarm Directives](./improve-alarm-directives.md) - Improvement directive
---
## Notes for Explorers
* Fill in checkboxes (☐) as you complete each test
* Document actual results in "Actual Result" columns
* Add notes for any unexpected behavior
* Reference code locations when documenting findings
* Update "Findings & Gaps" section as you discover issues
* Use platform capability reference to understand expected OS behavior

View File

@@ -0,0 +1,552 @@
# Plugin Requirements & Implementation Directive
**Author**: Matthew Raymer
**Date**: November 2025
**Status**: Active Requirements - Implementation Guide
## Purpose
This document defines the **rules the plugin must follow** to behave predictably across Android and iOS platforms. It specifies:
* Persistence requirements
* Recovery strategies
* JS/TS API contract and caveats
* Missed alarm handling
* Platform-specific requirements
* Testing requirements
**This document should be updated** after exploration findings are documented.
**Reference**: See [Platform Capability Reference](./platform-capability-reference.md) for OS-level facts.
---
## 1. Core Requirements
### 1.1 Plugin Behavior Guarantees
The plugin **must** guarantee the following behaviors:
| Behavior | Android | iOS | Implementation Required |
| -------- | ------- | --- | ----------------------- |
| Notification fires after swipe/termination | ✅ Yes | ✅ Yes | OS-guaranteed (verify) |
| Notification fires after reboot | ⚠️ Only if rescheduled | ✅ Yes | Android: Boot receiver required |
| Missed alarm detection | ✅ Required | ✅ Required | Both: App launch recovery |
| Force stop recovery | ✅ Required | N/A | Android: App restart recovery |
| Exact timing | ✅ With permission | ⚠️ ±180s tolerance | Android: Permission check |
### 1.2 Plugin Behavior Limitations
The plugin **cannot** guarantee:
| Limitation | Platform | Reason |
| ---------- | -------- | ------ |
| Notification after Force Stop (Android) | Android | OS hard kill |
| App code execution on iOS notification fire | iOS | OS limitation |
| Background execution timing (iOS) | iOS | System-controlled |
| Exact timing (iOS) | iOS | ±180s tolerance |
---
## 2. Persistence Requirements
### 2.1 Required Persistence Items
The plugin **must** persist the following for each scheduled alarm/notification:
| Field | Type | Required | Purpose |
| ----- | ---- | -------- | ------- |
| `alarm_id` | String | ✅ Yes | Unique identifier |
| `trigger_time` | Long/TimeInterval | ✅ Yes | When to fire |
| `repeat_rule` | String/Enum | ✅ Yes | NONE, DAILY, WEEKLY, CUSTOM |
| `channel_id` | String | ✅ Yes | Notification channel (Android) |
| `priority` | String/Int | ✅ Yes | Notification priority |
| `title` | String | ✅ Yes | Notification title |
| `body` | String | ✅ Yes | Notification body |
| `sound_enabled` | Boolean | ✅ Yes | Sound preference |
| `vibration_enabled` | Boolean | ✅ Yes | Vibration preference |
| `payload` | String/JSON | ⚠️ Optional | Additional content |
| `created_at` | Long/TimeInterval | ✅ Yes | Creation timestamp |
| `updated_at` | Long/TimeInterval | ✅ Yes | Last update timestamp |
| `enabled` | Boolean | ✅ Yes | Whether alarm is active |
### 2.2 Storage Implementation
**Android**:
* **Primary**: Room database (`DailyNotificationDatabase`)
* **Location**: `android/src/main/java/com/timesafari/dailynotification/`
* **Entities**: `Schedule`, `NotificationContentEntity`, `ContentCache`
**iOS**:
* **Primary**: UNUserNotificationCenter (OS-managed)
* **Secondary**: Plugin storage (UserDefaults, CoreData, or files)
* **Location**: `ios/Plugin/`
* **Component**: `DailyNotificationStorage?`
### 2.3 Persistence Validation
The plugin **must**:
* Validate persistence on every alarm schedule
* Log persistence failures
* Handle persistence errors gracefully
* Provide recovery mechanism if persistence fails
---
## 3. Recovery Requirements
### 3.1 Required Recovery Points
The plugin **must** implement recovery at the following points:
#### 3.1.1 Boot Event (Android Only)
**Trigger**: `BOOT_COMPLETED` broadcast
**Required Actions**:
1. Load all enabled alarms from persistent storage
2. Reschedule each alarm using AlarmManager
3. Detect missed alarms (trigger_time < now)
4. Generate missed alarm events/notifications
5. Log recovery actions
**Code Reference**: `BootReceiver.kt` line 24
**Implementation Status**: ☐ Implemented / ☐ Missing
---
#### 3.1.2 App Cold Start
**Trigger**: App launched from terminated state
**Required Actions**:
1. Load all enabled alarms from persistent storage
2. Verify active alarms match stored alarms
3. Detect missed alarms (trigger_time < now)
4. Reschedule future alarms
5. Generate missed alarm events/notifications
6. Log recovery actions
**Implementation Status**: ☐ Implemented / ☐ Missing
**Code Location**: Check plugin initialization (`DailyNotificationPlugin.load()` or equivalent)
---
#### 3.1.3 App Warm Start
**Trigger**: App returning from background
**Required Actions**:
1. Verify active alarms are still scheduled
2. Detect missed alarms (trigger_time < now)
3. Reschedule if needed
4. Log recovery actions
**Implementation Status**: ☐ Implemented / ☐ Missing
---
#### 3.1.4 User Taps Notification
**Trigger**: User interaction with notification
**Required Actions**:
1. Launch app (OS handles)
2. Detect if notification was missed
3. Handle notification action
4. Update alarm state if needed
**Implementation Status**: ☐ Implemented / ☐ Missing
---
### 3.2 Missed Alarm Handling
The plugin **must** detect and handle missed alarms:
**Definition**: An alarm is "missed" if:
* `trigger_time < now`
* Alarm was not fired (or firing status unknown)
* Alarm is still enabled
**Required Actions**:
1. **Detect** missed alarms during recovery
2. **Generate** missed alarm event/notification
3. **Reschedule** future occurrences (if repeating)
4. **Log** missed alarm for debugging
5. **Update** alarm state (mark as missed or reschedule)
**Implementation Requirements**:
* Must run on app launch (cold/warm start)
* Must run on boot (Android)
* Must not duplicate missed alarm notifications
* Must handle timezone changes
**Code Location**: To be implemented in recovery logic
---
## 4. JS/TS API Contract
### 4.1 API Guarantees
The plugin **must** document and guarantee the following behaviors to JavaScript/TypeScript developers:
#### 4.1.1 `scheduleDailyNotification(options)`
**Guarantees**:
* ✅ Notification will fire if app is swiped from recents
* ✅ Notification will fire if app is terminated by OS
* ⚠️ Notification will fire after reboot **only if**:
* Android: Boot receiver is registered and working
* iOS: Automatic (OS handles)
* ❌ Notification will **NOT** fire after Android Force Stop until app is opened
* ⚠️ iOS notifications have ±180s timing tolerance
**Caveats**:
* Android requires `SCHEDULE_EXACT_ALARM` permission on Android 12+
* Android requires `RECEIVE_BOOT_COMPLETED` permission for reboot recovery
* iOS requires notification authorization
**Error Codes**:
* `EXACT_ALARM_PERMISSION_REQUIRED` - Android 12+ exact alarm permission needed
* `NOTIFICATIONS_DENIED` - Notification permission denied
* `SCHEDULE_FAILED` - Scheduling failed (check logs)
---
#### 4.1.2 `scheduleDailyReminder(options)`
**Guarantees**:
* Same as `scheduleDailyNotification()` above
* Static reminder (no content dependency)
* Fires even if content fetch fails
---
#### 4.1.3 `getNotificationStatus()`
**Guarantees**:
* Returns current notification status
* Includes pending notifications
* Includes last notification time
* May include missed alarm information
---
### 4.2 API Warnings
The plugin **must** document the following warnings:
**Android**:
* "Notifications will not fire after device reboot unless the app is opened at least once"
* "Force Stop will prevent all notifications until the app is manually opened"
* "Exact alarm permission is required on Android 12+ for precise timing"
**iOS**:
* "Notifications have ±180 seconds timing tolerance"
* "App code does not run when notifications fire (unless user interacts)"
* "Background execution is system-controlled and not guaranteed"
**Cross-Platform**:
* "Missed alarms are detected on app launch, not at trigger time"
* "Repeating alarms must be rescheduled for each occurrence"
---
### 4.3 API Error Handling
The plugin **must**:
* Return clear error messages
* Include error codes for programmatic handling
* Open system settings when permission is needed
* Provide actionable guidance in error messages
**Example Error Response**:
```typescript
{
code: "EXACT_ALARM_PERMISSION_REQUIRED",
message: "Exact alarm permission required. Please grant 'Alarms & reminders' permission in Settings, then try again.",
action: "opened_settings" // or "permission_denied"
}
```
---
## 5. Platform-Specific Requirements
### 5.1 Android Requirements
#### 5.1.1 Permissions
**Required Permissions**:
* `RECEIVE_BOOT_COMPLETED` - Boot receiver
* `SCHEDULE_EXACT_ALARM` - Android 12+ (API 31+) for exact alarms
* `POST_NOTIFICATIONS` - Android 13+ (API 33+) for notifications
**Permission Handling**:
* Check permission before scheduling
* Request permission if not granted
* Open system settings if permission denied
* Provide clear error messages
**Code Reference**: `DailyNotificationPlugin.kt` line 1309
---
#### 5.1.2 Manifest Entries
**Required Manifest Entries**:
```xml
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<receiver
android:name="com.timesafari.dailynotification.BootReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
</intent-filter>
</receiver>
<receiver
android:name="com.timesafari.dailynotification.DailyNotificationReceiver"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="com.timesafari.daily.NOTIFICATION" />
</intent-filter>
</receiver>
```
**Location**: Test app manifest: `test-apps/android-test-app/app/src/main/AndroidManifest.xml`
---
#### 5.1.3 Notification Channels
**Required Channels**:
* `timesafari.daily` - Primary notification channel
* `daily_reminders` - Reminder notifications (if used)
**Channel Configuration**:
* Importance: HIGH (for alarms), DEFAULT (for reminders)
* Sound: Enabled by default
* Vibration: Enabled by default
* Show badge: Enabled
**Code Reference**: `ChannelManager.java` or `NotifyReceiver.kt` line 454
---
#### 5.1.4 Alarm Scheduling
**Required API Usage**:
* `setAlarmClock()` for Android 5.0+ (preferred)
* `setExactAndAllowWhileIdle()` for Android 6.0+ (fallback)
* `setExact()` for older versions (fallback)
**Code Reference**: `NotifyReceiver.kt` line 219, 223, 231
---
### 5.2 iOS Requirements
#### 5.2.1 Permissions
**Required Permissions**:
* Notification authorization (requested at runtime)
**Permission Handling**:
* Request permission before scheduling
* Handle authorization status
* Provide clear error messages
---
#### 5.2.2 Background Tasks
**Required Background Task Identifiers**:
* `com.timesafari.dailynotification.fetch` - Background fetch
* `com.timesafari.dailynotification.notify` - Notification task (if used)
**Background Task Registration**:
* Register in `Info.plist`:
```xml
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>com.timesafari.dailynotification.fetch</string>
</array>
```
**Code Reference**: `DailyNotificationPlugin.swift` line 31-32
---
#### 5.2.3 Notification Scheduling
**Required API Usage**:
* `UNUserNotificationCenter.add()` - Schedule notifications
* `UNCalendarNotificationTrigger` - Calendar-based triggers (preferred)
* `UNTimeIntervalNotificationTrigger` - Time interval triggers
**Code Reference**: `DailyNotificationScheduler.swift` line 185
---
#### 5.2.4 Notification Categories
**Required Categories**:
* `DAILY_NOTIFICATION` - Primary notification category
**Category Configuration**:
* Actions: Configure as needed
* Options: Custom sound, custom actions
**Code Reference**: `DailyNotificationScheduler.swift` line 62+
---
## 6. Testing Requirements
### 6.1 Required Test Scenarios
The plugin **must** be tested for:
**Android**:
* [ ] Base case (alarm fires on time)
* [ ] Swipe from recents
* [ ] OS kill (memory pressure)
* [ ] Device reboot (with and without app launch)
* [ ] Force stop (with app restart)
* [ ] Exact alarm permission (Android 12+)
* [ ] Boot receiver functionality
* [ ] Missed alarm detection
**iOS**:
* [ ] Base case (notification fires on time)
* [ ] Swipe app away
* [ ] Device reboot (without app launch)
* [ ] Hard termination and relaunch
* [ ] Background execution limits
* [ ] Missed notification detection
**Cross-Platform**:
* [ ] Timezone changes
* [ ] Clock adjustments
* [ ] Multiple simultaneous alarms
* [ ] Repeating alarms
* [ ] Alarm cancellation
---
### 6.2 Test Harness Requirements
**Required Test Tools**:
* Real devices (not just emulators)
* ADB commands for Android testing
* Xcode for iOS testing
* Log monitoring tools
**Required Test Documentation**:
* Test results matrix
* Log snippets for failures
* Screenshots/videos for UI issues
* Performance metrics
---
## 7. Versioning Requirements
### 7.1 Breaking Changes
Any change to alarm behavior is **breaking** and requires:
* **MAJOR version bump** (semantic versioning)
* **Migration guide** for existing users
* **Deprecation warnings** (if applicable)
* **Clear changelog entry**
### 7.2 Non-Breaking Changes
Non-breaking changes include:
* Bug fixes
* Performance improvements
* Additional features (backward compatible)
* Documentation updates
---
## 8. Implementation Checklist
### 8.1 Android Implementation
- [ ] Boot receiver registered in manifest
- [ ] Boot receiver reschedules alarms from database
- [ ] Exact alarm permission checked and requested
- [ ] Notification channels created
- [ ] Alarm scheduling uses correct API (`setAlarmClock` preferred)
- [ ] Persistence implemented (Room database)
- [ ] Missed alarm detection on app launch
- [ ] Force stop recovery on app restart
- [ ] Error handling and user guidance
### 8.2 iOS Implementation
- [ ] Notification authorization requested
- [ ] Background tasks registered in Info.plist
- [ ] Notification scheduling uses UNUserNotificationCenter
- [ ] Calendar triggers used (not just time interval)
- [ ] Plugin-side persistence (if needed for missed detection)
- [ ] Missed notification detection on app launch
- [ ] Background task limitations documented
- [ ] Error handling and user guidance
### 8.3 Cross-Platform Implementation
- [ ] JS/TS API contract documented
- [ ] Platform-specific caveats documented
- [ ] Error codes standardized
- [ ] Test scenarios covered
- [ ] Migration guide (if breaking changes)
---
## 9. Open Questions / TODOs
**To be filled after exploration**:
| Question | Platform | Priority | Status |
| -------- | -------- | -------- | ------ |
| Does boot receiver work correctly? | Android | High | ☐ |
| Is missed alarm detection implemented? | Both | High | ☐ |
| Are all required fields persisted? | Both | Medium | ☐ |
| Is force stop recovery implemented? | Android | High | ☐ |
| Does iOS plugin persist state separately? | iOS | Medium | ☐ |
---
## Related Documentation
- [Platform Capability Reference](./platform-capability-reference.md) - OS-level facts
- [Plugin Behavior Exploration Template](./plugin-behavior-exploration-template.md) - Exploration template
- [Improve Alarm Directives](./improve-alarm-directives.md) - Improvement directive
- [Boot Receiver Testing Guide](./boot-receiver-testing-guide.md) - Testing procedures
- [App Startup Recovery Solution](./app-startup-recovery-solution.md) - Recovery mechanisms
---
## Version History
- **v1.0** (November 2025): Initial requirements document
- Persistence requirements
- Recovery requirements
- JS/TS API contract
- Platform-specific requirements
- Testing requirements