44 KiB
Android Implementation Directive: App Launch Recovery & Missed Alarm Detection
Author: Matthew Raymer
Date: November 2025
Status: Active Implementation Directive - Android Only
Purpose
This directive provides descriptive overview and integration guidance for Android-specific gaps identified in the exploration:
- App Launch Recovery (cold/warm/force-stop)
- Missed Alarm Detection
- Force Stop Detection
- Boot Receiver Missed Alarm Handling
⚠️ CRITICAL: This document is descriptive and integrative. The normative implementation instructions are in the Phase 1–3 directives below. If any code or behavior in this file conflicts with a Phase directive, the Phase directive wins.
Reference: See Exploration Findings for gap analysis.
⚠️ IMPORTANT: For implementation, use the phase-specific directives (these are the canonical source of truth):
-
Phase 1: Cold Start Recovery - Minimal viable recovery
- Explicit acceptance criteria, rollback safety, data integrity checks
- Start here for fastest implementation
-
Phase 2: Force Stop Detection & Recovery - Comprehensive force stop handling
- Prerequisite: Phase 1 complete
-
Phase 3: Boot Receiver Missed Alarm Handling - Boot recovery enhancement
- Prerequisites: Phase 1 and Phase 2 complete
1. Implementation Overview
1.1 What Needs to Be Implemented
| Feature | Status | Priority | Location |
|---|---|---|---|
| App Launch Recovery | ❌ Missing | High | DailyNotificationPlugin.kt - load() method |
| Missed Alarm Detection | ⚠️ Partial | High | DailyNotificationPlugin.kt - new method |
| Force Stop Detection | ❌ Missing | High | DailyNotificationPlugin.kt - recovery logic |
| Boot Receiver Missed Alarms | ⚠️ Partial | Medium | BootReceiver.kt - rescheduleNotifications() |
1.2 Implementation Strategy
Phase 1 – Cold start recovery only
- Missed notification detection + future alarm verification
- No force-stop detection, no boot handling
- See Phase 1 directive for implementation
Phase 2 – Force stop detection & full recovery
- Force stop detection via AlarmManager state comparison
- Comprehensive recovery of all schedules (notify + fetch)
- Past alarms marked as missed, future alarms rescheduled
- See Phase 2 directive for implementation
Phase 3 – Boot receiver missed alarm detection & rescheduling
- Boot receiver detects missed alarms during device reboot
- Next occurrence rescheduled for repeating schedules
- See Phase 3 directive for implementation
2. Implementation: ReactivationManager
⚠️ Illustrative only – See Phase 1 and Phase 2 directives for canonical implementation.
ReactivationManager Responsibilities by Phase:
| Phase | Responsibilities |
|---|---|
| 1 | Cold start only (missed detection + verify/reschedule future) |
| 2 | Adds force stop detection & recovery |
| 3 | Warm start optimizations (future) |
For implementation details, see:
2.1 Create New File
File: android/src/main/java/com/timesafari/dailynotification/ReactivationManager.kt
Purpose: Centralized recovery logic for app launch scenarios
2.2 Class Structure
⚠️ Illustrative only – See Phase 1 for canonical implementation.
package com.timesafari.dailynotification
import android.app.AlarmManager
import android.content.Context
import android.os.Build
import android.util.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
/**
* Manages recovery of alarms and notifications on app launch
* Handles cold start, warm start, and force stop recovery scenarios
*
* @author Matthew Raymer
* @version 1.0.0
*/
class ReactivationManager(private val context: Context) {
companion object {
private const val TAG = "DNP-REACTIVATION"
}
/**
* Perform recovery on app launch
* Detects scenario (cold/warm/force-stop) and handles accordingly
*/
fun performRecovery() {
CoroutineScope(Dispatchers.IO).launch {
try {
Log.i(TAG, "Starting app launch recovery")
// Step 1: Detect scenario
val scenario = detectScenario()
Log.i(TAG, "Detected scenario: $scenario")
// Step 2: Handle based on scenario
when (scenario) {
RecoveryScenario.FORCE_STOP -> handleForceStopRecovery()
RecoveryScenario.COLD_START -> handleColdStartRecovery()
RecoveryScenario.WARM_START -> handleWarmStartRecovery()
}
Log.i(TAG, "App launch recovery completed")
} catch (e: Exception) {
Log.e(TAG, "Error during app launch recovery", e)
}
}
}
// ... implementation methods below ...
}
2.3 Scenario Detection
⚠️ Illustrative only – See Phase 2 for canonical scenario detection implementation.
/**
* Detect recovery scenario based on AlarmManager state vs database
*/
private suspend fun detectScenario(): RecoveryScenario {
val db = DailyNotificationDatabase.getDatabase(context)
val dbSchedules = db.scheduleDao().getEnabled()
if (dbSchedules.isEmpty()) {
// No schedules in database - normal first launch
return RecoveryScenario.COLD_START
}
// Check AlarmManager for active alarms
val activeAlarmCount = getActiveAlarmCount()
// Force Stop Detection: DB has schedules but AlarmManager has zero
if (dbSchedules.isNotEmpty() && activeAlarmCount == 0) {
return RecoveryScenario.FORCE_STOP
}
// Check if this is warm start (app was in background)
// For now, treat as cold start - can be enhanced later
return RecoveryScenario.COLD_START
}
/**
* Get count of active alarms in AlarmManager
*/
private fun getActiveAlarmCount(): Int {
return try {
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// Use nextAlarmClock as indicator (not perfect, but best available)
val nextAlarm = alarmManager.nextAlarmClock
if (nextAlarm != null) {
// At least one alarm exists
// Note: This doesn't give exact count, but indicates alarms exist
return 1 // Assume at least one
} else {
return 0
}
} else {
// Pre-Lollipop: Cannot query AlarmManager directly
// Assume alarms exist if we can't check
return 1
}
} catch (e: Exception) {
Log.e(TAG, "Error checking active alarm count", e)
return 0
}
}
enum class RecoveryScenario {
COLD_START,
WARM_START,
FORCE_STOP
}
2.4 Force Stop Recovery
⚠️ Illustrative only – See Phase 2 for canonical force stop recovery implementation.
/**
* Handle force stop recovery
* All alarms were cancelled, need to restore everything
*/
private suspend fun handleForceStopRecovery() {
Log.i(TAG, "Handling force stop recovery")
val db = DailyNotificationDatabase.getDatabase(context)
val dbSchedules = db.scheduleDao().getEnabled()
val currentTime = System.currentTimeMillis()
var missedCount = 0
var rescheduledCount = 0
dbSchedules.forEach { schedule ->
try {
when (schedule.kind) {
"notify" -> {
val nextRunTime = calculateNextRunTime(schedule)
if (nextRunTime < currentTime) {
// Past alarm - was missed during force stop
missedCount++
handleMissedAlarm(schedule, nextRunTime)
// Reschedule next occurrence if repeating
if (isRepeating(schedule)) {
val nextOccurrence = calculateNextOccurrence(schedule, currentTime)
rescheduleAlarm(schedule, nextOccurrence)
rescheduledCount++
}
} else {
// Future alarm - reschedule immediately
rescheduleAlarm(schedule, nextRunTime)
rescheduledCount++
}
}
"fetch" -> {
// Reschedule fetch work
rescheduleFetch(schedule)
rescheduledCount++
}
}
} catch (e: Exception) {
Log.e(TAG, "Error recovering schedule ${schedule.id}", e)
}
}
// Record recovery in history
recordRecoveryHistory(db, "force_stop", missedCount, rescheduledCount)
Log.i(TAG, "Force stop recovery complete: $missedCount missed, $rescheduledCount rescheduled")
}
2.5 Cold Start Recovery
⚠️ Illustrative only – See Phase 1 for canonical cold start recovery implementation.
/**
* Handle cold start recovery
* Check for missed alarms and reschedule future ones
*/
private suspend fun handleColdStartRecovery() {
Log.i(TAG, "Handling cold start recovery")
val db = DailyNotificationDatabase.getDatabase(context)
val currentTime = System.currentTimeMillis()
// Step 1: Detect missed alarms
val missedNotifications = db.notificationContentDao()
.getNotificationsReadyForDelivery(currentTime)
var missedCount = 0
missedNotifications.forEach { notification ->
try {
handleMissedNotification(notification)
missedCount++
} catch (e: Exception) {
Log.e(TAG, "Error handling missed notification ${notification.id}", e)
}
}
// Step 2: Reschedule future alarms from database
val dbSchedules = db.scheduleDao().getEnabled()
var rescheduledCount = 0
dbSchedules.forEach { schedule ->
try {
val nextRunTime = calculateNextRunTime(schedule)
if (nextRunTime >= currentTime) {
// Future alarm - verify it's scheduled
if (!isAlarmScheduled(schedule, nextRunTime)) {
rescheduleAlarm(schedule, nextRunTime)
rescheduledCount++
}
}
} catch (e: Exception) {
Log.e(TAG, "Error rescheduling schedule ${schedule.id}", e)
}
}
// Record recovery in history
recordRecoveryHistory(db, "cold_start", missedCount, rescheduledCount)
Log.i(TAG, "Cold start recovery complete: $missedCount missed, $rescheduledCount rescheduled")
}
2.6 Warm Start Recovery
/**
* Handle warm start recovery
* Verify active alarms and check for missed ones
*/
private suspend fun handleWarmStartRecovery() {
Log.i(TAG, "Handling warm start recovery")
// Similar to cold start, but lighter weight
// Just verify alarms are still scheduled
val db = DailyNotificationDatabase.getDatabase(context)
val currentTime = System.currentTimeMillis()
// Check for missed alarms (same as cold start)
val missedNotifications = db.notificationContentDao()
.getNotificationsReadyForDelivery(currentTime)
var missedCount = 0
missedNotifications.forEach { notification ->
try {
handleMissedNotification(notification)
missedCount++
} catch (e: Exception) {
Log.e(TAG, "Error handling missed notification ${notification.id}", e)
}
}
// Verify active alarms (lighter check than cold start)
val dbSchedules = db.scheduleDao().getEnabled()
var verifiedCount = 0
dbSchedules.forEach { schedule ->
try {
val nextRunTime = calculateNextRunTime(schedule)
if (nextRunTime >= currentTime && isAlarmScheduled(schedule, nextRunTime)) {
verifiedCount++
}
} catch (e: Exception) {
Log.e(TAG, "Error verifying schedule ${schedule.id}", e)
}
}
Log.i(TAG, "Warm start recovery complete: $missedCount missed, $verifiedCount verified")
}
2.7 Helper Methods
/**
* Handle a missed alarm/notification
*/
private suspend fun handleMissedAlarm(schedule: Schedule, scheduledTime: Long) {
val db = DailyNotificationDatabase.getDatabase(context)
// Update delivery status
// Note: This assumes NotificationContentEntity exists for this schedule
// May need to create if it doesn't exist
// Generate missed alarm event
// Option 1: Fire callback
fireMissedAlarmCallback(schedule, scheduledTime)
// Option 2: Generate missed alarm notification
// generateMissedAlarmNotification(schedule, scheduledTime)
Log.i(TAG, "Handled missed alarm: ${schedule.id} scheduled for $scheduledTime")
}
/**
* Handle a missed notification (from NotificationContentEntity)
*/
private suspend fun handleMissedNotification(notification: NotificationContentEntity) {
val db = DailyNotificationDatabase.getDatabase(context)
// Update delivery status
notification.deliveryStatus = "missed"
db.notificationContentDao().update(notification)
// Generate missed notification event
fireMissedNotificationCallback(notification)
Log.i(TAG, "Handled missed notification: ${notification.id}")
}
/**
* Reschedule an alarm
*/
private suspend fun rescheduleAlarm(schedule: Schedule, nextRunTime: Long) {
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)
// Update schedule in database
schedule.nextRunAt = nextRunTime
val db = DailyNotificationDatabase.getDatabase(context)
db.scheduleDao().update(schedule)
Log.i(TAG, "Rescheduled alarm: ${schedule.id} for $nextRunTime")
}
/**
* Check if alarm is scheduled in AlarmManager
*/
private fun isAlarmScheduled(schedule: Schedule, triggerTime: Long): Boolean {
return NotifyReceiver.isAlarmScheduled(context, triggerTime)
}
/**
* Calculate next run time from schedule
*/
private fun calculateNextRunTime(schedule: Schedule): Long {
// Use existing logic from BootReceiver or create new
// For now, simplified version
val now = System.currentTimeMillis()
return when {
schedule.cron != null -> {
// Parse cron and calculate next run
// For now, return next day at 9 AM
now + (24 * 60 * 60 * 1000L)
}
schedule.clockTime != null -> {
// Parse HH:mm and calculate next run
// For now, return next day at specified time
now + (24 * 60 * 60 * 1000L)
}
schedule.nextRunAt != null -> {
schedule.nextRunAt
}
else -> {
// Default to next day at 9 AM
now + (24 * 60 * 60 * 1000L)
}
}
}
/**
* Check if schedule is repeating
*/
private fun isRepeating(schedule: Schedule): Boolean {
// Check cron or clockTime pattern to determine if repeating
// For now, assume daily schedules are repeating
return schedule.cron != null || schedule.clockTime != null
}
/**
* Calculate next occurrence for repeating schedule
*/
private fun calculateNextOccurrence(schedule: Schedule, fromTime: Long): Long {
// Calculate next occurrence after fromTime
// For daily: add 24 hours
// For weekly: add 7 days
// etc.
return fromTime + (24 * 60 * 60 * 1000L) // Simplified: daily
}
/**
* Reschedule fetch work
*/
private suspend fun rescheduleFetch(schedule: Schedule) {
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: ${schedule.id}")
}
/**
* Record recovery in history
*/
private suspend fun recordRecoveryHistory(
db: DailyNotificationDatabase,
scenario: String,
missedCount: Int,
rescheduledCount: Int
) {
try {
db.historyDao().insert(
History(
refId = "recovery_${System.currentTimeMillis()}",
kind = "recovery",
occurredAt = System.currentTimeMillis(),
outcome = "success",
diagJson = """
{
"scenario": "$scenario",
"missed_count": $missedCount,
"rescheduled_count": $rescheduledCount
}
""".trimIndent()
)
)
} catch (e: Exception) {
Log.e(TAG, "Failed to record recovery history", e)
}
}
/**
* Fire missed alarm callback
*/
private suspend fun fireMissedAlarmCallback(schedule: Schedule, scheduledTime: Long) {
// TODO: Implement callback mechanism
// This could fire a Capacitor event or call a registered callback
Log.d(TAG, "Missed alarm callback: ${schedule.id} at $scheduledTime")
}
/**
* Fire missed notification callback
*/
private suspend fun fireMissedNotificationCallback(notification: NotificationContentEntity) {
// TODO: Implement callback mechanism
Log.d(TAG, "Missed notification callback: ${notification.id}")
}
3. Integration: DailyNotificationPlugin
3.1 Update load() Method
File: android/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.kt
Location: Line 91
Current Code:
override fun load() {
super.load()
try {
if (context == null) {
Log.e(TAG, "Context is null, cannot initialize database")
return
}
db = DailyNotificationDatabase.getDatabase(context)
Log.i(TAG, "Daily Notification Plugin loaded successfully")
} catch (e: Exception) {
Log.e(TAG, "Failed to initialize Daily Notification Plugin", e)
}
}
Updated Code:
override fun load() {
super.load()
try {
if (context == null) {
Log.e(TAG, "Context is null, cannot initialize database")
return
}
db = DailyNotificationDatabase.getDatabase(context)
Log.i(TAG, "Daily Notification Plugin loaded successfully")
// Perform app launch recovery
val reactivationManager = ReactivationManager(context)
reactivationManager.performRecovery()
} catch (e: Exception) {
Log.e(TAG, "Failed to initialize Daily Notification Plugin", e)
}
}
4. Enhancement: BootReceiver
⚠️ Illustrative only – See Phase 3 directive for canonical boot receiver implementation.
For implementation details, see Phase 3: Boot Receiver Enhancement
4.1 Update rescheduleNotifications() Method
File: android/src/main/java/com/timesafari/dailynotification/BootReceiver.kt
Location: Line 38
Current Issue: Only reschedules future alarms (line 64: if (nextRunTime > System.currentTimeMillis()))
Updated Code:
private suspend fun rescheduleNotifications(context: Context) {
val db = DailyNotificationDatabase.getDatabase(context)
val enabledSchedules = db.scheduleDao().getEnabled()
val currentTime = System.currentTimeMillis()
Log.i(TAG, "Found ${enabledSchedules.size} enabled schedules to reschedule")
var futureRescheduled = 0
var missedDetected = 0
enabledSchedules.forEach { schedule ->
try {
when (schedule.kind) {
"fetch" -> {
// Reschedule WorkManager fetch
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" -> {
// Reschedule AlarmManager notification
val nextRunTime = calculateNextRunTime(schedule)
if (nextRunTime > currentTime) {
// Future alarm - reschedule
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 notification for schedule: ${schedule.id}")
futureRescheduled++
} else {
// Past alarm - was missed during reboot
missedDetected++
handleMissedAlarmOnBoot(context, schedule, nextRunTime)
// Reschedule next occurrence if repeating
if (isRepeating(schedule)) {
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}")
futureRescheduled++
}
}
}
else -> {
Log.w(TAG, "Unknown schedule kind: ${schedule.kind}")
}
}
} catch (e: Exception) {
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 = "success",
diagJson = """
{
"schedules_rescheduled": $futureRescheduled,
"missed_detected": $missedDetected
}
""".trimIndent()
)
)
} catch (e: Exception) {
Log.e(TAG, "Failed to record boot recovery", e)
}
Log.i(TAG, "Boot recovery complete: $futureRescheduled rescheduled, $missedDetected missed")
}
/**
* Handle missed alarm detected during boot recovery
*/
private suspend fun handleMissedAlarmOnBoot(
context: Context,
schedule: Schedule,
scheduledTime: Long
) {
// Option 1: Generate missed alarm notification
// Option 2: Fire callback
// Option 3: Update delivery status in database
Log.i(TAG, "Missed alarm detected on boot: ${schedule.id} scheduled for $scheduledTime")
// TODO: Implement missed alarm handling
// This could generate a notification or fire a callback
}
5. Testing Requirements
⚠️ Illustrative only – See Phase 1, Phase 2, and Phase 3 directives for canonical testing procedures.
For testing details, see:
5.1 Test Setup
Package Name: com.timesafari.dailynotification
Test App Location: test-apps/android-test-app/
Prerequisites:
- Android device or emulator connected via ADB
- Test app built and installed
- ADB access enabled
- Logcat monitoring capability
Useful ADB Commands:
# Check if device is connected
adb devices
# Install test app
cd test-apps/android-test-app
./gradlew installDebug
# Monitor logs (filter for plugin)
adb logcat | grep -E "DNP|DailyNotification|REACTIVATION|BOOT"
# Clear logs before test
adb logcat -c
# Check scheduled alarms
adb shell dumpsys alarm | grep -i timesafari
# Check WorkManager tasks
adb shell dumpsys jobscheduler | grep -i timesafari
5.2 Test 1: Cold Start Recovery
Purpose: Verify plugin detects and handles missed alarms when app is launched after process kill.
Test Procedure:
Step 1: Schedule Alarm
Via Test App UI:
- Launch test app:
adb shell am start -n com.timesafari.dailynotification/.MainActivity - Click "Test Notification" button
- Schedule alarm for 4 minutes in future (test app default)
- Note the scheduled time
Via ADB (Alternative):
# Launch app
adb shell am start -n com.timesafari.dailynotification/.MainActivity
# Wait for app to load, then use UI to schedule
# Or use monkey to click button (if button ID known)
Verify Alarm Scheduled:
# Check AlarmManager
adb shell dumpsys alarm | grep -A 5 -B 5 timesafari
# Check logs for scheduling confirmation
adb logcat -d | grep "DN|SCHEDULE\|DN|ALARM"
Step 2: Kill App Process (Not Force Stop)
Important: Use am kill NOT am force-stop. This simulates OS memory pressure kill.
# Kill app process (simulates OS kill)
adb shell am kill com.timesafari.dailynotification
# Verify app is killed
adb shell ps | grep timesafari
# Should return nothing (process killed)
# Verify alarm is still scheduled (should be)
adb shell dumpsys alarm | grep -i timesafari
# Should still show scheduled alarm
Step 3: Wait for Alarm Time to Pass
# Wait 5-10 minutes (longer than scheduled alarm time)
# Monitor system time
adb shell date
# Or set a timer and wait
# Alarm was scheduled for 4 minutes, wait 10 minutes total
During Wait Period:
- Alarm should NOT fire (app process is killed)
- AlarmManager still has the alarm scheduled
- No notification should appear
Step 4: Launch App (Cold Start)
# Launch app (triggers cold start)
adb shell am start -n com.timesafari.dailynotification/.MainActivity
# Monitor logs for recovery
adb logcat -c # Clear logs first
adb logcat | grep -E "DNP-REACTIVATION|DN|RECOVERY|missed"
Step 5: Verify Recovery
Check Logs:
# Look for recovery logs
adb logcat -d | grep -E "DNP-REACTIVATION|COLD_START|missed"
# Expected log entries:
# - "Starting app launch recovery"
# - "Detected scenario: COLD_START"
# - "Handling cold start recovery"
# - "Handled missed notification: <id>"
# - "Cold start recovery complete: X missed, Y rescheduled"
Check Database:
# Query database for missed notifications (requires root or debug build)
# Or check via test app UI if status display shows missed alarms
Expected Results:
- ✅ Plugin detects missed alarm
- ✅ Missed alarm event/notification generated
- ✅ Future alarms rescheduled
- ✅ Recovery logged in history
Pass/Fail Criteria:
- Pass: Logs show "missed" detection and recovery completion
- Fail: No recovery logs, or recovery doesn't detect missed alarm
5.3 Test 2: Force Stop Recovery
Purpose: Verify plugin detects force stop scenario and recovers ALL alarms (missed and future).
Test Procedure:
Step 1: Schedule Multiple Alarms
Via Test App UI:
- Launch app:
adb shell am start -n com.timesafari.dailynotification/.MainActivity - Schedule alarm #1 for 2 minutes in future
- Schedule alarm #2 for 5 minutes in future
- Schedule alarm #3 for 10 minutes in future
- Note all scheduled times
Verify Alarms Scheduled:
# Check AlarmManager
adb shell dumpsys alarm | grep -A 10 timesafari
# Check logs
adb logcat -d | grep "DN|SCHEDULE"
Step 2: Force Stop App
Important: Use am force-stop to trigger hard kill.
# Force stop app (hard kill)
adb shell am force-stop com.timesafari.dailynotification
# Verify app is force-stopped
adb shell ps | grep timesafari
# Should return nothing
# Verify alarms are cancelled
adb shell dumpsys alarm | grep -i timesafari
# Should return nothing (all alarms cancelled)
Expected: All alarms immediately cancelled by OS.
Step 3: Wait Past Scheduled Times
# Wait 15 minutes (past all scheduled times)
# Monitor time
adb shell date
# During wait: No alarms should fire (all cancelled)
Step 4: Launch App (Force Stop Recovery)
# Launch app (triggers force stop recovery)
adb shell am start -n com.timesafari.dailynotification/.MainActivity
# Monitor logs immediately
adb logcat -c
adb logcat | grep -E "DNP-REACTIVATION|FORCE_STOP|missed|rescheduled"
Step 5: Verify Force Stop Recovery
Check Logs:
# Look for force stop detection
adb logcat -d | grep -E "DNP-REACTIVATION|FORCE_STOP|force_stop"
# Expected log entries:
# - "Starting app launch recovery"
# - "Detected scenario: FORCE_STOP"
# - "Handling force stop recovery"
# - "Missed alarm detected: <id>"
# - "Rescheduled alarm: <id>"
# - "Force stop recovery complete: X missed, Y rescheduled"
Check AlarmManager:
# Verify alarms are rescheduled
adb shell dumpsys alarm | grep -A 10 timesafari
# Should show rescheduled alarms
Expected Results:
- ✅ Plugin detects force stop scenario (DB has alarms, AlarmManager has zero)
- ✅ All past alarms marked as missed
- ✅ All future alarms rescheduled
- ✅ Recovery logged in history
Pass/Fail Criteria:
- Pass: Logs show "FORCE_STOP" detection and all alarms recovered
- Fail: No force stop detection, or alarms not recovered
5.4 Test 3: Boot Recovery Missed Alarms
Purpose: Verify boot receiver detects and handles missed alarms during device reboot.
Test Procedure:
Step 1: Schedule Alarm Before Reboot
Via Test App UI:
- Launch app:
adb shell am start -n com.timesafari.dailynotification/.MainActivity - Schedule alarm for 5 minutes in future
- Note scheduled time
Verify Alarm Scheduled:
adb shell dumpsys alarm | grep -A 5 timesafari
Step 2: Reboot Device
Emulator:
# Reboot emulator
adb reboot
# Wait for reboot to complete (30-60 seconds)
# Monitor with:
adb wait-for-device
adb shell getprop sys.boot_completed
# Wait until returns "1"
Physical Device:
- Manually reboot device
- Wait for boot to complete
- Reconnect ADB:
adb devices
Step 3: Wait for Alarm Time to Pass
# After reboot, wait 10 minutes (past scheduled alarm time)
# Do NOT open app during this time
# Monitor system time
adb shell date
During Wait:
- Boot receiver should have run (check logs after reboot)
- Alarm should NOT fire (was wiped on reboot)
- No notification should appear
Step 4: Check Boot Receiver Logs
# Check logs from boot
adb logcat -d | grep -E "DNP-BOOT|BOOT_COMPLETED|reschedule"
# Expected log entries:
# - "Boot completed, rescheduling notifications"
# - "Found X enabled schedules to reschedule"
# - "Rescheduled notification for schedule: <id>"
# - "Missed alarm detected on boot: <id>"
# - "Boot recovery complete: X rescheduled, Y missed"
Step 5: Launch App and Verify
# Launch app to verify state
adb shell am start -n com.timesafari.dailynotification/.MainActivity
# Check if missed alarms were handled
adb logcat -d | grep -E "missed|boot_recovery"
Expected Results:
- ✅ Boot receiver runs on reboot
- ✅ Boot receiver detects missed alarms (past scheduled time)
- ✅ Missed alarms handled (notification generated or callback fired)
- ✅ Next occurrence rescheduled if repeating
- ✅ Future alarms rescheduled
Pass/Fail Criteria:
- Pass: Boot receiver logs show missed alarm detection and handling
- Fail: Boot receiver doesn't run, or doesn't detect missed alarms
5.5 Test 4: Warm Start Verification
Purpose: Verify plugin checks for missed alarms when app returns from background.
Test Procedure:
Step 1: Schedule Alarm
Via Test App UI:
- Launch app:
adb shell am start -n com.timesafari.dailynotification/.MainActivity - Schedule alarm for 10 minutes in future
- Note scheduled time
Verify Alarm Scheduled:
adb shell dumpsys alarm | grep -A 5 timesafari
Step 2: Put App in Background
# Press home button (puts app in background)
adb shell input keyevent KEYCODE_HOME
# Verify app is in background
adb shell dumpsys activity activities | grep -A 5 timesafari
# App should be in "stopped" or "paused" state
Alternative: Use app switcher on device/emulator to swipe away (but don't force stop).
Step 3: Wait and Verify Alarm Still Scheduled
# Wait 2-3 minutes (before alarm time)
# Verify alarm is still scheduled
adb shell dumpsys alarm | grep -A 5 timesafari
# Should still show scheduled alarm
# Verify app process status
adb shell ps | grep timesafari
# Process may still be running (warm start scenario)
Step 4: Bring App to Foreground
# Bring app to foreground
adb shell am start -n com.timesafari.dailynotification/.MainActivity
# Monitor logs for warm start recovery
adb logcat -c
adb logcat | grep -E "DNP-REACTIVATION|WARM_START|verified"
Step 5: Verify Warm Start Recovery
Check Logs:
# Look for warm start recovery
adb logcat -d | grep -E "DNP-REACTIVATION|WARM_START|verified"
# Expected log entries:
# - "Starting app launch recovery"
# - "Detected scenario: WARM_START" (or COLD_START if process was killed)
# - "Handling warm start recovery"
# - "Warm start recovery complete: X missed, Y verified"
Check Alarm Status:
# Verify alarm is still scheduled
adb shell dumpsys alarm | grep -A 5 timesafari
# Should still show scheduled alarm
Expected Results:
- ✅ Plugin checks for missed alarms on warm start
- ✅ Plugin verifies active alarms are still scheduled
- ✅ No false positives (alarm still valid, not marked as missed)
Pass/Fail Criteria:
- Pass: Logs show verification, alarm still scheduled
- Fail: Alarm incorrectly marked as missed, or verification fails
5.6 Test 5: Swipe from Recents (Alarm Persistence)
Purpose: Verify alarms persist and fire even after app is swiped from recents.
Test Procedure:
Step 1: Schedule Alarm
# Launch app and schedule alarm for 4 minutes
adb shell am start -n com.timesafari.dailynotification/.MainActivity
# Use UI to schedule alarm
Step 2: Swipe App from Recents
Emulator:
# Open recent apps
adb shell input keyevent KEYCODE_APP_SWITCH
# Swipe app away (requires manual interaction or automation)
# Or use:
adb shell input swipe 500 1000 500 500 # Swipe up gesture
Physical Device: Manually swipe app from recent apps list.
Step 3: Verify App Process Status
# Check if process is still running
adb shell ps | grep timesafari
# Process may be killed or still running (OS decision)
# Verify alarm is still scheduled (CRITICAL)
adb shell dumpsys alarm | grep -A 5 timesafari
# Should still show scheduled alarm
Step 4: Wait for Alarm Time
# Wait for scheduled alarm time
# Monitor logs
adb logcat -c
adb logcat | grep -E "DN|RECEIVE|DN|NOTIFICATION|DN|WORK"
Step 5: Verify Alarm Fires
Check Logs:
# Look for alarm firing
adb logcat -d | grep -E "DN|RECEIVE_START|DN|WORK_START|DN|DISPLAY"
# Expected log entries:
# - "DN|RECEIVE_START" (DailyNotificationReceiver triggered)
# - "DN|WORK_START" (WorkManager worker started)
# - "DN|DISPLAY_NOTIF_START" (Notification displayed)
Check Notification:
- Notification should appear in system notification tray
- App process should be recreated by OS to deliver notification
Expected Results:
- ✅ Alarm fires even though app was swiped away
- ✅ App process recreated by OS
- ✅ Notification displayed
Pass/Fail Criteria:
- Pass: Notification appears, logs show alarm fired
- Fail: No notification, or alarm doesn't fire
5.7 Test 6: Exact Alarm Permission (Android 12+)
Purpose: Verify exact alarm permission handling on Android 12+.
Test Procedure:
Step 1: Revoke Exact Alarm Permission
Android 12+ (API 31+):
# Check current permission status
adb shell dumpsys package com.timesafari.dailynotification | grep -i "schedule_exact_alarm"
# Revoke permission (requires root or system app)
# Or use Settings UI:
adb shell am start -a android.settings.REQUEST_SCHEDULE_EXACT_ALARM
# Manually revoke in settings
Alternative: Use test app UI to check permission status.
Step 2: Attempt to Schedule Alarm
Via Test App UI:
- Launch app
- Click "Test Notification"
- Should trigger permission request flow
Check Logs:
adb logcat -d | grep -E "EXACT_ALARM|permission|SCHEDULE_EXACT"
Step 3: Grant Permission
Via Settings:
# Open exact alarm settings
adb shell am start -a android.settings.REQUEST_SCHEDULE_EXACT_ALARM
# Or navigate manually:
adb shell am start -a android.settings.APPLICATION_DETAILS_SETTINGS -d package:com.timesafari.dailynotification
Step 4: Verify Alarm Scheduling
# Schedule alarm again
# Check if it succeeds
adb logcat -d | grep "DN|SCHEDULE"
# Verify alarm is scheduled
adb shell dumpsys alarm | grep -A 5 timesafari
Expected Results:
- ✅ Plugin detects missing permission
- ✅ Settings opened automatically
- ✅ After granting, alarm schedules successfully
5.8 Test Validation Matrix
| Test | Scenario | Expected OS Behavior | Expected Plugin Behavior | Verification Method | Pass Criteria |
|---|---|---|---|---|---|
| 1 | Cold Start | N/A | Detect missed alarms | Logs show recovery | ✅ Recovery logs present |
| 2 | Force Stop | Alarms cancelled | Detect force stop, recover all | Logs + AlarmManager check | ✅ All alarms recovered |
| 3 | Boot Recovery | Alarms wiped | Boot receiver reschedules + detects missed | Boot logs | ✅ Boot receiver handles missed |
| 4 | Warm Start | Alarms persist | Verify alarms still scheduled | Logs + AlarmManager check | ✅ Verification logs present |
| 5 | Swipe from Recents | Alarm fires | Alarm fires (OS handles) | Notification appears | ✅ Notification displayed |
| 6 | Exact Alarm Permission | Permission required | Request permission, schedule after grant | Logs + AlarmManager | ✅ Permission flow works |
5.9 Log Monitoring Commands
Comprehensive Log Monitoring:
# Monitor all plugin-related logs
adb logcat | grep -E "DNP|DailyNotification|REACTIVATION|BOOT|DN\|"
# Monitor recovery specifically
adb logcat | grep -E "REACTIVATION|recovery|missed|reschedule"
# Monitor alarm scheduling
adb logcat | grep -E "DN\|SCHEDULE|DN\|ALARM|setAlarmClock|setExact"
# Monitor notification delivery
adb logcat | grep -E "DN\|RECEIVE|DN\|WORK|DN\|DISPLAY|Notification"
# Save logs to file for analysis
adb logcat -d > recovery_test.log
Filter by Tag:
# DNP-REACTIVATION (recovery manager)
adb logcat -s DNP-REACTIVATION
# DNP-BOOT (boot receiver)
adb logcat -s DNP-BOOT
# DailyNotificationWorker
adb logcat -s DailyNotificationWorker
# DailyNotificationReceiver
adb logcat -s DailyNotificationReceiver
5.10 Troubleshooting Test Failures
If Recovery Doesn't Run:
# Check if plugin loaded
adb logcat -d | grep "Daily Notification Plugin loaded"
# Check for errors
adb logcat -d | grep -E "ERROR|Exception|Failed"
# Verify database initialized
adb logcat -d | grep "DailyNotificationDatabase"
If Force Stop Not Detected:
# Check AlarmManager state
adb shell dumpsys alarm | grep -c timesafari
# Count should be 0 after force stop
# Check database state
# (Requires database inspection tool or test app UI)
If Missed Alarms Not Detected:
# Check database query
adb logcat -d | grep "getNotificationsReadyForDelivery"
# Verify scheduled_time < currentTime
adb shell date +%s
# Compare with alarm scheduled times
5.11 Automated Test Script
Basic Test Script (save as test_recovery.sh):
#!/bin/bash
PACKAGE="com.timesafari.dailynotification"
ACTIVITY="${PACKAGE}/.MainActivity"
echo "=== Test 1: Cold Start Recovery ==="
echo "1. Schedule alarm via UI, then run:"
echo " adb shell am kill $PACKAGE"
echo "2. Wait 10 minutes"
echo "3. Launch app:"
echo " adb shell am start -n $ACTIVITY"
echo "4. Check logs:"
echo " adb logcat -d | grep DNP-REACTIVATION"
echo ""
echo "=== Test 2: Force Stop Recovery ==="
echo "1. Schedule alarms via UI"
echo "2. Force stop:"
echo " adb shell am force-stop $PACKAGE"
echo "3. Wait 15 minutes"
echo "4. Launch app:"
echo " adb shell am start -n $ACTIVITY"
echo "5. Check logs:"
echo " adb logcat -d | grep FORCE_STOP"
echo ""
echo "=== Test 3: Boot Recovery ==="
echo "1. Schedule alarm via UI"
echo "2. Reboot:"
echo " adb reboot"
echo "3. Wait for boot, then 10 minutes"
echo "4. Check boot logs:"
echo " adb logcat -d | grep DNP-BOOT"
Make executable: chmod +x test_recovery.sh
6. Implementation Checklist
- Create
ReactivationManager.ktfile - Implement scenario detection
- Implement force stop recovery
- Implement cold start recovery
- Implement warm start recovery
- Implement missed alarm handling
- Implement missed notification handling
- Update
DailyNotificationPlugin.load()to call recovery - Update
BootReceiver.rescheduleNotifications()to handle missed alarms - Add helper methods (calculateNextRunTime, isRepeating, etc.)
- Add logging for debugging
- Test cold start recovery
- Test force stop recovery
- Test boot recovery missed alarms
- Test warm start verification
7. Code References
Existing Code to Reuse:
NotifyReceiver.scheduleExactNotification()- Line 92NotifyReceiver.isAlarmScheduled()- Line 279NotifyReceiver.getNextAlarmTime()- Line 305FetchWorker.scheduleFetch()- Line 31NotificationContentDao.getNotificationsReadyForDelivery()- Line 98BootReceiver.calculateNextRunTime()- Line 103
New Code to Create:
ReactivationManager.kt- New file- Helper methods in
BootReceiver.kt- Add to existing file
Related Documentation
- Exploration Findings - Gap analysis
- Plugin Requirements & Implementation - Requirements
- Platform Capability Reference - Android OS behavior
Notes
- Implementation should be done incrementally
- Test each scenario after implementation
- Log extensively for debugging
- Handle errors gracefully (don't crash on recovery failure)
- Consider performance (recovery should not block app launch)