chore: synch this plan
This commit is contained in:
686
docs/android-implementation-directive-phase1.md
Normal file
686
docs/android-implementation-directive-phase1.md
Normal file
@@ -0,0 +1,686 @@
|
||||
# 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.
|
||||
|
||||
Reference in New Issue
Block a user