Files
daily-notification-plugin/docs/android-implementation-directive.md
Matthew Raymer 35babb3126 docs(alarms): unify and enhance alarm directive documentation stack
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.
2025-11-25 10:09:46 +00:00

1504 lines
45 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 13 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: <id>"
# - "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: <id>"
# - "Rescheduled alarm: <id>"
# - "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: <id>"
# - "Missed alarm detected on boot: <id>"
# - "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)