# 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: 1. App Launch Recovery (cold/warm/force-stop) 2. Missed Alarm Detection 3. Force Stop Detection 4. 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 [Plugin Requirements](./alarms/03-plugin-requirements.md) for requirements that Phase directives implement. **Reference**: See [Exploration Findings](./exploration-findings-initial.md) for gap analysis. **⚠️ IMPORTANT**: For implementation, use the phase-specific directives (these are the canonical source of truth): - **[Phase 1: Cold Start Recovery](./android-implementation-directive-phase1.md)** - Minimal viable recovery - Implements: [Plugin Requirements §3.1.2](./alarms/03-plugin-requirements.md#312-app-cold-start) - Explicit acceptance criteria, rollback safety, data integrity checks - **Start here** for fastest implementation - **[Phase 2: Force Stop Detection & Recovery](./android-implementation-directive-phase2.md)** - Comprehensive force stop handling - Implements: [Plugin Requirements §3.1.4](./alarms/03-plugin-requirements.md#314-force-stop-recovery-android-only) - Prerequisite: Phase 1 complete - **[Phase 3: Boot Receiver Missed Alarm Handling](./android-implementation-directive-phase3.md)** - Boot recovery enhancement - Implements: [Plugin Requirements §3.1.1](./alarms/03-plugin-requirements.md#311-boot-event-android-only) - Prerequisites: Phase 1 and Phase 2 complete **See Also**: [Unified Alarm Directive](./alarms/000-UNIFIED-ALARM-DIRECTIVE.md) for master coordination document. --- ## 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](./android-implementation-directive-phase1.md) 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](./android-implementation-directive-phase2.md) 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](./android-implementation-directive-phase3.md) 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:** - [Phase 1: ReactivationManager creation](./android-implementation-directive-phase1.md#2-implementation-reactivationmanager) - [Phase 2: Force stop detection](./android-implementation-directive-phase2.md#2-implementation-force-stop-detection) ### 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. ```kotlin 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. ```kotlin /** * 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. ```kotlin /** * 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. ```kotlin /** * 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 ```kotlin /** * 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 ```kotlin /** * 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**: ```kotlin 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**: ```kotlin 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](./android-implementation-directive-phase3.md#2-implementation-bootreceiver-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**: ```kotlin 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:** - [Phase 1: Testing Requirements](./android-implementation-directive-phase1.md#8-testing-requirements) - [Phase 2: Testing Requirements](./android-implementation-directive-phase2.md#6-testing-requirements) - [Phase 3: Testing Requirements](./android-implementation-directive-phase3.md#5-testing-requirements) ### 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**: ```bash # 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**: 1. Launch test app: `adb shell am start -n com.timesafari.dailynotification/.MainActivity` 2. Click "Test Notification" button 3. Schedule alarm for 4 minutes in future (test app default) 4. Note the scheduled time **Via ADB (Alternative)**: ```bash # 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**: ```bash # 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. ```bash # 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 ```bash # 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) ```bash # 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**: ```bash # 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: " # - "Cold start recovery complete: X missed, Y rescheduled" ``` **Check Database**: ```bash # 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**: 1. Launch app: `adb shell am start -n com.timesafari.dailynotification/.MainActivity` 2. Schedule alarm #1 for 2 minutes in future 3. Schedule alarm #2 for 5 minutes in future 4. Schedule alarm #3 for 10 minutes in future 5. Note all scheduled times **Verify Alarms Scheduled**: ```bash # 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. ```bash # 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 ```bash # 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) ```bash # 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**: ```bash # 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: " # - "Rescheduled alarm: " # - "Force stop recovery complete: X missed, Y rescheduled" ``` **Check AlarmManager**: ```bash # 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**: 1. Launch app: `adb shell am start -n com.timesafari.dailynotification/.MainActivity` 2. Schedule alarm for 5 minutes in future 3. Note scheduled time **Verify Alarm Scheduled**: ```bash adb shell dumpsys alarm | grep -A 5 timesafari ``` #### Step 2: Reboot Device **Emulator**: ```bash # 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 ```bash # 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 ```bash # 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: " # - "Missed alarm detected on boot: " # - "Boot recovery complete: X rescheduled, Y missed" ``` #### Step 5: Launch App and Verify ```bash # 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**: 1. Launch app: `adb shell am start -n com.timesafari.dailynotification/.MainActivity` 2. Schedule alarm for 10 minutes in future 3. Note scheduled time **Verify Alarm Scheduled**: ```bash adb shell dumpsys alarm | grep -A 5 timesafari ``` #### Step 2: Put App in Background ```bash # 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 ```bash # 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 ```bash # 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**: ```bash # 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**: ```bash # 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 ```bash # 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**: ```bash # 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 ```bash # 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 ```bash # 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**: ```bash # 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+)**: ```bash # 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**: 1. Launch app 2. Click "Test Notification" 3. Should trigger permission request flow **Check Logs**: ```bash adb logcat -d | grep -E "EXACT_ALARM|permission|SCHEDULE_EXACT" ``` #### Step 3: Grant Permission **Via Settings**: ```bash # 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 ```bash # 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**: ```bash # 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**: ```bash # 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**: ```bash # 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**: ```bash # 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**: ```bash # 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`): ```bash #!/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.kt` file - [ ] 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 92 - `NotifyReceiver.isAlarmScheduled()` - Line 279 - `NotifyReceiver.getNextAlarmTime()` - Line 305 - `FetchWorker.scheduleFetch()` - Line 31 - `NotificationContentDao.getNotificationsReadyForDelivery()` - Line 98 - `BootReceiver.calculateNextRunTime()` - Line 103 **New Code to Create**: - `ReactivationManager.kt` - New file - Helper methods in `BootReceiver.kt` - Add to existing file --- ## Related Documentation - [Exploration Findings](./exploration-findings-initial.md) - Gap analysis - [Plugin Requirements & Implementation](./plugin-requirements-implementation.md) - Requirements - [Platform Capability Reference](./platform-capability-reference.md) - 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)