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.
1504 lines
45 KiB
Markdown
1504 lines
45 KiB
Markdown
# 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: <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)
|
||
|