Create unified alarm documentation system with strict role separation: - Doc A: Platform capability reference (canonical OS facts) - Doc B: Plugin behavior exploration (executable test harness) - Doc C: Plugin requirements (guarantees, JS/TS contract, traceability) Changes: - Add canonical rule to Doc A preventing platform fact duplication - Convert Doc B to pure executable test spec with scenario tables - Complete Doc C with guarantees matrix, JS/TS API contract, recovery contract, unsupported behaviors, and traceability matrix - Remove implementation details from unified directive - Add compliance milestone tracking and iOS parity gates - Add deprecation banners to legacy platform docs All documents now enforce strict role separation with cross-references to prevent duplication and ensure single source of truth.
24 KiB
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
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 - Requirements this phase implements
- Platform Capability Reference - OS-level facts
- Phase 1 - Prerequisite
- Phase 2 - Prerequisite
- Full Implementation Directive - 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:
-
✅ Boot receiver detects missed alarms
- Alarms with
nextRunAt < currentTimedetected during boot recovery - Detection runs automatically on
BOOT_COMPLETEDintent - Detection completes within 5 seconds (boot receiver timeout)
- Alarms with
-
✅ Missed alarms are marked in database
delivery_statusupdated to'missed'last_delivery_attemptupdated to current time- Status change logged in history table
-
✅ 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
-
✅ Future alarms are rescheduled
- All future alarms (not missed) rescheduled normally
- Existing boot receiver logic enhanced, not replaced
-
✅ 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):
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:
- Device reboots →
BootReceiver.onReceive()called - BootReceiver sets
last_boot_atflag in SharedPreferences - BootReceiver queues
ReactivationManager.performRecovery() - ReactivationManager detects BOOT scenario via
isBootRecovery() - 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
/**
* 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
/**
* 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.
/**
* 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:
- Catch all exceptions - Never propagate exceptions
- Continue processing - One schedule failure doesn't stop recovery
- Log errors - All failures logged with context
- 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:
- Schedule notification for 5 minutes in future
- Verify alarm scheduled:
adb shell dumpsys alarm | grep timesafari - Reboot device:
adb reboot - Wait for boot:
adb wait-for-device && adb shell getprop sys.boot_completed(wait for "1") - Wait 10 minutes (past scheduled time)
- 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:
- Schedule daily notification (cron: "0 9 * * *") for today at 9 AM
- Reboot device
- Wait until 10 AM (past scheduled time)
- Check boot logs
- 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:
- Schedule notification for 1 hour in future
- Reboot device
- Wait for boot
- 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:
- Schedule one-time notification (no cron/clockTime) for 5 minutes in future
- Reboot device
- Wait 10 minutes (past scheduled time)
- Check boot logs
- 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:
- Manually corrupt database (insert invalid schedule)
- Reboot device
- 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 92ScheduleDao.getEnabled()- Line 298NotificationContentDao.getNotificationById()- Line 69
New Code to Create:
handleMissedAlarmOnBoot()- Add to BootReceiverisRepeating()- Add to BootReceiver (or reuse from ReactivationManager)calculateNextOccurrence()- Add to BootReceiver (or reuse from ReactivationManager)
8. Success Criteria Summary
Phase 3 is complete when:
- ✅ Boot receiver detects missed alarms
- ✅ Missed alarms marked in database
- ✅ Next occurrence rescheduled for repeating schedules
- ✅ Future alarms rescheduled normally
- ✅ Boot recovery never crashes
- ✅ All tests pass
Related Documentation
- Phase 1: Cold Start Recovery - Prerequisite
- Phase 2: Force Stop Recovery - Prerequisite
- Full Implementation Directive - Complete scope
- Exploration Findings - 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