687 lines
22 KiB
Markdown
687 lines
22 KiB
Markdown
# Android Implementation Directive: Phase 1 - Cold Start Recovery
|
|
|
|
**Author**: Matthew Raymer
|
|
**Date**: November 2025
|
|
**Status**: Phase 1 - Minimal Viable Recovery
|
|
**Version**: 1.0.0
|
|
|
|
## Purpose
|
|
|
|
Phase 1 implements **minimal viable app launch recovery** for cold start scenarios. This focuses on detecting and handling missed notifications when the app launches after the process was killed.
|
|
|
|
**Scope**: Phase 1 implements **cold start recovery only**. Force stop detection, warm start optimization, and boot receiver enhancements are **out of scope** for this phase and deferred to later phases.
|
|
|
|
**Reference**:
|
|
- [Full Implementation Directive](./android-implementation-directive.md) - Complete scope
|
|
- [Phase 2: Force Stop Recovery](./android-implementation-directive-phase2.md) - Next phase
|
|
- [Phase 3: Boot Receiver Enhancement](./android-implementation-directive-phase3.md) - Final phase
|
|
|
|
---
|
|
|
|
## 1. Acceptance Criteria
|
|
|
|
### 1.1 Definition of Done
|
|
|
|
**Phase 1 is complete when:**
|
|
|
|
1. ✅ **On cold start, missed notifications are detected**
|
|
- Notifications with `scheduled_time < currentTime` and `delivery_status != 'delivered'` are identified
|
|
- Detection runs automatically on app launch (via `DailyNotificationPlugin.load()`)
|
|
- Detection completes within 2 seconds (non-blocking)
|
|
|
|
2. ✅ **Missed notifications are marked in database**
|
|
- `delivery_status` updated to `'missed'`
|
|
- `last_delivery_attempt` updated to current time
|
|
- Status change logged in history table
|
|
|
|
3. ✅ **Future alarms are verified and rescheduled if missing**
|
|
- All enabled `notify` schedules checked against AlarmManager
|
|
- Missing alarms rescheduled using existing `NotifyReceiver.scheduleExactNotification()`
|
|
- No duplicate alarms created (verified before rescheduling)
|
|
|
|
4. ✅ **Recovery never crashes the app**
|
|
- All exceptions caught and logged
|
|
- Database errors don't propagate
|
|
- Invalid data handled gracefully
|
|
|
|
5. ✅ **Recovery is observable**
|
|
- All recovery actions logged with `DNP-REACTIVATION` tag
|
|
- Recovery metrics recorded in history table
|
|
- Logs include counts: missed detected, rescheduled, errors
|
|
|
|
### 1.2 Success Metrics
|
|
|
|
| Metric | Target | Measurement |
|
|
|--------|--------|-------------|
|
|
| Recovery execution time | < 2 seconds | Log timestamp difference |
|
|
| Missed detection accuracy | 100% | Manual verification via logs |
|
|
| Reschedule success rate | > 95% | History table outcome field |
|
|
| Crash rate | 0% | No exceptions propagate to app |
|
|
|
|
### 1.3 Out of Scope (Phase 1)
|
|
|
|
- ❌ Force stop detection (Phase 2)
|
|
- ❌ Warm start optimization (Phase 2)
|
|
- ❌ Boot receiver missed alarm handling (Phase 2)
|
|
- ❌ Callback event emission (Phase 2)
|
|
- ❌ Fetch work recovery (Phase 2)
|
|
|
|
---
|
|
|
|
## 2. Implementation: ReactivationManager
|
|
|
|
### 2.1 Create New File
|
|
|
|
**File**: `android/src/main/java/com/timesafari/dailynotification/ReactivationManager.kt`
|
|
|
|
**Purpose**: Centralized cold start recovery logic
|
|
|
|
### 2.2 Class Structure
|
|
|
|
```kotlin
|
|
package com.timesafari.dailynotification
|
|
|
|
import android.content.Context
|
|
import android.util.Log
|
|
import kotlinx.coroutines.CoroutineScope
|
|
import kotlinx.coroutines.Dispatchers
|
|
import kotlinx.coroutines.launch
|
|
import kotlinx.coroutines.withTimeout
|
|
import java.util.concurrent.TimeUnit
|
|
|
|
/**
|
|
* Manages recovery of alarms and notifications on app launch
|
|
* Phase 1: Cold start recovery only
|
|
*
|
|
* @author Matthew Raymer
|
|
* @version 1.0.0
|
|
*/
|
|
class ReactivationManager(private val context: Context) {
|
|
|
|
companion object {
|
|
private const val TAG = "DNP-REACTIVATION"
|
|
private const val RECOVERY_TIMEOUT_SECONDS = 2L
|
|
}
|
|
|
|
/**
|
|
* Perform recovery on app launch
|
|
* Phase 1: Calls only performColdStartRecovery() when DB is non-empty
|
|
*
|
|
* Scenario detection is not implemented in Phase 1 - all app launches
|
|
* with non-empty DB are treated as cold start. Force stop, boot, and
|
|
* warm start handling are deferred to Phase 2.
|
|
*
|
|
* **Correction**: Must not run when DB is empty (first launch).
|
|
*
|
|
* Runs asynchronously with timeout to avoid blocking app startup
|
|
*
|
|
* Rollback Safety: If recovery fails, app continues normally
|
|
*/
|
|
fun performRecovery() {
|
|
CoroutineScope(Dispatchers.IO).launch {
|
|
try {
|
|
withTimeout(TimeUnit.SECONDS.toMillis(RECOVERY_TIMEOUT_SECONDS)) {
|
|
Log.i(TAG, "Starting app launch recovery (Phase 1: cold start only)")
|
|
|
|
// Correction: Short-circuit if DB is empty (first launch)
|
|
val db = DailyNotificationDatabase.getDatabase(context)
|
|
val dbSchedules = db.scheduleDao().getEnabled()
|
|
|
|
if (dbSchedules.isEmpty()) {
|
|
Log.i(TAG, "No schedules present — skipping recovery (first launch)")
|
|
return@withTimeout
|
|
}
|
|
|
|
val result = performColdStartRecovery()
|
|
Log.i(TAG, "App launch recovery completed: $result")
|
|
}
|
|
} catch (e: Exception) {
|
|
// Rollback: Log error but don't crash
|
|
Log.e(TAG, "Recovery failed (non-fatal): ${e.message}", e)
|
|
// Record failure in history (best effort, don't fail if this fails)
|
|
try {
|
|
recordRecoveryFailure(e)
|
|
} catch (historyError: Exception) {
|
|
Log.w(TAG, "Failed to record recovery failure in history", historyError)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ... implementation methods below ...
|
|
}
|
|
```
|
|
|
|
### 2.3 Cold Start Recovery
|
|
|
|
```kotlin
|
|
/**
|
|
* Perform cold start recovery
|
|
*
|
|
* Steps:
|
|
* 1. Detect missed notifications (scheduled_time < now, not delivered)
|
|
* 2. Mark missed notifications in database
|
|
* 3. Verify future alarms are scheduled
|
|
* 4. Reschedule missing future alarms
|
|
*
|
|
* @return RecoveryResult with counts
|
|
*/
|
|
private suspend fun performColdStartRecovery(): RecoveryResult {
|
|
val db = DailyNotificationDatabase.getDatabase(context)
|
|
val currentTime = System.currentTimeMillis()
|
|
|
|
Log.i(TAG, "Cold start recovery: checking for missed notifications")
|
|
|
|
// Step 1: Detect missed notifications
|
|
val missedNotifications = try {
|
|
db.notificationContentDao().getNotificationsReadyForDelivery(currentTime)
|
|
.filter { it.deliveryStatus != "delivered" }
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to query missed notifications", e)
|
|
emptyList()
|
|
}
|
|
|
|
var missedCount = 0
|
|
var missedErrors = 0
|
|
|
|
// Step 2: Mark missed notifications
|
|
missedNotifications.forEach { notification ->
|
|
try {
|
|
// Data integrity check: verify notification is valid
|
|
if (notification.id.isBlank()) {
|
|
Log.w(TAG, "Skipping invalid notification: empty ID")
|
|
return@forEach
|
|
}
|
|
|
|
// Update delivery status
|
|
notification.deliveryStatus = "missed"
|
|
notification.lastDeliveryAttempt = currentTime
|
|
notification.deliveryAttempts = (notification.deliveryAttempts ?: 0) + 1
|
|
|
|
db.notificationContentDao().updateNotification(notification)
|
|
missedCount++
|
|
|
|
Log.d(TAG, "Marked missed notification: ${notification.id}")
|
|
} catch (e: Exception) {
|
|
missedErrors++
|
|
Log.e(TAG, "Failed to mark missed notification: ${notification.id}", e)
|
|
// Continue processing other notifications
|
|
}
|
|
}
|
|
|
|
// Step 3: Verify and reschedule future alarms
|
|
val schedules = try {
|
|
db.scheduleDao().getEnabled()
|
|
.filter { it.kind == "notify" }
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to query schedules", e)
|
|
emptyList()
|
|
}
|
|
|
|
var rescheduledCount = 0
|
|
var verifiedCount = 0
|
|
var rescheduleErrors = 0
|
|
|
|
schedules.forEach { schedule ->
|
|
try {
|
|
// Data integrity check: verify schedule is valid
|
|
if (schedule.id.isBlank() || schedule.nextRunAt == null) {
|
|
Log.w(TAG, "Skipping invalid schedule: ${schedule.id}")
|
|
return@forEach
|
|
}
|
|
|
|
val nextRunTime = schedule.nextRunAt!!
|
|
|
|
// Only check future alarms
|
|
if (nextRunTime >= currentTime) {
|
|
// Verify alarm is scheduled
|
|
val isScheduled = NotifyReceiver.isAlarmScheduled(context, nextRunTime)
|
|
|
|
if (isScheduled) {
|
|
verifiedCount++
|
|
Log.d(TAG, "Verified scheduled alarm: ${schedule.id} at $nextRunTime")
|
|
} else {
|
|
// Reschedule missing alarm
|
|
rescheduleAlarm(schedule, nextRunTime, db)
|
|
rescheduledCount++
|
|
Log.i(TAG, "Rescheduled missing alarm: ${schedule.id} at $nextRunTime")
|
|
}
|
|
}
|
|
} catch (e: Exception) {
|
|
rescheduleErrors++
|
|
Log.e(TAG, "Failed to verify/reschedule: ${schedule.id}", e)
|
|
// Continue processing other schedules
|
|
}
|
|
}
|
|
|
|
// Step 4: Record recovery in history
|
|
val result = RecoveryResult(
|
|
missedCount = missedCount,
|
|
rescheduledCount = rescheduledCount,
|
|
verifiedCount = verifiedCount,
|
|
errors = missedErrors + rescheduleErrors
|
|
)
|
|
|
|
recordRecoveryHistory(db, "cold_start", result)
|
|
|
|
Log.i(TAG, "Cold start recovery complete: $result")
|
|
return result
|
|
}
|
|
|
|
/**
|
|
* Data class for recovery results
|
|
*/
|
|
private data class RecoveryResult(
|
|
val missedCount: Int,
|
|
val rescheduledCount: Int,
|
|
val verifiedCount: Int,
|
|
val errors: Int
|
|
) {
|
|
override fun toString(): String {
|
|
return "missed=$missedCount, rescheduled=$rescheduledCount, verified=$verifiedCount, errors=$errors"
|
|
}
|
|
}
|
|
```
|
|
|
|
### 2.4 Helper Methods
|
|
|
|
```kotlin
|
|
/**
|
|
* Reschedule an alarm
|
|
*
|
|
* Data integrity: Validates schedule before rescheduling
|
|
*/
|
|
private suspend fun rescheduleAlarm(
|
|
schedule: Schedule,
|
|
nextRunTime: Long,
|
|
db: DailyNotificationDatabase
|
|
) {
|
|
try {
|
|
// Use existing BootReceiver logic for calculating next run time
|
|
// For now, use schedule.nextRunAt directly
|
|
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 (best effort)
|
|
try {
|
|
db.scheduleDao().updateRunTimes(schedule.id, schedule.lastRunAt, nextRunTime)
|
|
} catch (e: Exception) {
|
|
Log.w(TAG, "Failed to update schedule in database: ${schedule.id}", e)
|
|
// Don't fail rescheduling if DB update fails
|
|
}
|
|
|
|
Log.i(TAG, "Rescheduled alarm: ${schedule.id} for $nextRunTime")
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to reschedule alarm: ${schedule.id}", e)
|
|
throw e // Re-throw to be caught by caller
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Record recovery in history
|
|
*
|
|
* Rollback safety: If history recording fails, log warning but don't fail recovery
|
|
*/
|
|
private suspend fun recordRecoveryHistory(
|
|
db: DailyNotificationDatabase,
|
|
scenario: String,
|
|
result: RecoveryResult
|
|
) {
|
|
try {
|
|
db.historyDao().insert(
|
|
History(
|
|
refId = "recovery_${System.currentTimeMillis()}",
|
|
kind = "recovery",
|
|
occurredAt = System.currentTimeMillis(),
|
|
outcome = if (result.errors == 0) "success" else "partial",
|
|
diagJson = """
|
|
{
|
|
"scenario": "$scenario",
|
|
"missed_count": ${result.missedCount},
|
|
"rescheduled_count": ${result.rescheduledCount},
|
|
"verified_count": ${result.verifiedCount},
|
|
"errors": ${result.errors}
|
|
}
|
|
""".trimIndent()
|
|
)
|
|
)
|
|
} catch (e: Exception) {
|
|
Log.w(TAG, "Failed to record recovery history (non-fatal)", e)
|
|
// Don't throw - history recording failure shouldn't fail recovery
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Record recovery failure in history
|
|
*/
|
|
private suspend fun recordRecoveryFailure(e: Exception) {
|
|
try {
|
|
val db = DailyNotificationDatabase.getDatabase(context)
|
|
db.historyDao().insert(
|
|
History(
|
|
refId = "recovery_failure_${System.currentTimeMillis()}",
|
|
kind = "recovery",
|
|
occurredAt = System.currentTimeMillis(),
|
|
outcome = "failure",
|
|
diagJson = """
|
|
{
|
|
"error": "${e.message}",
|
|
"error_type": "${e.javaClass.simpleName}"
|
|
}
|
|
""".trimIndent()
|
|
)
|
|
)
|
|
} catch (historyError: Exception) {
|
|
// Silently fail - we're already in error handling
|
|
Log.w(TAG, "Failed to record recovery failure", historyError)
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 3. Integration: DailyNotificationPlugin
|
|
|
|
### 3.1 Update `load()` Method
|
|
|
|
**File**: `android/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.kt`
|
|
|
|
**Location**: After database initialization (line 98)
|
|
|
|
**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")
|
|
|
|
// Phase 1: Perform app launch recovery (cold start only)
|
|
// Runs asynchronously, non-blocking, with timeout
|
|
val reactivationManager = ReactivationManager(context)
|
|
reactivationManager.performRecovery()
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to initialize Daily Notification Plugin", e)
|
|
// Don't throw - allow plugin to load even if recovery fails
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 4. Data Integrity Checks
|
|
|
|
### 4.1 Validation Rules
|
|
|
|
**Notification Validation**:
|
|
- ✅ `id` must not be blank
|
|
- ✅ `scheduled_time` must be valid timestamp
|
|
- ✅ `delivery_status` must be valid enum value
|
|
|
|
**Schedule Validation**:
|
|
- ✅ `id` must not be blank
|
|
- ✅ `kind` must be "notify" or "fetch"
|
|
- ✅ `nextRunAt` must be set for verification
|
|
- ✅ `enabled` must be true (filtered by DAO)
|
|
|
|
### 4.2 Orphaned Data Handling
|
|
|
|
**Orphaned Notifications** (no matching schedule):
|
|
- Log warning but don't fail recovery
|
|
- Mark as missed if past scheduled time
|
|
|
|
**Orphaned Schedules** (no matching notification content):
|
|
- Log warning but don't fail recovery
|
|
- Reschedule if future alarm is missing
|
|
|
|
**Mismatched Data**:
|
|
- If `NotificationContentEntity.scheduled_time` doesn't match `Schedule.nextRunAt`, use `scheduled_time` for missed detection
|
|
- Log warning for data inconsistency
|
|
|
|
---
|
|
|
|
## 5. Rollback Safety
|
|
|
|
### 5.1 No-Crash Guarantee
|
|
|
|
**All recovery operations must:**
|
|
|
|
1. **Catch all exceptions** - Never propagate exceptions to app
|
|
2. **Log errors** - All failures logged with context
|
|
3. **Continue processing** - One failure doesn't stop recovery
|
|
4. **Timeout protection** - Recovery completes within 2 seconds or times out
|
|
5. **Best-effort updates** - Database failures don't prevent alarm rescheduling
|
|
|
|
### 5.2 Error Handling Strategy
|
|
|
|
| Error Type | Handling | Log Level |
|
|
|------------|----------|-----------|
|
|
| Database query failure | Return empty list, continue | ERROR |
|
|
| Invalid notification data | Skip notification, continue | WARN |
|
|
| Alarm reschedule failure | Log error, continue to next | ERROR |
|
|
| History recording failure | Log warning, don't fail | WARN |
|
|
| Timeout | Log timeout, abort recovery | WARN |
|
|
|
|
### 5.3 Fallback Behavior
|
|
|
|
**If recovery fails completely:**
|
|
- App continues normally
|
|
- No alarms are lost (existing alarms remain scheduled)
|
|
- User can manually trigger recovery via app restart
|
|
- Error logged in history table (if possible)
|
|
|
|
---
|
|
|
|
## 6. Callback Behavior (Phase 1 - Deferred)
|
|
|
|
**Phase 1 does NOT emit callbacks.** Callback behavior is deferred to Phase 2.
|
|
|
|
**Future callback contract** (for Phase 2):
|
|
|
|
| Event | Fired When | Payload | Guarantees |
|
|
|-------|------------|---------|------------|
|
|
| `missed_notification` | Missed notification detected | `{notificationId, scheduledTime, detectedAt}` | Fired once per missed notification |
|
|
| `recovery_complete` | Recovery finished | `{scenario, missedCount, rescheduledCount, errors}` | Fired once per recovery run |
|
|
|
|
**Implementation notes:**
|
|
- Callbacks will use Capacitor event system
|
|
- Events batched if multiple missed notifications detected
|
|
- Callbacks fire after database updates complete
|
|
|
|
---
|
|
|
|
## 7. Versioning & Migration
|
|
|
|
### 7.1 Version Bump
|
|
|
|
**Plugin Version**: Increment patch version (e.g., `1.1.0` → `1.1.1`)
|
|
|
|
**Reason**: New feature (recovery), no breaking changes
|
|
|
|
### 7.2 Database Migration
|
|
|
|
**No database migration required** for Phase 1.
|
|
|
|
**Existing tables used:**
|
|
- `notification_content` - Already has `delivery_status` field
|
|
- `schedules` - Already has `nextRunAt` field
|
|
- `history` - Already supports recovery events
|
|
|
|
### 7.3 Backward Compatibility
|
|
|
|
**Phase 1 is backward compatible:**
|
|
- Existing alarms continue to work
|
|
- No schema changes
|
|
- Recovery is additive (doesn't break existing functionality)
|
|
|
|
---
|
|
|
|
## 8. Testing Requirements
|
|
|
|
### 8.1 Test 1: Cold Start Missed Detection
|
|
|
|
**Purpose**: Verify missed notifications are detected and marked.
|
|
|
|
**Steps**:
|
|
1. Schedule notification for 2 minutes in future
|
|
2. Kill app process: `adb shell am kill com.timesafari.dailynotification`
|
|
3. Wait 5 minutes (past scheduled time)
|
|
4. Launch app: `adb shell am start -n com.timesafari.dailynotification/.MainActivity`
|
|
5. Check logs: `adb logcat -d | grep DNP-REACTIVATION`
|
|
|
|
**Expected**:
|
|
- ✅ Log shows "Cold start recovery: checking for missed notifications"
|
|
- ✅ Log shows "Marked missed notification: <id>"
|
|
- ✅ Database shows `delivery_status = 'missed'`
|
|
- ✅ History table has recovery entry
|
|
|
|
**Pass Criteria**: Missed notification detected and marked in database.
|
|
|
|
### 8.2 Test 2: Future Alarm Rescheduling
|
|
|
|
**Purpose**: Verify missing future alarms are rescheduled.
|
|
|
|
**Steps**:
|
|
1. Schedule notification for 10 minutes in future
|
|
2. Manually cancel alarm: `adb shell dumpsys alarm | grep timesafari` (note request code)
|
|
3. Launch app
|
|
4. Check logs: `adb logcat -d | grep DNP-REACTIVATION`
|
|
5. Verify alarm rescheduled: `adb shell dumpsys alarm | grep timesafari`
|
|
|
|
**Expected**:
|
|
- ✅ Log shows "Rescheduled missing alarm: <id>"
|
|
- ✅ AlarmManager shows rescheduled alarm
|
|
- ✅ No duplicate alarms created
|
|
|
|
**Pass Criteria**: Missing alarm rescheduled, no duplicates.
|
|
|
|
### 8.3 Test 3: Recovery Timeout
|
|
|
|
**Purpose**: Verify recovery times out gracefully.
|
|
|
|
**Steps**:
|
|
1. Create large number of schedules (100+)
|
|
2. Launch app
|
|
3. Check logs for timeout
|
|
|
|
**Expected**:
|
|
- ✅ Recovery completes within 2 seconds OR times out
|
|
- ✅ App doesn't crash
|
|
- ✅ Partial recovery logged if timeout occurs
|
|
|
|
**Pass Criteria**: Recovery doesn't block app launch.
|
|
|
|
### 8.4 Test 4: Invalid Data Handling
|
|
|
|
**Purpose**: Verify invalid data doesn't crash recovery.
|
|
|
|
**Steps**:
|
|
1. Manually insert invalid notification (empty ID) into database
|
|
2. Launch app
|
|
3. Check logs
|
|
|
|
**Expected**:
|
|
- ✅ Invalid notification skipped
|
|
- ✅ Warning logged
|
|
- ✅ Recovery continues normally
|
|
|
|
**Pass Criteria**: Invalid data handled gracefully.
|
|
|
|
---
|
|
|
|
## 9. Implementation Checklist
|
|
|
|
- [ ] Create `ReactivationManager.kt` file
|
|
- [ ] Implement `performRecovery()` with timeout
|
|
- [ ] Implement `performColdStartRecovery()`
|
|
- [ ] Implement missed notification detection
|
|
- [ ] Implement missed notification marking
|
|
- [ ] Implement future alarm verification
|
|
- [ ] Implement missing alarm rescheduling
|
|
- [ ] Add data integrity checks
|
|
- [ ] Add error handling (no-crash guarantee)
|
|
- [ ] Add recovery history recording
|
|
- [ ] Update `DailyNotificationPlugin.load()` to call recovery
|
|
- [ ] Test cold start missed detection
|
|
- [ ] Test future alarm rescheduling
|
|
- [ ] Test recovery timeout
|
|
- [ ] Test invalid data handling
|
|
- [ ] Verify no duplicate alarms
|
|
- [ ] Verify recovery doesn't block app launch
|
|
|
|
---
|
|
|
|
## 10. Code References
|
|
|
|
**Existing Code to Reuse**:
|
|
- `NotifyReceiver.scheduleExactNotification()` - Line 92
|
|
- `NotifyReceiver.isAlarmScheduled()` - Line 279
|
|
- `BootReceiver.calculateNextRunTime()` - Line 103 (for Phase 2)
|
|
- `NotificationContentDao.getNotificationsReadyForDelivery()` - Line 99
|
|
- `ScheduleDao.getEnabled()` - Line 298
|
|
|
|
**New Code to Create**:
|
|
- `ReactivationManager.kt` - New file (Phase 1)
|
|
|
|
---
|
|
|
|
## 11. Success Criteria Summary
|
|
|
|
**Phase 1 is complete when:**
|
|
|
|
1. ✅ Missed notifications detected on cold start
|
|
2. ✅ Missed notifications marked in database
|
|
3. ✅ Future alarms verified and rescheduled if missing
|
|
4. ✅ Recovery never crashes app
|
|
5. ✅ Recovery completes within 2 seconds
|
|
6. ✅ All tests pass
|
|
7. ✅ No duplicate alarms created
|
|
|
|
---
|
|
|
|
## Related Documentation
|
|
|
|
- [Full Implementation Directive](./android-implementation-directive.md) - Complete scope (all phases)
|
|
- [Exploration Findings](./exploration-findings-initial.md) - Gap analysis
|
|
- [Plugin Requirements](./plugin-requirements-implementation.md) - Requirements
|
|
|
|
---
|
|
|
|
## Notes
|
|
|
|
- **Incremental approach**: Phase 1 focuses on cold start only. Force stop and boot recovery in Phase 2.
|
|
- **Safety first**: All recovery operations are non-blocking and non-fatal.
|
|
- **Observability**: Extensive logging for debugging and monitoring.
|
|
- **Data integrity**: Validation prevents invalid data from causing failures.
|
|
|