# 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 **Last Synced With Plugin Version**: v1.1.0 **Implements**: [Plugin Requirements §3.1.1 - Boot Event](./alarms/03-plugin-requirements.md#311-boot-event-android-only) ## 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**: - [Plugin Requirements](./alarms/03-plugin-requirements.md) - Requirements this phase implements - [Platform Capability Reference](./alarms/01-platform-capability-reference.md) - OS-level facts - [Phase 1](./android-implementation-directive-phase1.md) - Prerequisite - [Phase 2](./android-implementation-directive-phase2.md) - Prerequisite - [Full Implementation Directive](./android-implementation-directive.md) - Complete scope **Boot vs App Launch Recovery**: | Scenario | Entry point | Directive | Responsibility | | -------------------------------- | --------------------------------------- | --------- | ---------------------------------------- | | App launch after kill/force-stop | `ReactivationManager.performRecovery()` | Phase 1–2 | 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: " - ✅ 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