chore: synch this plan

This commit is contained in:
Matthew Raymer
2025-11-25 08:04:53 +00:00
parent 6aa9140f67
commit afbc98f7dc
5 changed files with 2629 additions and 12 deletions

View File

@@ -0,0 +1,619 @@
# Android Implementation Directive: Phase 3 - Boot Receiver Missed Alarm Handling
**Author**: Matthew Raymer
**Date**: November 2025
**Status**: Phase 3 - Boot Recovery Enhancement
**Version**: 1.0.0
## Purpose
Phase 3 enhances the **boot receiver** to detect and handle missed alarms during device reboot. This handles alarms that were scheduled before reboot but were missed because alarms are wiped on reboot.
**Prerequisites**: Phase 1 and Phase 2 must be complete.
**Scope**: Boot receiver missed alarm detection and handling.
**Dependencies**: Boot receiver behavior assumes that Phase 1 and Phase 2 definitions of 'missed alarm', 'next occurrence', and `Schedule`/`NotificationContentEntity` semantics are already in place.
**Reference**: See [Phase 1](./android-implementation-directive-phase1.md) and [Phase 2](./android-implementation-directive-phase2.md) for app launch recovery, [Full Implementation Directive](./android-implementation-directive.md) for complete scope.
**Boot vs App Launch Recovery**:
| Scenario | Entry point | Directive | Responsibility |
| -------------------------------- | --------------------------------------- | --------- | ---------------------------------------- |
| App launch after kill/force-stop | `ReactivationManager.performRecovery()` | Phase 12 | Detect & recover missed |
| Device reboot | `BootReceiver``ReactivationManager` | Phase 3 | Queue recovery, ReactivationManager handles |
**User-Facing Behavior**: In Phase 3, missed alarms are **recorded** and **rescheduled**, but not yet surfaced to the user with explicit "you missed this" UX (that's a future concern).
---
## 1. Acceptance Criteria
### 1.1 Definition of Done
**Phase 3 is complete when:**
1.**Boot receiver detects missed alarms**
- Alarms with `nextRunAt < currentTime` detected during boot recovery
- Detection runs automatically on `BOOT_COMPLETED` intent
- Detection completes within 5 seconds (boot receiver timeout)
2.**Missed alarms are marked in database**
- `delivery_status` updated to `'missed'`
- `last_delivery_attempt` updated to current time
- Status change logged in history table
3.**Next occurrence is rescheduled for repeating schedules**
- Repeating schedules calculate next occurrence after missed time
- Next occurrence scheduled via AlarmManager
- Non-repeating schedules not rescheduled
4.**Future alarms are rescheduled**
- All future alarms (not missed) rescheduled normally
- Existing boot receiver logic enhanced, not replaced
5.**Boot recovery never crashes**
- All exceptions caught and logged
- Database errors don't propagate
- Invalid data handled gracefully
### 1.2 Success Metrics
| Metric | Target | Measurement |
|--------|--------|-------------|
| Boot receiver execution time | < 5 seconds | Log timestamp difference |
| Missed detection accuracy | 100% | Manual verification via logs |
| Next occurrence calculation accuracy | 100% | Verify scheduled time matches expected |
| Recovery success rate | > 95% | History table outcome field |
| Crash rate | 0% | No exceptions propagate |
### 1.3 Out of Scope (Phase 3)
- ❌ Warm start optimization (future phase)
- ❌ Callback event emission (future phase)
- ❌ User notification of missed alarms (future phase)
- ❌ Boot receiver performance optimization (future phase)
---
## 2. Implementation: BootReceiver Enhancement
### 2.1 Canonical Source of Truth
**⚠️ CRITICAL CORRECTION**: BootReceiver must **NOT** implement recovery logic directly. It must **only queue** ReactivationManager.performRecovery() with a BOOT flag.
**ReactivationManager.kt** is the **only** file allowed to:
- Perform scenario detection
- Initiate recovery logic
- Branch execution per phase
### 2.2 Update BootReceiver
**File**: `android/src/main/java/com/timesafari/dailynotification/BootReceiver.kt`
**Location**: `onReceive()` method
### 2.3 Corrected Implementation
**Corrected Code** (BootReceiver only queues recovery):
```kotlin
package com.timesafari.dailynotification
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.util.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
/**
* Boot recovery receiver to trigger recovery after device reboot
* Phase 3: Only queues ReactivationManager, does not implement recovery directly
*
* @author Matthew Raymer
* @version 2.0.0 - Corrected to queue ReactivationManager only
*/
class BootReceiver : BroadcastReceiver() {
companion object {
private const val TAG = "DNP-BOOT"
private const val PREFS_NAME = "dailynotification_recovery"
private const val KEY_LAST_BOOT_AT = "last_boot_at"
}
override fun onReceive(context: Context, intent: Intent?) {
if (intent?.action == Intent.ACTION_BOOT_COMPLETED) {
Log.i(TAG, "Boot completed, queuing ReactivationManager recovery")
// Set boot flag in SharedPreferences
// ReactivationManager will detect this and handle recovery
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
prefs.edit().putLong(KEY_LAST_BOOT_AT, System.currentTimeMillis()).apply()
// Queue ReactivationManager recovery
// Recovery will run when app launches or can be triggered immediately
CoroutineScope(Dispatchers.IO).launch {
try {
val reactivationManager = ReactivationManager(context)
reactivationManager.performRecovery()
Log.i(TAG, "Boot recovery queued to ReactivationManager")
} catch (e: Exception) {
Log.e(TAG, "Failed to queue boot recovery", e)
}
}
}
}
}
```
**⚠️ REMOVED**: All direct rescheduling logic from BootReceiver. Recovery is now handled entirely by ReactivationManager.
### 2.4 How Boot Recovery Works
**Flow**:
1. Device reboots → `BootReceiver.onReceive()` called
2. BootReceiver sets `last_boot_at` flag in SharedPreferences
3. BootReceiver queues `ReactivationManager.performRecovery()`
4. ReactivationManager detects BOOT scenario via `isBootRecovery()`
5. ReactivationManager handles recovery (same logic as force stop - all alarms wiped)
**Key Points**:
- BootReceiver **never** implements recovery directly
- All recovery logic is in ReactivationManager
- Boot recovery uses same recovery path as force stop (all alarms wiped on reboot)
---
## 3. Data Integrity Checks
```kotlin
/**
* Reschedule notifications after device reboot
* Phase 3: Adds missed alarm detection and handling
*
* @param context Application context
*/
private suspend fun rescheduleNotifications(context: Context) {
val db = DailyNotificationDatabase.getDatabase(context)
val enabledSchedules = db.scheduleDao().getEnabled()
val currentTime = System.currentTimeMillis()
Log.i(TAG, "Boot recovery: Found ${enabledSchedules.size} enabled schedules to reschedule")
var futureRescheduled = 0
var missedDetected = 0
var missedRescheduled = 0
var errors = 0
enabledSchedules.forEach { schedule ->
try {
when (schedule.kind) {
"fetch" -> {
// Reschedule WorkManager fetch (unchanged)
val config = ContentFetchConfig(
enabled = schedule.enabled,
schedule = schedule.cron ?: schedule.clockTime ?: "0 9 * * *",
url = null,
timeout = 30000,
retryAttempts = 3,
retryDelay = 1000,
callbacks = CallbackConfig()
)
FetchWorker.scheduleFetch(context, config)
Log.i(TAG, "Rescheduled fetch for schedule: ${schedule.id}")
futureRescheduled++
}
"notify" -> {
// Phase 3: Handle both past and future alarms
val nextRunTime = calculateNextRunTime(schedule)
if (nextRunTime > currentTime) {
// Future alarm - reschedule normally
val config = UserNotificationConfig(
enabled = schedule.enabled,
schedule = schedule.cron ?: schedule.clockTime ?: "0 9 * * *",
title = "Daily Notification",
body = "Your daily update is ready",
sound = true,
vibration = true,
priority = "normal"
)
NotifyReceiver.scheduleExactNotification(context, nextRunTime, config)
Log.i(TAG, "Rescheduled future notification: ${schedule.id} for $nextRunTime")
futureRescheduled++
} else {
// Past alarm - was missed during reboot
missedDetected++
Log.i(TAG, "Missed alarm detected on boot: ${schedule.id} scheduled for $nextRunTime")
// Mark as missed
handleMissedAlarmOnBoot(context, schedule, nextRunTime, db)
// Reschedule next occurrence if repeating
if (isRepeating(schedule)) {
try {
val nextOccurrence = calculateNextOccurrence(schedule, currentTime)
val config = UserNotificationConfig(
enabled = schedule.enabled,
schedule = schedule.cron ?: schedule.clockTime ?: "0 9 * * *",
title = "Daily Notification",
body = "Your daily update is ready",
sound = true,
vibration = true,
priority = "normal"
)
NotifyReceiver.scheduleExactNotification(context, nextOccurrence, config)
Log.i(TAG, "Rescheduled next occurrence for missed alarm: ${schedule.id} for $nextOccurrence")
missedRescheduled++
} catch (e: Exception) {
errors++
Log.e(TAG, "Failed to reschedule next occurrence: ${schedule.id}", e)
}
}
}
}
else -> {
Log.w(TAG, "Unknown schedule kind: ${schedule.kind}")
}
}
} catch (e: Exception) {
errors++
Log.e(TAG, "Failed to reschedule ${schedule.kind} for ${schedule.id}", e)
}
}
// Record boot recovery in history
try {
db.historyDao().insert(
History(
refId = "boot_recovery_${System.currentTimeMillis()}",
kind = "boot_recovery",
occurredAt = System.currentTimeMillis(),
outcome = if (errors == 0) "success" else "partial",
diagJson = """
{
"schedules_rescheduled": $futureRescheduled,
"missed_detected": $missedDetected,
"missed_rescheduled": $missedRescheduled,
"errors": $errors
}
""".trimIndent()
)
)
} catch (e: Exception) {
Log.e(TAG, "Failed to record boot recovery history", e)
// Don't fail boot recovery if history recording fails
}
Log.i(TAG, "Boot recovery complete: $futureRescheduled future, $missedDetected missed, $missedRescheduled next occurrences, $errors errors")
}
```
**Note**: All data integrity checks are handled by ReactivationManager (Phase 2). BootReceiver does not perform any data operations directly.
### 3.1 Missed Alarm Detection Validation
```kotlin
/**
* Handle missed alarm detected during boot recovery
* Phase 3: Marks missed alarm in database
*
* @param context Application context
* @param schedule Schedule that was missed
* @param scheduledTime When the alarm was scheduled
* @param db Database instance
*/
private suspend fun handleMissedAlarmOnBoot(
context: Context,
schedule: Schedule,
scheduledTime: Long,
db: DailyNotificationDatabase
) {
try {
// Data integrity check
if (schedule.id.isBlank()) {
Log.w(TAG, "Skipping invalid schedule: empty ID")
return
}
// Try to find existing NotificationContentEntity
val notificationId = schedule.id
val existingNotification = db.notificationContentDao().getNotificationById(notificationId)
if (existingNotification != null) {
// Update existing notification
existingNotification.deliveryStatus = "missed"
existingNotification.lastDeliveryAttempt = System.currentTimeMillis()
existingNotification.deliveryAttempts = (existingNotification.deliveryAttempts ?: 0) + 1
db.notificationContentDao().updateNotification(existingNotification)
Log.d(TAG, "Marked missed notification on boot: $notificationId")
} else {
// No NotificationContentEntity found - this is okay for boot recovery
// The schedule exists but content may not have been fetched yet
Log.d(TAG, "No NotificationContentEntity found for missed schedule: $notificationId (expected for boot recovery)")
}
// Record missed alarm in history
try {
db.historyDao().insert(
History(
refId = "missed_boot_${schedule.id}_${System.currentTimeMillis()}",
kind = "missed_alarm",
occurredAt = System.currentTimeMillis(),
outcome = "missed",
diagJson = """
{
"schedule_id": "${schedule.id}",
"scheduled_time": $scheduledTime,
"detected_at": ${System.currentTimeMillis()},
"scenario": "boot_recovery"
}
""".trimIndent()
)
)
} catch (e: Exception) {
Log.w(TAG, "Failed to record missed alarm history", e)
// Don't fail if history recording fails
}
} catch (e: Exception) {
Log.e(TAG, "Failed to handle missed alarm on boot: ${schedule.id}", e)
// Don't throw - continue with boot recovery
}
}
```
### 2.5 Helper Methods
**⚠️ Implementation Consistency**: These helpers must match the implementation used in `ReactivationManager` (Phase 2). Treat ReactivationManager as canonical and keep these in sync.
```kotlin
/**
* Check if schedule is repeating
*
* **Implementation Note**: Must match `isRepeating()` in ReactivationManager (Phase 2).
* This is a duplication for now; treat ReactivationManager as canonical.
*
* @param schedule Schedule to check
* @return true if repeating, false if one-time
*/
private fun isRepeating(schedule: Schedule): Boolean {
// Schedules with cron or clockTime are repeating
return schedule.cron != null || schedule.clockTime != null
}
/**
* Calculate next occurrence for repeating schedule
*
* **Implementation Note**: Must match `calculateNextOccurrence()` in ReactivationManager (Phase 2).
* This is a duplication for now; treat ReactivationManager as canonical.
*
* @param schedule Schedule to calculate for
* @param fromTime Calculate next occurrence after this time
* @return Next occurrence time in milliseconds
*/
private fun calculateNextOccurrence(schedule: Schedule, fromTime: Long): Long {
// TODO: Implement proper calculation based on cron/clockTime
// For now, simplified: daily schedules add 24 hours
// This should match the logic in ReactivationManager (Phase 2)
return when {
schedule.cron != null -> {
// Parse cron and calculate next occurrence
// For now, simplified: daily schedules
fromTime + (24 * 60 * 60 * 1000L)
}
schedule.clockTime != null -> {
// Parse HH:mm and calculate next occurrence
// For now, simplified: daily schedules
fromTime + (24 * 60 * 60 * 1000L)
}
else -> {
// Not repeating
fromTime
}
}
}
```
---
## 3. Data Integrity Checks
### 3.1 Missed Alarm Detection Validation
**Boot Flag Rules**:
- ✅ BootReceiver sets flag immediately on BOOT_COMPLETED
- ✅ Flag is valid for 60 seconds after boot
- ✅ ReactivationManager clears flag after reading
- ✅ Stale flags are ignored (prevents false positives)
**Edge Cases**:
- ✅ Multiple boot broadcasts: Flag is overwritten (last one wins)
- ✅ App not launched after boot: Flag expires after 60 seconds
- ✅ SharedPreferences errors: Log error, recovery continues
---
## 4. Rollback Safety
### 4.1 No-Crash Guarantee
**All boot recovery operations must:**
1. **Catch all exceptions** - Never propagate exceptions
2. **Continue processing** - One schedule failure doesn't stop recovery
3. **Log errors** - All failures logged with context
4. **Timeout protection** - Boot receiver has 10-second timeout (Android limit)
### 4.2 Error Handling Strategy
| Error Type | Handling | Log Level |
|------------|----------|-----------|
| Schedule query failure | Return empty list, log error | ERROR |
| Invalid schedule data | Skip schedule, continue | WARN |
| Missed alarm marking failure | Log error, continue | ERROR |
| Next occurrence calculation failure | Log error, don't reschedule | ERROR |
| Alarm reschedule failure | Log error, continue | ERROR |
| History recording failure | Log warning, don't fail | WARN |
---
## 5. Testing Requirements
### 5.1 Test 1: Boot Recovery Missed Detection
**Purpose**: Verify boot receiver detects missed alarms.
**Steps**:
1. Schedule notification for 5 minutes in future
2. Verify alarm scheduled: `adb shell dumpsys alarm | grep timesafari`
3. Reboot device: `adb reboot`
4. Wait for boot: `adb wait-for-device && adb shell getprop sys.boot_completed` (wait for "1")
5. Wait 10 minutes (past scheduled time)
6. Check boot logs: `adb logcat -d | grep DNP-BOOT`
**Expected**:
- ✅ Log shows "Boot recovery: Found X enabled schedules"
- ✅ Log shows "Missed alarm detected on boot: <id>"
- ✅ Database shows `delivery_status = 'missed'`
**Pass Criteria**: Missed alarm detected during boot recovery.
### 5.2 Test 2: Next Occurrence Rescheduling
**Purpose**: Verify repeating schedules calculate next occurrence correctly.
**Steps**:
1. Schedule daily notification (cron: "0 9 * * *") for today at 9 AM
2. Reboot device
3. Wait until 10 AM (past scheduled time)
4. Check boot logs
5. Verify next occurrence scheduled: `adb shell dumpsys alarm | grep timesafari`
**Expected**:
- ✅ Log shows "Missed alarm detected on boot"
- ✅ Log shows "Rescheduled next occurrence for missed alarm"
- ✅ AlarmManager shows alarm scheduled for tomorrow 9 AM
**Pass Criteria**: Next occurrence correctly calculated and scheduled.
### 5.3 Test 3: Future Alarm Rescheduling
**Purpose**: Verify future alarms are still rescheduled normally.
**Steps**:
1. Schedule notification for 1 hour in future
2. Reboot device
3. Wait for boot
4. Verify alarm rescheduled: `adb shell dumpsys alarm | grep timesafari`
**Expected**:
- ✅ Log shows "Rescheduled future notification"
- ✅ AlarmManager shows alarm scheduled for original time
- ✅ No missed alarm detection for future alarms
**Pass Criteria**: Future alarms rescheduled normally.
### 5.4 Test 4: Non-Repeating Schedule Handling
**Purpose**: Verify non-repeating schedules don't reschedule next occurrence.
**Steps**:
1. Schedule one-time notification (no cron/clockTime) for 5 minutes in future
2. Reboot device
3. Wait 10 minutes (past scheduled time)
4. Check boot logs
5. Verify no next occurrence scheduled
**Expected**:
- ✅ Log shows "Missed alarm detected on boot"
- ✅ Log does NOT show "Rescheduled next occurrence"
- ✅ AlarmManager does NOT show new alarm
**Pass Criteria**: Non-repeating schedules don't reschedule.
### 5.5 Test 5: Boot Recovery Error Handling
**Purpose**: Verify boot recovery handles errors gracefully.
**Steps**:
1. Manually corrupt database (insert invalid schedule)
2. Reboot device
3. Check boot logs
**Expected**:
- ✅ Invalid schedule skipped with warning
- ✅ Boot recovery continues normally
- ✅ Valid schedules still recovered
- ✅ No crash or exception
**Pass Criteria**: Errors handled gracefully, recovery continues.
---
## 6. Implementation Checklist
- [ ] Update `BootReceiver.onReceive()` to set boot flag
- [ ] Update `BootReceiver.onReceive()` to queue ReactivationManager
- [ ] Remove all direct rescheduling logic from BootReceiver
- [ ] Verify ReactivationManager detects BOOT scenario correctly
- [ ] Update history recording to include missed alarm counts
- [ ] Add data integrity checks
- [ ] Add error handling
- [ ] Test boot recovery missed detection
- [ ] Test next occurrence rescheduling
- [ ] Test future alarm rescheduling
- [ ] Test non-repeating schedule handling
- [ ] Test error handling
- [ ] Verify no duplicate alarms
---
## 7. Code References
**Existing Code to Reuse**:
- `BootReceiver.rescheduleNotifications()` - Line 38 (update existing)
- `BootReceiver.calculateNextRunTime()` - Line 103 (already exists)
- `NotifyReceiver.scheduleExactNotification()` - Line 92
- `ScheduleDao.getEnabled()` - Line 298
- `NotificationContentDao.getNotificationById()` - Line 69
**New Code to Create**:
- `handleMissedAlarmOnBoot()` - Add to BootReceiver
- `isRepeating()` - Add to BootReceiver (or reuse from ReactivationManager)
- `calculateNextOccurrence()` - Add to BootReceiver (or reuse from ReactivationManager)
---
## 8. Success Criteria Summary
**Phase 3 is complete when:**
1. ✅ Boot receiver detects missed alarms
2. ✅ Missed alarms marked in database
3. ✅ Next occurrence rescheduled for repeating schedules
4. ✅ Future alarms rescheduled normally
5. ✅ Boot recovery never crashes
6. ✅ All tests pass
---
## Related Documentation
- [Phase 1: Cold Start Recovery](./android-implementation-directive-phase1.md) - Prerequisite
- [Phase 2: Force Stop Recovery](./android-implementation-directive-phase2.md) - Prerequisite
- [Full Implementation Directive](./android-implementation-directive.md) - Complete scope
- [Exploration Findings](./exploration-findings-initial.md) - Gap analysis
---
## Notes
- **Prerequisites**: Phase 1 and Phase 2 must be complete before starting Phase 3
- **Boot receiver timeout**: Android limits boot receiver execution to 10 seconds
- **Comprehensive recovery**: Boot recovery handles both missed and future alarms
- **Safety first**: All recovery operations are non-blocking and non-fatal
- **Code reuse**: Consider extracting helper methods to shared utility class