chore: synch this plan
This commit is contained in:
483
docs/alarms/000-UNIFIED-ALARM-DIRECTIVE.md
Normal file
483
docs/alarms/000-UNIFIED-ALARM-DIRECTIVE.md
Normal file
@@ -0,0 +1,483 @@
|
|||||||
|
# 🔧 UNIFIED DIRECTIVE: Alarm / Schedule / Notification Documentation & Implementation Stack
|
||||||
|
|
||||||
|
**Author:** Matthew Raymer
|
||||||
|
**Status:** Active – Master Coordination Directive
|
||||||
|
**Scope:** Android & iOS, Capacitor plugin, alarms/schedules/notifications
|
||||||
|
**Version:** 1.0.0
|
||||||
|
**Last Updated:** November 2025
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. Purpose
|
||||||
|
|
||||||
|
Unify all existing alarm/notification documents into a **coherent, layered system** that:
|
||||||
|
|
||||||
|
1. **Eliminates duplication** (especially Android alarm behavior repeated across docs)
|
||||||
|
2. **Separates concerns** into:
|
||||||
|
* Platform facts
|
||||||
|
* Exploration/testing
|
||||||
|
* Plugin requirements
|
||||||
|
* Implementation directives (Phases 1–3)
|
||||||
|
3. **Provides iOS parity** to the existing Android-heavy material
|
||||||
|
4. **Maps OS-level capabilities → plugin guarantees → JS/TS API contract**
|
||||||
|
5. **Standardizes testing** into executable matrices for exploration and regression
|
||||||
|
6. **Connects exploration → design → implementation** and tracks status across that pipeline
|
||||||
|
|
||||||
|
**This directive is the top of the stack.** If any lower-level document conflicts with this one, **this directive wins** unless explicitly noted.
|
||||||
|
|
||||||
|
**⚠️ ENFORCEMENT RULE**: No new alarm-related documentation may be created outside Documents A, B, C, or P1–P3. All future changes must modify these documents, not create additional standalone files.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Inputs (Existing Documents)
|
||||||
|
|
||||||
|
### 1.1 Platform & Reference Layer
|
||||||
|
|
||||||
|
* `platform-capability-reference.md` – OS-level facts (Android & iOS)
|
||||||
|
* `android-alarm-persistence-directive.md` – Android abilities & limitations, engineering-grade
|
||||||
|
|
||||||
|
### 1.2 Exploration & Findings
|
||||||
|
|
||||||
|
* `plugin-behavior-exploration-template.md` – Structured template for exploring behavior
|
||||||
|
* `explore-alarm-behavior-directive.md` – Exploration directive for behavior & persistence
|
||||||
|
* `exploration-findings-initial.md` – Initial code-level discovery
|
||||||
|
|
||||||
|
### 1.3 Requirements & Implementation
|
||||||
|
|
||||||
|
* `plugin-requirements-implementation.md` – Plugin behavior rules & guarantees
|
||||||
|
* `android-implementation-directive.md` – Umbrella Android implementation overview (app launch recovery, missed alarms, force stop, boot)
|
||||||
|
* Phase directives (normative):
|
||||||
|
* `android-implementation-directive-phase1.md` – Cold start recovery (minimal viable)
|
||||||
|
* `android-implementation-directive-phase2.md` – Force stop detection & recovery
|
||||||
|
* `android-implementation-directive-phase3.md` – Boot receiver missed alarm handling
|
||||||
|
|
||||||
|
### 1.4 Improvement Directive
|
||||||
|
|
||||||
|
* `improve-alarm-directives.md` – Current plan for structural and content improvements across the stack
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Target Structure (What We're Building)
|
||||||
|
|
||||||
|
We standardize around **three primary doc roles**, plus implementation phases:
|
||||||
|
|
||||||
|
### A. **Platform Reference (Document A)**
|
||||||
|
|
||||||
|
**Goal:** Single, stable, normative OS facts.
|
||||||
|
|
||||||
|
* Merge:
|
||||||
|
* Android section from `android-alarm-persistence-directive.md`
|
||||||
|
* Android & iOS matrices from `platform-capability-reference.md`
|
||||||
|
* Content rules:
|
||||||
|
* NO plugin-specific rules
|
||||||
|
* Just: "what Android/iOS can and cannot do"
|
||||||
|
* Use matrices & short prose only
|
||||||
|
* Label each item as:
|
||||||
|
* **OS-guaranteed**, **Plugin-required**, or **Forbidden** (where relevant)
|
||||||
|
* Output path (recommended):
|
||||||
|
`docs/alarms/01-platform-capability-reference.md`
|
||||||
|
|
||||||
|
### B. **Plugin Behavior Exploration (Document B)**
|
||||||
|
|
||||||
|
**Goal:** Executable exploration & testing spec.
|
||||||
|
|
||||||
|
* Baseline: `plugin-behavior-exploration-template.md` + `explore-alarm-behavior-directive.md`
|
||||||
|
* Remove duplicated platform explanations (refer to Document A instead)
|
||||||
|
* For each scenario (swipe, reboot, force stop, etc.):
|
||||||
|
* Link to source files & functions (line or section refs)
|
||||||
|
* Provide **Expected (OS)** & **Expected (Plugin)** from Docs A + C
|
||||||
|
* Add columns for **Actual Result** and **Notes** (checkbox matrix)
|
||||||
|
* Output path:
|
||||||
|
`docs/alarms/02-plugin-behavior-exploration.md`
|
||||||
|
|
||||||
|
### C. **Plugin Requirements & Implementation Rules (Document C)**
|
||||||
|
|
||||||
|
**Goal:** What the plugin MUST guarantee & how.
|
||||||
|
|
||||||
|
* Baseline: `plugin-requirements-implementation.md` + `improve-alarm-directives.md`
|
||||||
|
* Must include:
|
||||||
|
1. **Plugin Behavior Guarantees & Limitations** by platform (table)
|
||||||
|
2. **Persistence Requirements** (fields, storage, validation, failure modes)
|
||||||
|
3. **Recovery Requirements**:
|
||||||
|
* Boot (Android)
|
||||||
|
* Cold start
|
||||||
|
* Warm start
|
||||||
|
* Force stop (Android)
|
||||||
|
* User-tap recovery
|
||||||
|
4. **Missed alarm handling contract** (definition, triggers, required actions)
|
||||||
|
5. **JS/TS API Contract**:
|
||||||
|
* What JS devs can rely on
|
||||||
|
* Platform caveats (e.g., Force Stop, iOS background limits)
|
||||||
|
6. **Unsupported features / limitations** clearly called out
|
||||||
|
* Output path:
|
||||||
|
`docs/alarms/03-plugin-requirements.md`
|
||||||
|
|
||||||
|
### D. **Implementation Directives (Phase 1–3)**
|
||||||
|
|
||||||
|
Already exist; this directive standardizes their role:
|
||||||
|
|
||||||
|
* **Phase docs are normative for code.**
|
||||||
|
If they conflict with Document C, update the phase docs and then C.
|
||||||
|
|
||||||
|
**⚠️ STRUCTURE FREEZE**: The structure defined in this directive is FINAL. No new document categories may be introduced without updating this master directive first.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Ownership, Versioning & Status
|
||||||
|
|
||||||
|
### 3.1 Ownership
|
||||||
|
|
||||||
|
**Assignable Ownership** (replace abstract roles with concrete owners):
|
||||||
|
|
||||||
|
| Layer | Owner | Responsibility |
|
||||||
|
| ---------------------- | ------------------- | ------------------------------------------------- |
|
||||||
|
| Doc A – Platform Facts | Engineering Lead | Long-lived reference, rare changes |
|
||||||
|
| Doc B – Exploration | QA / Testing Lead | Living document, updated with test results |
|
||||||
|
| Doc C – Requirements | Plugin Architect | Versioned with plugin, defines guarantees |
|
||||||
|
| Phases P1–P3 | Implementation Lead | Normative code specs, tied to Doc C requirements |
|
||||||
|
|
||||||
|
**Note**: If owners are not yet assigned, use role names above as placeholders until assignment.
|
||||||
|
|
||||||
|
### 3.2 Versioning Rules
|
||||||
|
|
||||||
|
* Any change that alters **JS/TS-visible behavior** → bump plugin **MINOR** at least
|
||||||
|
* Any change that breaks prior guarantees or adds new required migration → **MAJOR bump**
|
||||||
|
* Each doc should include:
|
||||||
|
* `Version: x.y.z`
|
||||||
|
* `Last Synced With Plugin Version: vX.Y.Z`
|
||||||
|
|
||||||
|
### 3.3 Status Matrix
|
||||||
|
|
||||||
|
Add this table to the end of *this* directive and keep it updated:
|
||||||
|
|
||||||
|
| Doc | Path | Role | Drafted? | Cleaned? | In Use? |
|
||||||
|
| --- | ------------------------------------- | ----------------- | -------- | -------- | ------- |
|
||||||
|
| A | `01-platform-capability-reference.md` | Platform facts | ☐ | ☐ | ☐ |
|
||||||
|
| B | `02-plugin-behavior-exploration.md` | Exploration | ☐ | ☐ | ☐ |
|
||||||
|
| C | `03-plugin-requirements.md` | Requirements | ☐ | ☐ | ☐ |
|
||||||
|
| P1 | `android-implementation-phase1.md` | Impl – Cold start | ✅ | ☐ | ☐ |
|
||||||
|
| P2 | `android-implementation-phase2.md` | Impl – Force stop | ✅ | ☐ | ☐ |
|
||||||
|
| P3 | `android-implementation-phase3.md` | Impl – Boot | ✅ | ☐ | ☐ |
|
||||||
|
|
||||||
|
(You can edit this directly in git as you go.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Work Plan – "Do All of the Above"
|
||||||
|
|
||||||
|
This is the **concrete to-do list** that satisfies:
|
||||||
|
|
||||||
|
* Consolidation
|
||||||
|
* Versioning & ownership
|
||||||
|
* Status tracking
|
||||||
|
* Single-source master structure
|
||||||
|
* Next-phase readiness
|
||||||
|
* Improvements from `improve-alarm-directives.md`
|
||||||
|
|
||||||
|
### Step 1 – Normalize Platform Reference (Doc A)
|
||||||
|
|
||||||
|
1. Start from `platform-capability-reference.md` + `android-alarm-persistence-directive.md`
|
||||||
|
2. Merge into a single doc:
|
||||||
|
* Android matrix
|
||||||
|
* iOS matrix
|
||||||
|
* Core principles (no plugin rules)
|
||||||
|
3. Ensure each line is labeled:
|
||||||
|
* **OS-guaranteed**, **Plugin-required**, or **Forbidden** (where relevant)
|
||||||
|
4. Mark the old Android alarm persistence doc as:
|
||||||
|
* **Deprecated in favor of Doc A** (leave file but add a banner)
|
||||||
|
|
||||||
|
### Step 2 – Rewrite Exploration (Doc B)
|
||||||
|
|
||||||
|
1. Use `plugin-behavior-exploration-template.md` as the skeleton
|
||||||
|
2. Integrate concrete scenario descriptions and code paths from `explore-alarm-behavior-directive.md`
|
||||||
|
3. For **each scenario**:
|
||||||
|
* App swipe, OS kill, reboot, force stop, cold start, warm start, notification tap:
|
||||||
|
* Add row: Step-by-step actions + Expected (OS) + Expected (Plugin) + Actual + Notes
|
||||||
|
4. Remove any explanation that duplicates Doc A; replace with "See Doc A, section X"
|
||||||
|
|
||||||
|
### Step 3 – Rewrite Plugin Requirements (Doc C)
|
||||||
|
|
||||||
|
1. Start from `plugin-requirements-implementation.md` & improvement goals
|
||||||
|
2. Add / enforce sections:
|
||||||
|
* **Plugin Behavior Guarantees & Limitations**
|
||||||
|
* **Persistence Spec** (fields, mandatory vs optional)
|
||||||
|
* **Recovery Points** (boot, cold, warm, force stop, user tap)
|
||||||
|
* **Missed Alarm Contract**
|
||||||
|
* **JS/TS API Contract**
|
||||||
|
* **Unsupported / impossible guarantees** (Force Stop, iOS background, etc.)
|
||||||
|
3. Everywhere it relies on platform behavior:
|
||||||
|
* Link back to Doc A using short cross-reference ("See A §2.1")
|
||||||
|
4. Add a **"Traceability"** mini-table:
|
||||||
|
* Row per behavior → Platform fact in A → Tested scenario in B → Implemented in Phase N
|
||||||
|
|
||||||
|
### Step 4 – Align Implementation Directives with Doc C
|
||||||
|
|
||||||
|
1. Treat Phase docs as **canonical code spec**: P1, P2, P3
|
||||||
|
2. For each behavior required in Doc C:
|
||||||
|
* Identify which Phase implements it (or will implement it)
|
||||||
|
3. Update:
|
||||||
|
* `android-implementation-directive.md` to be explicitly **descriptive/integrative**, not normative, pointing to Phases 1–3 & Doc C
|
||||||
|
* Ensure scenario model, alarm existence checks, and boot handling match the **corrected model** already defined in Phase 2 & 3
|
||||||
|
4. Add acceptance criteria per phase that directly reference:
|
||||||
|
* Requirements in Doc C
|
||||||
|
* Platform constraints in Doc A
|
||||||
|
5. **Phase 1–3 must REMOVE**:
|
||||||
|
* Any scenario logic (moved to Doc C)
|
||||||
|
* Any platform behavioral claims (moved to Doc A)
|
||||||
|
* Any recovery responsibilities not assigned to that phase
|
||||||
|
|
||||||
|
### Step 5 – Versioning & Status
|
||||||
|
|
||||||
|
1. At the top of Docs A, B, C, and each Phase doc, add:
|
||||||
|
* `Version`, `Last Updated`, `Sync'd with Plugin vX.Y.Z`
|
||||||
|
2. Maintain the status matrix (Section 3.3) as the **single source of truth** for doc maturity
|
||||||
|
3. When a Phase is fully implemented and deployed:
|
||||||
|
* Mark "In Use?" = ✅
|
||||||
|
* Add link to code tags/commit hash
|
||||||
|
|
||||||
|
### Step 6 – Readiness Check for Next Work Phase
|
||||||
|
|
||||||
|
Before starting any *new* implementation work on alarms / schedules:
|
||||||
|
|
||||||
|
1. **Confirm:**
|
||||||
|
* Doc A exists and is stable enough (no "TODO" in core matrices)
|
||||||
|
* Doc B has at least the base scenarios scaffolded
|
||||||
|
* Doc C clearly defines:
|
||||||
|
* What we guarantee on each platform
|
||||||
|
* What we cannot do (e.g., Force Stop auto-resume)
|
||||||
|
2. Only then:
|
||||||
|
* Modify or extend Phase 1–3
|
||||||
|
* Or add new phases (e.g., warm-start optimizations, iOS parity work)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Acceptance Criteria for THIS Directive (Revised and Final)
|
||||||
|
|
||||||
|
This directive is complete **ONLY** when:
|
||||||
|
|
||||||
|
1. **Doc A exists, is referenced, and no other doc contains platform facts**
|
||||||
|
* File exists: `docs/alarms/01-platform-capability-reference.md`
|
||||||
|
* All old platform docs marked as deprecated with banner
|
||||||
|
* No platform facts duplicated in other docs
|
||||||
|
|
||||||
|
2. **Doc B contains**:
|
||||||
|
* At least 6 scenarios (swipe, reboot, force stop, cold start, warm start, notification tap)
|
||||||
|
* Expected OS vs Plugin behavior columns
|
||||||
|
* Space for Actual Result and Notes
|
||||||
|
* Links to source files/functions
|
||||||
|
|
||||||
|
3. **Doc C contains**:
|
||||||
|
* Guarantees by platform (table format)
|
||||||
|
* Recovery matrix (boot, cold, warm, force stop, user tap)
|
||||||
|
* JS/TS API contract
|
||||||
|
* Unsupported behaviors clearly called out
|
||||||
|
|
||||||
|
4. **Phase docs**:
|
||||||
|
* Contain NO duplication of Doc A (platform facts)
|
||||||
|
* Reference Doc C explicitly (requirements)
|
||||||
|
* Have acceptance criteria tied to Doc C requirements
|
||||||
|
|
||||||
|
5. **Deprecated files have**:
|
||||||
|
* Banner: "⚠️ **DEPRECATED**: Superseded by [000-UNIFIED-ALARM-DIRECTIVE](./000-UNIFIED-ALARM-DIRECTIVE.md)"
|
||||||
|
* Link to replacement document
|
||||||
|
|
||||||
|
6. **Status matrix fields are no longer empty** (Section 10)
|
||||||
|
* All docs marked as Drafted at minimum
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Document Relationships & Cross-References
|
||||||
|
|
||||||
|
### 6.1 Reference Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
Unified Directive (this doc)
|
||||||
|
↓
|
||||||
|
├─→ Doc A (Platform Reference)
|
||||||
|
│ └─→ Referenced by: B, C, P1-P3
|
||||||
|
│
|
||||||
|
├─→ Doc B (Exploration)
|
||||||
|
│ └─→ References: A (platform facts), C (expected behavior)
|
||||||
|
│ └─→ Feeds into: P1-P3 (test results inform implementation)
|
||||||
|
│
|
||||||
|
├─→ Doc C (Requirements)
|
||||||
|
│ └─→ References: A (platform constraints)
|
||||||
|
│ └─→ Referenced by: P1-P3 (implementation must satisfy)
|
||||||
|
│
|
||||||
|
└─→ P1-P3 (Implementation)
|
||||||
|
└─→ References: A (platform facts), C (requirements)
|
||||||
|
└─→ Validated by: B (exploration results)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 Cross-Reference Format
|
||||||
|
|
||||||
|
When referencing between documents, use this format:
|
||||||
|
|
||||||
|
* **Doc A**: `[Platform Reference §2.1](../alarms/01-platform-capability-reference.md#21-android-alarm-persistence)`
|
||||||
|
* **Doc B**: `[Exploration §3.2](../alarms/02-plugin-behavior-exploration.md#32-cold-start-scenario)`
|
||||||
|
* **Doc C**: `[Requirements §4.3](../alarms/03-plugin-requirements.md#43-recovery-requirements)`
|
||||||
|
* **Phase docs**: `[Phase 1 §2.3](../android-implementation-directive-phase1.md#23-cold-start-recovery)`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Canonical Source of Truth Rules
|
||||||
|
|
||||||
|
### 7.1 Scenario Detection
|
||||||
|
|
||||||
|
**Only ReactivationManager.kt** performs scenario detection. No other component (BootReceiver, DailyNotificationPlugin, etc.) implements detection logic.
|
||||||
|
|
||||||
|
**Corrected Scenario Model** (from Phase 2):
|
||||||
|
```kotlin
|
||||||
|
enum class RecoveryScenario {
|
||||||
|
COLD_START, // Process killed, alarms may or may not exist
|
||||||
|
FORCE_STOP, // Alarms cleared, DB still populated
|
||||||
|
BOOT, // Device reboot
|
||||||
|
NONE // No recovery required (warm resume or first launch)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 Recovery Logic
|
||||||
|
|
||||||
|
**Only ReactivationManager.kt** implements recovery logic. BootReceiver only sets flags and queues ReactivationManager.
|
||||||
|
|
||||||
|
### 7.3 Platform Facts
|
||||||
|
|
||||||
|
**Only Doc A** contains platform facts. All other docs reference Doc A, never duplicate platform behavior.
|
||||||
|
|
||||||
|
### 7.4 Requirements
|
||||||
|
|
||||||
|
**Only Doc C** defines plugin requirements. Phase docs implement Doc C requirements.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Conflict Resolution
|
||||||
|
|
||||||
|
If conflicts arise between documents:
|
||||||
|
|
||||||
|
1. **This directive (000)** wins for structure and organization
|
||||||
|
2. **Doc A** wins for platform facts
|
||||||
|
3. **Doc C** wins for requirements
|
||||||
|
4. **Phase docs (P1-P3)** win for implementation details
|
||||||
|
5. **Doc B** is observational (actual test results) and may reveal conflicts
|
||||||
|
|
||||||
|
When a conflict is found:
|
||||||
|
1. Document it in this section
|
||||||
|
2. Resolve by updating the lower-priority document
|
||||||
|
3. Update the status matrix
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Next Steps
|
||||||
|
|
||||||
|
### ⚠️ Time-Based Triggers (Enforcement)
|
||||||
|
|
||||||
|
**Doc A must be created BEFORE any further implementation work.**
|
||||||
|
- No Phase 2 or Phase 3 changes until Doc A exists
|
||||||
|
- No new platform behavior claims in any doc until Doc A is canonical
|
||||||
|
|
||||||
|
**Doc B must be baseline-complete BEFORE Phase 2 changes.**
|
||||||
|
- At least 6 core scenarios scaffolded
|
||||||
|
- Expected behavior columns populated from Doc A + Doc C
|
||||||
|
|
||||||
|
**Doc C must be finalized BEFORE any JS/TS API changes.**
|
||||||
|
- All guarantees documented
|
||||||
|
- API contract defined
|
||||||
|
- No breaking changes to JS/TS API without Doc C update first
|
||||||
|
|
||||||
|
### Immediate (Before New Implementation)
|
||||||
|
|
||||||
|
1. Create stub documents A, B, C with structure
|
||||||
|
2. Migrate content from existing docs into new structure
|
||||||
|
3. Update all cross-references
|
||||||
|
4. Mark old docs as deprecated (with pointers to new docs)
|
||||||
|
|
||||||
|
**Deliverables Required**:
|
||||||
|
- Doc A exists as a file in repo
|
||||||
|
- Doc B exists with scenario tables scaffolded
|
||||||
|
- Doc C exists with required sections
|
||||||
|
- Status matrix updated in this document
|
||||||
|
- Deprecated docs marked with header banner
|
||||||
|
|
||||||
|
### Short-term (During Implementation)
|
||||||
|
|
||||||
|
1. Keep Doc B updated with test results
|
||||||
|
2. Update Phase docs as implementation progresses
|
||||||
|
3. Maintain status matrix
|
||||||
|
|
||||||
|
### Long-term (Post-Implementation)
|
||||||
|
|
||||||
|
1. Add iOS parity documentation
|
||||||
|
2. Expand exploration scenarios
|
||||||
|
3. Create regression test suite based on Doc B
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Change-Control Rules
|
||||||
|
|
||||||
|
Any change to Docs A–C requires:
|
||||||
|
|
||||||
|
1. **Update version header** in the document
|
||||||
|
2. **Update status matrix** (Section 10) in this directive
|
||||||
|
3. **Commit message tag**: `[ALARM-DOCS]` prefix
|
||||||
|
4. **Notification in CHANGELOG** if JS/TS-visible behavior changes
|
||||||
|
|
||||||
|
**Phase docs may not be modified unless Doc C changes first** (or explicit exception documented).
|
||||||
|
|
||||||
|
**Example commit message**:
|
||||||
|
```
|
||||||
|
[ALARM-DOCS] Update Doc C with force stop recovery guarantee
|
||||||
|
|
||||||
|
- Added force stop detection requirement
|
||||||
|
- Updated recovery matrix
|
||||||
|
- Version bumped to 1.1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Status Matrix
|
||||||
|
|
||||||
|
| Doc | Path | Role | Drafted? | Cleaned? | In Use? | Notes |
|
||||||
|
| --- | ------------------------------------- | ----------------- | -------- | -------- | ------- | ---------------------------------------- |
|
||||||
|
| A | `01-platform-capability-reference.md` | Platform facts | ☐ | ☐ | ☐ | Merge from existing platform docs |
|
||||||
|
| B | `02-plugin-behavior-exploration.md` | Exploration | ☐ | ☐ | ☐ | Based on exploration template |
|
||||||
|
| C | `03-plugin-requirements.md` | Requirements | ☐ | ☐ | ☐ | Consolidate from requirements doc |
|
||||||
|
| P1 | `android-implementation-phase1.md` | Impl – Cold start | ✅ | ☐ | ☐ | Exists, needs alignment with Doc C |
|
||||||
|
| P2 | `android-implementation-phase2.md` | Impl – Force stop | ✅ | ☐ | ☐ | Exists, needs alignment with Doc C |
|
||||||
|
| P3 | `android-implementation-phase3.md` | Impl – Boot | ✅ | ☐ | ☐ | Exists, needs alignment with Doc C |
|
||||||
|
|
||||||
|
**Legend:**
|
||||||
|
- ✅ = Complete
|
||||||
|
- ☐ = Not started / In progress
|
||||||
|
- ⚠️ = Needs attention
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
* [Android Implementation Directive](../android-implementation-directive.md) – Umbrella overview
|
||||||
|
* [Phase 1: Cold Start Recovery](../android-implementation-directive-phase1.md) – Minimal viable recovery
|
||||||
|
* [Phase 2: Force Stop Recovery](../android-implementation-directive-phase2.md) – Force stop detection
|
||||||
|
* [Phase 3: Boot Recovery](../android-implementation-directive-phase3.md) – Boot receiver enhancement
|
||||||
|
* [Exploration Findings](../exploration-findings-initial.md) – Initial code discovery
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Status**: Active master coordination directive
|
||||||
|
**Last Updated**: November 2025
|
||||||
|
**Next Review**: After Docs A, B, C are created
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Single Instruction for Team
|
||||||
|
|
||||||
|
**⚠️ BLOCKING RULE**: No engineering or documentation work on alarms/schedules/notifications may continue until Steps 1–3 in §9 ("Immediate") are complete and committed.
|
||||||
|
|
||||||
|
**Specifically**:
|
||||||
|
- Doc A must exist as a file
|
||||||
|
- Doc B must have scenario tables scaffolded
|
||||||
|
- Doc C must have required sections
|
||||||
|
- Status matrix must be updated
|
||||||
|
- Deprecated files must be marked
|
||||||
|
|
||||||
|
**Exception**: Only emergency bug fixes may proceed, but must be documented retroactively in the appropriate Doc A/B/C structure.
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
780
docs/android-implementation-directive-phase2.md
Normal file
780
docs/android-implementation-directive-phase2.md
Normal file
@@ -0,0 +1,780 @@
|
|||||||
|
# Android Implementation Directive: Phase 2 - Force Stop Detection & Recovery
|
||||||
|
|
||||||
|
**Author**: Matthew Raymer
|
||||||
|
**Date**: November 2025
|
||||||
|
**Status**: Phase 2 - Force Stop Recovery
|
||||||
|
**Version**: 1.0.0
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Phase 2 implements **force stop detection and comprehensive recovery**. This handles the scenario where the user force-stops the app, causing all alarms to be cancelled by the OS.
|
||||||
|
|
||||||
|
**⚠️ IMPORTANT**: This phase **modifies and extends** the `ReactivationManager` introduced in Phase 1. Do not create a second copy; update the existing class.
|
||||||
|
|
||||||
|
**Prerequisites**: Phase 1 must be complete (cold start recovery implemented).
|
||||||
|
|
||||||
|
**Scope**: Force stop detection, scenario differentiation, and full alarm recovery.
|
||||||
|
|
||||||
|
**Reference**: See [Phase 1](./android-implementation-directive-phase1.md) for cold start recovery, [Full Implementation Directive](./android-implementation-directive.md) for complete scope.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Acceptance Criteria
|
||||||
|
|
||||||
|
### 1.1 Definition of Done
|
||||||
|
|
||||||
|
**Phase 2 is complete when:**
|
||||||
|
|
||||||
|
1. ✅ **Force stop scenario is detected correctly**
|
||||||
|
- Detection: `(DB schedules count > 0) && (AlarmManager alarms count == 0)`
|
||||||
|
- Detection runs on app launch (via `ReactivationManager`)
|
||||||
|
- False positives avoided (distinguishes from first launch)
|
||||||
|
|
||||||
|
2. ✅ **All past alarms are marked as missed**
|
||||||
|
- All schedules with `nextRunAt < currentTime` marked as missed
|
||||||
|
- Missed notifications created/updated in database
|
||||||
|
- History records created for each missed alarm
|
||||||
|
|
||||||
|
3. ✅ **All future alarms are rescheduled**
|
||||||
|
- All schedules with `nextRunAt >= currentTime` rescheduled
|
||||||
|
- Repeating schedules calculate next occurrence correctly
|
||||||
|
- No duplicate alarms created
|
||||||
|
|
||||||
|
4. ✅ **Recovery handles both notify and fetch schedules**
|
||||||
|
- `notify` schedules rescheduled via AlarmManager
|
||||||
|
- `fetch` schedules rescheduled via WorkManager
|
||||||
|
- Both types recovered completely
|
||||||
|
|
||||||
|
5. ✅ **Recovery never crashes the app**
|
||||||
|
- All exceptions caught and logged
|
||||||
|
- Partial recovery logged if some schedules fail
|
||||||
|
- App continues normally even if recovery fails
|
||||||
|
|
||||||
|
### 1.2 Success Metrics
|
||||||
|
|
||||||
|
| Metric | Target | Measurement |
|
||||||
|
|--------|--------|-------------|
|
||||||
|
| Force stop detection accuracy | 100% | Manual verification via logs |
|
||||||
|
| Past alarm recovery rate | 100% | All past alarms marked as missed |
|
||||||
|
| Future alarm recovery rate | > 95% | History table outcome field |
|
||||||
|
| Recovery execution time | < 3 seconds | Log timestamp difference |
|
||||||
|
| Crash rate | 0% | No exceptions propagate to app |
|
||||||
|
|
||||||
|
### 1.3 Out of Scope (Phase 2)
|
||||||
|
|
||||||
|
- ❌ Warm start optimization (Phase 3)
|
||||||
|
- ❌ Boot receiver missed alarm handling (Phase 3)
|
||||||
|
- ❌ Callback event emission (Phase 3)
|
||||||
|
- ❌ User notification of missed alarms (Phase 3)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Implementation: Force Stop Detection
|
||||||
|
|
||||||
|
### 2.1 Update ReactivationManager
|
||||||
|
|
||||||
|
**File**: `android/src/main/java/com/timesafari/dailynotification/ReactivationManager.kt`
|
||||||
|
|
||||||
|
**Location**: Add scenario detection after Phase 1 implementation
|
||||||
|
|
||||||
|
### 2.2 Scenario Detection
|
||||||
|
|
||||||
|
**⚠️ Canonical Source**: This method supersedes any earlier scenario detection code shown in the full directive.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
/**
|
||||||
|
* Detect recovery scenario based on AlarmManager state vs database
|
||||||
|
*
|
||||||
|
* Phase 2: Adds force stop detection
|
||||||
|
*
|
||||||
|
* This is the normative implementation of scenario detection.
|
||||||
|
*
|
||||||
|
* @return RecoveryScenario enum value
|
||||||
|
*/
|
||||||
|
private suspend fun detectScenario(): RecoveryScenario {
|
||||||
|
val db = DailyNotificationDatabase.getDatabase(context)
|
||||||
|
val dbSchedules = db.scheduleDao().getEnabled()
|
||||||
|
|
||||||
|
// Check for first launch (empty DB)
|
||||||
|
if (dbSchedules.isEmpty()) {
|
||||||
|
Log.d(TAG, "No schedules in database - first launch (NONE)")
|
||||||
|
return RecoveryScenario.NONE
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for boot recovery (set by BootReceiver)
|
||||||
|
if (isBootRecovery()) {
|
||||||
|
Log.i(TAG, "Boot recovery detected")
|
||||||
|
return RecoveryScenario.BOOT
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for force stop: DB has schedules but no alarms exist
|
||||||
|
if (!alarmsExist()) {
|
||||||
|
Log.i(TAG, "Force stop detected: DB has ${dbSchedules.size} schedules, but no alarms exist")
|
||||||
|
return RecoveryScenario.FORCE_STOP
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal cold start: DB has schedules and alarms exist
|
||||||
|
// (Alarms may have fired or may be future alarms - need to verify/resync)
|
||||||
|
Log.d(TAG, "Cold start: DB has ${dbSchedules.size} schedules, alarms exist")
|
||||||
|
return RecoveryScenario.COLD_START
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this is a boot recovery scenario
|
||||||
|
*
|
||||||
|
* BootReceiver sets a flag in SharedPreferences when boot completes.
|
||||||
|
* This allows ReactivationManager to detect boot scenario.
|
||||||
|
*
|
||||||
|
* @return true if boot recovery, false otherwise
|
||||||
|
*/
|
||||||
|
private fun isBootRecovery(): Boolean {
|
||||||
|
val prefs = context.getSharedPreferences("dailynotification_recovery", Context.MODE_PRIVATE)
|
||||||
|
val lastBootAt = prefs.getLong("last_boot_at", 0)
|
||||||
|
val currentTime = System.currentTimeMillis()
|
||||||
|
|
||||||
|
// Boot flag is valid for 60 seconds after boot
|
||||||
|
// This prevents false positives from stale flags
|
||||||
|
if (lastBootAt > 0 && (currentTime - lastBootAt) < 60000) {
|
||||||
|
// Clear the flag after reading
|
||||||
|
prefs.edit().remove("last_boot_at").apply()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if alarms exist in AlarmManager
|
||||||
|
*
|
||||||
|
* **Correction**: Replaces unreliable nextAlarmClock check with PendingIntent check.
|
||||||
|
* This eliminates false positives from nextAlarmClock.
|
||||||
|
*
|
||||||
|
* @return true if at least one alarm exists, false otherwise
|
||||||
|
*/
|
||||||
|
private fun alarmsExist(): Boolean {
|
||||||
|
return try {
|
||||||
|
// Check if any PendingIntent for our receiver exists
|
||||||
|
// This is more reliable than nextAlarmClock
|
||||||
|
val intent = Intent(context, DailyNotificationReceiver::class.java).apply {
|
||||||
|
action = "com.timesafari.daily.NOTIFICATION"
|
||||||
|
}
|
||||||
|
val pendingIntent = PendingIntent.getBroadcast(
|
||||||
|
context,
|
||||||
|
0, // Use 0 to check for any alarm
|
||||||
|
intent,
|
||||||
|
PendingIntent.FLAG_NO_CREATE or PendingIntent.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
val exists = pendingIntent != null
|
||||||
|
Log.d(TAG, "Alarm check: alarms exist = $exists")
|
||||||
|
exists
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Error checking if alarms exist", e)
|
||||||
|
// On error, assume no alarms (conservative for force stop detection)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recovery scenario enum
|
||||||
|
*
|
||||||
|
* **Corrected Model**: Only these four scenarios are supported
|
||||||
|
*/
|
||||||
|
enum class RecoveryScenario {
|
||||||
|
COLD_START, // Process killed, alarms may or may not exist
|
||||||
|
FORCE_STOP, // Alarms cleared, DB still populated
|
||||||
|
BOOT, // Device reboot
|
||||||
|
NONE // No recovery required (warm resume or first launch)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 Update performRecovery()
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
/**
|
||||||
|
* Perform recovery on app launch
|
||||||
|
* Phase 2: Adds force stop handling
|
||||||
|
*/
|
||||||
|
fun performRecovery() {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
try {
|
||||||
|
withTimeout(TimeUnit.SECONDS.toMillis(RECOVERY_TIMEOUT_SECONDS)) {
|
||||||
|
Log.i(TAG, "Starting app launch recovery (Phase 2)")
|
||||||
|
|
||||||
|
// Step 1: Detect scenario
|
||||||
|
val scenario = detectScenario()
|
||||||
|
Log.i(TAG, "Detected scenario: $scenario")
|
||||||
|
|
||||||
|
// Step 2: Handle based on scenario
|
||||||
|
when (scenario) {
|
||||||
|
RecoveryScenario.FORCE_STOP -> {
|
||||||
|
// Phase 2: Force stop recovery (new in this phase)
|
||||||
|
val result = performForceStopRecovery()
|
||||||
|
Log.i(TAG, "Force stop recovery completed: $result")
|
||||||
|
}
|
||||||
|
RecoveryScenario.COLD_START -> {
|
||||||
|
// Phase 1: Cold start recovery (reuse existing implementation)
|
||||||
|
val result = performColdStartRecovery()
|
||||||
|
Log.i(TAG, "Cold start recovery completed: $result")
|
||||||
|
}
|
||||||
|
RecoveryScenario.BOOT -> {
|
||||||
|
// Phase 3: Boot recovery (handled via ReactivationManager)
|
||||||
|
// Boot recovery uses same logic as force stop (all alarms wiped)
|
||||||
|
val result = performForceStopRecovery()
|
||||||
|
Log.i(TAG, "Boot recovery completed: $result")
|
||||||
|
}
|
||||||
|
RecoveryScenario.NONE -> {
|
||||||
|
// No recovery needed (warm resume or first launch)
|
||||||
|
Log.d(TAG, "No recovery needed (NONE scenario)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.i(TAG, "App launch recovery completed")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Recovery failed (non-fatal): ${e.message}", e)
|
||||||
|
try {
|
||||||
|
recordRecoveryFailure(e)
|
||||||
|
} catch (historyError: Exception) {
|
||||||
|
Log.w(TAG, "Failed to record recovery failure in history", historyError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Implementation: Force Stop Recovery
|
||||||
|
|
||||||
|
### 3.1 Force Stop Recovery Method
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
/**
|
||||||
|
* Perform force stop recovery
|
||||||
|
*
|
||||||
|
* Force stop scenario: ALL alarms were cancelled by OS
|
||||||
|
* Need to:
|
||||||
|
* 1. Mark all past alarms as missed
|
||||||
|
* 2. Reschedule all future alarms
|
||||||
|
* 3. Handle both notify and fetch schedules
|
||||||
|
*
|
||||||
|
* @return RecoveryResult with counts
|
||||||
|
*/
|
||||||
|
private suspend fun performForceStopRecovery(): RecoveryResult {
|
||||||
|
val db = DailyNotificationDatabase.getDatabase(context)
|
||||||
|
val currentTime = System.currentTimeMillis()
|
||||||
|
|
||||||
|
Log.i(TAG, "Force stop recovery: recovering all schedules")
|
||||||
|
|
||||||
|
val dbSchedules = try {
|
||||||
|
db.scheduleDao().getEnabled()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to query schedules", e)
|
||||||
|
return RecoveryResult(0, 0, 0, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
var missedCount = 0
|
||||||
|
var rescheduledCount = 0
|
||||||
|
var errors = 0
|
||||||
|
|
||||||
|
dbSchedules.forEach { schedule ->
|
||||||
|
try {
|
||||||
|
when (schedule.kind) {
|
||||||
|
"notify" -> {
|
||||||
|
val result = recoverNotifySchedule(schedule, currentTime, db)
|
||||||
|
missedCount += result.missedCount
|
||||||
|
rescheduledCount += result.rescheduledCount
|
||||||
|
errors += result.errors
|
||||||
|
}
|
||||||
|
"fetch" -> {
|
||||||
|
val result = recoverFetchSchedule(schedule, currentTime, db)
|
||||||
|
rescheduledCount += result.rescheduledCount
|
||||||
|
errors += result.errors
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Log.w(TAG, "Unknown schedule kind: ${schedule.kind}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
errors++
|
||||||
|
Log.e(TAG, "Failed to recover schedule: ${schedule.id}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = RecoveryResult(
|
||||||
|
missedCount = missedCount,
|
||||||
|
rescheduledCount = rescheduledCount,
|
||||||
|
verifiedCount = 0, // Not applicable for force stop
|
||||||
|
errors = errors
|
||||||
|
)
|
||||||
|
|
||||||
|
recordRecoveryHistory(db, "force_stop", result)
|
||||||
|
|
||||||
|
Log.i(TAG, "Force stop recovery complete: $result")
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data class for schedule recovery results
|
||||||
|
*/
|
||||||
|
private data class ScheduleRecoveryResult(
|
||||||
|
val missedCount: Int = 0,
|
||||||
|
val rescheduledCount: Int = 0,
|
||||||
|
val errors: Int = 0
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 Recover Notify Schedule
|
||||||
|
|
||||||
|
**Behavior**: Handles `kind == "notify"` schedules. Reschedules via AlarmManager.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
/**
|
||||||
|
* Recover a notify schedule after force stop
|
||||||
|
*
|
||||||
|
* Handles notify schedules (kind == "notify")
|
||||||
|
*
|
||||||
|
* @param schedule Schedule to recover
|
||||||
|
* @param currentTime Current time in milliseconds
|
||||||
|
* @param db Database instance
|
||||||
|
* @return ScheduleRecoveryResult
|
||||||
|
*/
|
||||||
|
private suspend fun recoverNotifySchedule(
|
||||||
|
schedule: Schedule,
|
||||||
|
currentTime: Long,
|
||||||
|
db: DailyNotificationDatabase
|
||||||
|
): ScheduleRecoveryResult {
|
||||||
|
|
||||||
|
// Data integrity check
|
||||||
|
if (schedule.id.isBlank()) {
|
||||||
|
Log.w(TAG, "Skipping invalid schedule: empty ID")
|
||||||
|
return ScheduleRecoveryResult(errors = 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
var missedCount = 0
|
||||||
|
var rescheduledCount = 0
|
||||||
|
var errors = 0
|
||||||
|
|
||||||
|
// Calculate next run time
|
||||||
|
val nextRunTime = calculateNextRunTime(schedule, currentTime)
|
||||||
|
|
||||||
|
if (nextRunTime < currentTime) {
|
||||||
|
// Past alarm - was missed during force stop
|
||||||
|
Log.i(TAG, "Past alarm detected: ${schedule.id} scheduled for $nextRunTime")
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Mark as missed
|
||||||
|
markMissedNotification(schedule, nextRunTime, db)
|
||||||
|
missedCount++
|
||||||
|
} catch (e: Exception) {
|
||||||
|
errors++
|
||||||
|
Log.e(TAG, "Failed to mark missed notification: ${schedule.id}", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reschedule next occurrence if repeating
|
||||||
|
if (isRepeating(schedule)) {
|
||||||
|
try {
|
||||||
|
val nextOccurrence = calculateNextOccurrence(schedule, currentTime)
|
||||||
|
rescheduleAlarm(schedule, nextOccurrence, db)
|
||||||
|
rescheduledCount++
|
||||||
|
Log.i(TAG, "Rescheduled next occurrence: ${schedule.id} for $nextOccurrence")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
errors++
|
||||||
|
Log.e(TAG, "Failed to reschedule next occurrence: ${schedule.id}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Future alarm - reschedule immediately
|
||||||
|
Log.i(TAG, "Future alarm detected: ${schedule.id} scheduled for $nextRunTime")
|
||||||
|
|
||||||
|
try {
|
||||||
|
rescheduleAlarm(schedule, nextRunTime, db)
|
||||||
|
rescheduledCount++
|
||||||
|
} catch (e: Exception) {
|
||||||
|
errors++
|
||||||
|
Log.e(TAG, "Failed to reschedule future alarm: ${schedule.id}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ScheduleRecoveryResult(missedCount, rescheduledCount, errors)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 Recover Fetch Schedule
|
||||||
|
|
||||||
|
**Behavior**: Handles `kind == "fetch"` schedules. Reschedules via WorkManager.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
/**
|
||||||
|
* Recover a fetch schedule after force stop
|
||||||
|
*
|
||||||
|
* Handles fetch schedules (kind == "fetch")
|
||||||
|
*
|
||||||
|
* @param schedule Schedule to recover
|
||||||
|
* @param currentTime Current time in milliseconds
|
||||||
|
* @param db Database instance
|
||||||
|
* @return ScheduleRecoveryResult
|
||||||
|
*/
|
||||||
|
private suspend fun recoverFetchSchedule(
|
||||||
|
schedule: Schedule,
|
||||||
|
currentTime: Long,
|
||||||
|
db: DailyNotificationDatabase
|
||||||
|
): ScheduleRecoveryResult {
|
||||||
|
|
||||||
|
// Data integrity check
|
||||||
|
if (schedule.id.isBlank()) {
|
||||||
|
Log.w(TAG, "Skipping invalid schedule: empty ID")
|
||||||
|
return ScheduleRecoveryResult(errors = 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
var rescheduledCount = 0
|
||||||
|
var errors = 0
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Reschedule fetch work via WorkManager
|
||||||
|
val config = ContentFetchConfig(
|
||||||
|
enabled = schedule.enabled,
|
||||||
|
schedule = schedule.cron ?: schedule.clockTime ?: "0 9 * * *",
|
||||||
|
url = null, // Will use registered native fetcher
|
||||||
|
timeout = 30000,
|
||||||
|
retryAttempts = 3,
|
||||||
|
retryDelay = 1000,
|
||||||
|
callbacks = CallbackConfig()
|
||||||
|
)
|
||||||
|
|
||||||
|
FetchWorker.scheduleFetch(context, config)
|
||||||
|
rescheduledCount++
|
||||||
|
|
||||||
|
Log.i(TAG, "Rescheduled fetch: ${schedule.id}")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
errors++
|
||||||
|
Log.e(TAG, "Failed to reschedule fetch: ${schedule.id}", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ScheduleRecoveryResult(rescheduledCount = rescheduledCount, errors = errors)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 Helper Methods
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
/**
|
||||||
|
* Mark a notification as missed
|
||||||
|
*
|
||||||
|
* @param schedule Schedule that was missed
|
||||||
|
* @param scheduledTime When the notification was scheduled
|
||||||
|
* @param db Database instance
|
||||||
|
*/
|
||||||
|
private suspend fun markMissedNotification(
|
||||||
|
schedule: Schedule,
|
||||||
|
scheduledTime: Long,
|
||||||
|
db: DailyNotificationDatabase
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
// Try to find existing NotificationContentEntity
|
||||||
|
val notificationId = schedule.id
|
||||||
|
val existingNotification = db.notificationContentDao().getNotificationById(notificationId)
|
||||||
|
|
||||||
|
if (existingNotification != null) {
|
||||||
|
// Update existing notification
|
||||||
|
existingNotification.deliveryStatus = "missed"
|
||||||
|
existingNotification.lastDeliveryAttempt = System.currentTimeMillis()
|
||||||
|
existingNotification.deliveryAttempts = (existingNotification.deliveryAttempts ?: 0) + 1
|
||||||
|
db.notificationContentDao().updateNotification(existingNotification)
|
||||||
|
Log.d(TAG, "Updated existing notification as missed: $notificationId")
|
||||||
|
} else {
|
||||||
|
// Create missed notification entry
|
||||||
|
// Note: This may not have full content, but marks the missed event
|
||||||
|
Log.w(TAG, "No NotificationContentEntity found for schedule: $notificationId")
|
||||||
|
// Could create a minimal entry here if needed
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to mark missed notification: ${schedule.id}", e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate next run time from schedule
|
||||||
|
* Uses existing BootReceiver logic if available
|
||||||
|
*
|
||||||
|
* @param schedule Schedule to calculate for
|
||||||
|
* @param currentTime Current time in milliseconds
|
||||||
|
* @return Next run time in milliseconds
|
||||||
|
*/
|
||||||
|
private fun calculateNextRunTime(schedule: Schedule, currentTime: Long): Long {
|
||||||
|
// Prefer nextRunAt if set
|
||||||
|
if (schedule.nextRunAt != null) {
|
||||||
|
return schedule.nextRunAt!!
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate from cron or clockTime
|
||||||
|
// For now, simplified: use BootReceiver logic if available
|
||||||
|
// Otherwise, default to next day at 9 AM
|
||||||
|
return when {
|
||||||
|
schedule.cron != null -> {
|
||||||
|
// TODO: Parse cron and calculate next run
|
||||||
|
// For now, return next day at 9 AM
|
||||||
|
currentTime + (24 * 60 * 60 * 1000L)
|
||||||
|
}
|
||||||
|
schedule.clockTime != null -> {
|
||||||
|
// TODO: Parse HH:mm and calculate next run
|
||||||
|
// For now, return next day at specified time
|
||||||
|
currentTime + (24 * 60 * 60 * 1000L)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
// Default to next day at 9 AM
|
||||||
|
currentTime + (24 * 60 * 60 * 1000L)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if schedule is repeating
|
||||||
|
*
|
||||||
|
* **Helper Consistency Note**: This helper must remain consistent with any
|
||||||
|
* equivalent methods used in `BootReceiver` (Phase 3). If updated, update both places.
|
||||||
|
*
|
||||||
|
* @param schedule Schedule to check
|
||||||
|
* @return true if repeating, false if one-time
|
||||||
|
*/
|
||||||
|
private fun isRepeating(schedule: Schedule): Boolean {
|
||||||
|
// Schedules with cron or clockTime are repeating
|
||||||
|
return schedule.cron != null || schedule.clockTime != null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate next occurrence for repeating schedule
|
||||||
|
*
|
||||||
|
* **Helper Consistency Note**: This helper must remain consistent with any
|
||||||
|
* equivalent methods used in `BootReceiver` (Phase 3). If updated, update both places.
|
||||||
|
*
|
||||||
|
* @param schedule Schedule to calculate for
|
||||||
|
* @param fromTime Calculate next occurrence after this time
|
||||||
|
* @return Next occurrence time in milliseconds
|
||||||
|
*/
|
||||||
|
private fun calculateNextOccurrence(schedule: Schedule, fromTime: Long): Long {
|
||||||
|
// TODO: Implement proper calculation based on cron/clockTime
|
||||||
|
// For now, simplified: daily schedules add 24 hours
|
||||||
|
return fromTime + (24 * 60 * 60 * 1000L)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Data Integrity Checks
|
||||||
|
|
||||||
|
### 4.1 Force Stop Detection Validation
|
||||||
|
|
||||||
|
**False Positive Prevention**:
|
||||||
|
- ✅ First launch: `DB schedules count == 0` → Not force stop
|
||||||
|
- ✅ Normal cold start: `AlarmManager has alarms` → Not force stop
|
||||||
|
- ✅ Only detect force stop when: `DB schedules > 0 && AlarmManager alarms == 0`
|
||||||
|
|
||||||
|
**Edge Cases**:
|
||||||
|
- ✅ All alarms already fired: Still detect as force stop if AlarmManager is empty
|
||||||
|
- ✅ Partial alarm cancellation: Not detected as force stop (handled by cold start recovery)
|
||||||
|
|
||||||
|
### 4.2 Schedule Validation
|
||||||
|
|
||||||
|
**Notify Schedule Validation**:
|
||||||
|
- ✅ `id` must not be blank
|
||||||
|
- ✅ `kind` must be "notify"
|
||||||
|
- ✅ `nextRunAt` or `cron`/`clockTime` must be set
|
||||||
|
|
||||||
|
**Fetch Schedule Validation**:
|
||||||
|
- ✅ `id` must not be blank
|
||||||
|
- ✅ `kind` must be "fetch"
|
||||||
|
- ✅ `cron` or `clockTime` must be set
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Rollback Safety
|
||||||
|
|
||||||
|
### 5.1 No-Crash Guarantee
|
||||||
|
|
||||||
|
**All force stop recovery operations must:**
|
||||||
|
|
||||||
|
1. **Catch all exceptions** - Never propagate exceptions to app
|
||||||
|
2. **Continue processing** - One schedule failure doesn't stop recovery
|
||||||
|
3. **Log errors** - All failures logged with context
|
||||||
|
4. **Partial recovery** - Some schedules can recover even if others fail
|
||||||
|
|
||||||
|
### 5.2 Error Handling Strategy
|
||||||
|
|
||||||
|
| Error Type | Handling | Log Level |
|
||||||
|
|------------|----------|-----------|
|
||||||
|
| Schedule query failure | Return empty result, log error | ERROR |
|
||||||
|
| Invalid schedule data | Skip schedule, continue | WARN |
|
||||||
|
| Alarm reschedule failure | Log error, continue to next | ERROR |
|
||||||
|
| Fetch reschedule failure | Log error, continue to next | ERROR |
|
||||||
|
| Missed notification marking failure | Log error, continue | ERROR |
|
||||||
|
| History recording failure | Log warning, don't fail | WARN |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Testing Requirements
|
||||||
|
|
||||||
|
### 6.1 Test 1: Force Stop Detection
|
||||||
|
|
||||||
|
**Purpose**: Verify force stop scenario is detected correctly.
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
1. Schedule 3 notifications (2 minutes, 5 minutes, 10 minutes in future)
|
||||||
|
2. Verify alarms scheduled: `adb shell dumpsys alarm | grep timesafari`
|
||||||
|
3. Force stop app: `adb shell am force-stop com.timesafari.dailynotification`
|
||||||
|
4. Verify alarms cancelled: `adb shell dumpsys alarm | grep timesafari` (should be empty)
|
||||||
|
5. Launch app: `adb shell am start -n com.timesafari.dailynotification/.MainActivity`
|
||||||
|
6. Check logs: `adb logcat -d | grep DNP-REACTIVATION`
|
||||||
|
|
||||||
|
**Expected**:
|
||||||
|
- ✅ Log shows "Force stop detected: DB has X schedules, AlarmManager has 0 alarms"
|
||||||
|
- ✅ Log shows "Detected scenario: FORCE_STOP"
|
||||||
|
- ✅ Log shows "Force stop recovery: recovering all schedules"
|
||||||
|
|
||||||
|
**Pass Criteria**: Force stop correctly detected.
|
||||||
|
|
||||||
|
### 6.2 Test 2: Past Alarm Recovery
|
||||||
|
|
||||||
|
**Purpose**: Verify past alarms are marked as missed.
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
1. Schedule notification for 2 minutes in future
|
||||||
|
2. Force stop app
|
||||||
|
3. Wait 5 minutes (past scheduled time)
|
||||||
|
4. Launch app
|
||||||
|
5. Check database: `delivery_status = 'missed'` for past alarm
|
||||||
|
|
||||||
|
**Expected**:
|
||||||
|
- ✅ Past alarm marked as missed in database
|
||||||
|
- ✅ History entry created
|
||||||
|
- ✅ Log shows "Past alarm detected" and "Marked missed notification"
|
||||||
|
|
||||||
|
**Pass Criteria**: Past alarms correctly marked as missed.
|
||||||
|
|
||||||
|
### 6.3 Test 3: Future Alarm Recovery
|
||||||
|
|
||||||
|
**Purpose**: Verify future alarms are rescheduled.
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
1. Schedule 3 notifications (5, 10, 15 minutes in future)
|
||||||
|
2. Force stop app
|
||||||
|
3. Launch app immediately
|
||||||
|
4. Verify alarms rescheduled: `adb shell dumpsys alarm | grep timesafari`
|
||||||
|
|
||||||
|
**Expected**:
|
||||||
|
- ✅ All 3 alarms rescheduled in AlarmManager
|
||||||
|
- ✅ Log shows "Future alarm detected" and "Rescheduled alarm"
|
||||||
|
- ✅ No duplicate alarms created
|
||||||
|
|
||||||
|
**Pass Criteria**: Future alarms correctly rescheduled.
|
||||||
|
|
||||||
|
### 6.4 Test 4: Repeating Schedule Recovery
|
||||||
|
|
||||||
|
**Purpose**: Verify repeating schedules calculate next occurrence correctly.
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
1. Schedule daily notification (cron: "0 9 * * *")
|
||||||
|
2. Force stop app
|
||||||
|
3. Wait past scheduled time (e.g., wait until 10 AM)
|
||||||
|
4. Launch app
|
||||||
|
5. Verify next occurrence scheduled for tomorrow 9 AM
|
||||||
|
|
||||||
|
**Expected**:
|
||||||
|
- ✅ Past occurrence marked as missed
|
||||||
|
- ✅ Next occurrence scheduled for tomorrow
|
||||||
|
- ✅ Log shows "Rescheduled next occurrence"
|
||||||
|
|
||||||
|
**Pass Criteria**: Repeating schedules correctly calculate next occurrence.
|
||||||
|
|
||||||
|
### 6.5 Test 5: Fetch Schedule Recovery
|
||||||
|
|
||||||
|
**Purpose**: Verify fetch schedules are recovered.
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
1. Schedule fetch work (cron: "0 9 * * *")
|
||||||
|
2. Force stop app
|
||||||
|
3. Launch app
|
||||||
|
4. Check WorkManager: `adb shell dumpsys jobscheduler | grep timesafari`
|
||||||
|
|
||||||
|
**Expected**:
|
||||||
|
- ✅ Fetch work rescheduled in WorkManager
|
||||||
|
- ✅ Log shows "Rescheduled fetch"
|
||||||
|
|
||||||
|
**Pass Criteria**: Fetch schedules correctly recovered.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Implementation Checklist
|
||||||
|
|
||||||
|
- [ ] Add `detectScenario()` method to ReactivationManager
|
||||||
|
- [ ] Add `alarmsExist()` method (replaces getActiveAlarmCount)
|
||||||
|
- [ ] Add `isBootRecovery()` method
|
||||||
|
- [ ] Add `RecoveryScenario` enum
|
||||||
|
- [ ] Update `performRecovery()` to handle force stop
|
||||||
|
- [ ] Implement `performForceStopRecovery()`
|
||||||
|
- [ ] Implement `recoverNotifySchedule()`
|
||||||
|
- [ ] Implement `recoverFetchSchedule()`
|
||||||
|
- [ ] Implement `markMissedNotification()`
|
||||||
|
- [ ] Implement `calculateNextRunTime()` (or reuse BootReceiver logic)
|
||||||
|
- [ ] Implement `isRepeating()`
|
||||||
|
- [ ] Implement `calculateNextOccurrence()`
|
||||||
|
- [ ] Add data integrity checks
|
||||||
|
- [ ] Add error handling
|
||||||
|
- [ ] Test force stop detection
|
||||||
|
- [ ] Test past alarm recovery
|
||||||
|
- [ ] Test future alarm recovery
|
||||||
|
- [ ] Test repeating schedule recovery
|
||||||
|
- [ ] Test fetch schedule recovery
|
||||||
|
- [ ] Verify no duplicate alarms
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Code References
|
||||||
|
|
||||||
|
**Existing Code to Reuse**:
|
||||||
|
- `NotifyReceiver.scheduleExactNotification()` - Line 92
|
||||||
|
- `FetchWorker.scheduleFetch()` - Line 31
|
||||||
|
- `BootReceiver.calculateNextRunTime()` - Line 103 (for next run calculation)
|
||||||
|
- `ScheduleDao.getEnabled()` - Line 298
|
||||||
|
- `NotificationContentDao.getNotificationById()` - Line 69
|
||||||
|
|
||||||
|
**New Code to Create**:
|
||||||
|
- `detectScenario()` - Add to ReactivationManager
|
||||||
|
- `alarmsExist()` - Add to ReactivationManager (replaces getActiveAlarmCount)
|
||||||
|
- `isBootRecovery()` - Add to ReactivationManager
|
||||||
|
- `performForceStopRecovery()` - Add to ReactivationManager
|
||||||
|
- `recoverNotifySchedule()` - Add to ReactivationManager
|
||||||
|
- `recoverFetchSchedule()` - Add to ReactivationManager
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Success Criteria Summary
|
||||||
|
|
||||||
|
**Phase 2 is complete when:**
|
||||||
|
|
||||||
|
1. ✅ Force stop scenario detected correctly
|
||||||
|
2. ✅ All past alarms marked as missed
|
||||||
|
3. ✅ All future alarms rescheduled
|
||||||
|
4. ✅ Both notify and fetch schedules recovered
|
||||||
|
5. ✅ Repeating schedules calculate next occurrence correctly
|
||||||
|
6. ✅ Recovery never crashes app
|
||||||
|
7. ✅ All tests pass
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [Phase 1: Cold Start Recovery](./android-implementation-directive-phase1.md) - Prerequisite
|
||||||
|
- [Full Implementation Directive](./android-implementation-directive.md) - Complete scope
|
||||||
|
- [Exploration Findings](./exploration-findings-initial.md) - Gap analysis
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- **Prerequisite**: Phase 1 must be complete before starting Phase 2
|
||||||
|
- **Detection accuracy**: Force stop detection uses best available method (nextAlarmClock)
|
||||||
|
- **Comprehensive recovery**: Force stop recovery handles ALL schedules (past and future)
|
||||||
|
- **Safety first**: All recovery operations are non-blocking and non-fatal
|
||||||
|
|
||||||
619
docs/android-implementation-directive-phase3.md
Normal file
619
docs/android-implementation-directive-phase3.md
Normal file
@@ -0,0 +1,619 @@
|
|||||||
|
# Android Implementation Directive: Phase 3 - Boot Receiver Missed Alarm Handling
|
||||||
|
|
||||||
|
**Author**: Matthew Raymer
|
||||||
|
**Date**: November 2025
|
||||||
|
**Status**: Phase 3 - Boot Recovery Enhancement
|
||||||
|
**Version**: 1.0.0
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Phase 3 enhances the **boot receiver** to detect and handle missed alarms during device reboot. This handles alarms that were scheduled before reboot but were missed because alarms are wiped on reboot.
|
||||||
|
|
||||||
|
**Prerequisites**: Phase 1 and Phase 2 must be complete.
|
||||||
|
|
||||||
|
**Scope**: Boot receiver missed alarm detection and handling.
|
||||||
|
|
||||||
|
**Dependencies**: Boot receiver behavior assumes that Phase 1 and Phase 2 definitions of 'missed alarm', 'next occurrence', and `Schedule`/`NotificationContentEntity` semantics are already in place.
|
||||||
|
|
||||||
|
**Reference**: See [Phase 1](./android-implementation-directive-phase1.md) and [Phase 2](./android-implementation-directive-phase2.md) for app launch recovery, [Full Implementation Directive](./android-implementation-directive.md) for complete scope.
|
||||||
|
|
||||||
|
**Boot vs App Launch Recovery**:
|
||||||
|
|
||||||
|
| Scenario | Entry point | Directive | Responsibility |
|
||||||
|
| -------------------------------- | --------------------------------------- | --------- | ---------------------------------------- |
|
||||||
|
| App launch after kill/force-stop | `ReactivationManager.performRecovery()` | Phase 1–2 | Detect & recover missed |
|
||||||
|
| Device reboot | `BootReceiver` → `ReactivationManager` | Phase 3 | Queue recovery, ReactivationManager handles |
|
||||||
|
|
||||||
|
**User-Facing Behavior**: In Phase 3, missed alarms are **recorded** and **rescheduled**, but not yet surfaced to the user with explicit "you missed this" UX (that's a future concern).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Acceptance Criteria
|
||||||
|
|
||||||
|
### 1.1 Definition of Done
|
||||||
|
|
||||||
|
**Phase 3 is complete when:**
|
||||||
|
|
||||||
|
1. ✅ **Boot receiver detects missed alarms**
|
||||||
|
- Alarms with `nextRunAt < currentTime` detected during boot recovery
|
||||||
|
- Detection runs automatically on `BOOT_COMPLETED` intent
|
||||||
|
- Detection completes within 5 seconds (boot receiver timeout)
|
||||||
|
|
||||||
|
2. ✅ **Missed alarms are marked in database**
|
||||||
|
- `delivery_status` updated to `'missed'`
|
||||||
|
- `last_delivery_attempt` updated to current time
|
||||||
|
- Status change logged in history table
|
||||||
|
|
||||||
|
3. ✅ **Next occurrence is rescheduled for repeating schedules**
|
||||||
|
- Repeating schedules calculate next occurrence after missed time
|
||||||
|
- Next occurrence scheduled via AlarmManager
|
||||||
|
- Non-repeating schedules not rescheduled
|
||||||
|
|
||||||
|
4. ✅ **Future alarms are rescheduled**
|
||||||
|
- All future alarms (not missed) rescheduled normally
|
||||||
|
- Existing boot receiver logic enhanced, not replaced
|
||||||
|
|
||||||
|
5. ✅ **Boot recovery never crashes**
|
||||||
|
- All exceptions caught and logged
|
||||||
|
- Database errors don't propagate
|
||||||
|
- Invalid data handled gracefully
|
||||||
|
|
||||||
|
### 1.2 Success Metrics
|
||||||
|
|
||||||
|
| Metric | Target | Measurement |
|
||||||
|
|--------|--------|-------------|
|
||||||
|
| Boot receiver execution time | < 5 seconds | Log timestamp difference |
|
||||||
|
| Missed detection accuracy | 100% | Manual verification via logs |
|
||||||
|
| Next occurrence calculation accuracy | 100% | Verify scheduled time matches expected |
|
||||||
|
| Recovery success rate | > 95% | History table outcome field |
|
||||||
|
| Crash rate | 0% | No exceptions propagate |
|
||||||
|
|
||||||
|
### 1.3 Out of Scope (Phase 3)
|
||||||
|
|
||||||
|
- ❌ Warm start optimization (future phase)
|
||||||
|
- ❌ Callback event emission (future phase)
|
||||||
|
- ❌ User notification of missed alarms (future phase)
|
||||||
|
- ❌ Boot receiver performance optimization (future phase)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Implementation: BootReceiver Enhancement
|
||||||
|
|
||||||
|
### 2.1 Canonical Source of Truth
|
||||||
|
|
||||||
|
**⚠️ CRITICAL CORRECTION**: BootReceiver must **NOT** implement recovery logic directly. It must **only queue** ReactivationManager.performRecovery() with a BOOT flag.
|
||||||
|
|
||||||
|
**ReactivationManager.kt** is the **only** file allowed to:
|
||||||
|
- Perform scenario detection
|
||||||
|
- Initiate recovery logic
|
||||||
|
- Branch execution per phase
|
||||||
|
|
||||||
|
### 2.2 Update BootReceiver
|
||||||
|
|
||||||
|
**File**: `android/src/main/java/com/timesafari/dailynotification/BootReceiver.kt`
|
||||||
|
|
||||||
|
**Location**: `onReceive()` method
|
||||||
|
|
||||||
|
### 2.3 Corrected Implementation
|
||||||
|
|
||||||
|
**Corrected Code** (BootReceiver only queues recovery):
|
||||||
|
```kotlin
|
||||||
|
package com.timesafari.dailynotification
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.util.Log
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Boot recovery receiver to trigger recovery after device reboot
|
||||||
|
* Phase 3: Only queues ReactivationManager, does not implement recovery directly
|
||||||
|
*
|
||||||
|
* @author Matthew Raymer
|
||||||
|
* @version 2.0.0 - Corrected to queue ReactivationManager only
|
||||||
|
*/
|
||||||
|
class BootReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "DNP-BOOT"
|
||||||
|
private const val PREFS_NAME = "dailynotification_recovery"
|
||||||
|
private const val KEY_LAST_BOOT_AT = "last_boot_at"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onReceive(context: Context, intent: Intent?) {
|
||||||
|
if (intent?.action == Intent.ACTION_BOOT_COMPLETED) {
|
||||||
|
Log.i(TAG, "Boot completed, queuing ReactivationManager recovery")
|
||||||
|
|
||||||
|
// Set boot flag in SharedPreferences
|
||||||
|
// ReactivationManager will detect this and handle recovery
|
||||||
|
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
prefs.edit().putLong(KEY_LAST_BOOT_AT, System.currentTimeMillis()).apply()
|
||||||
|
|
||||||
|
// Queue ReactivationManager recovery
|
||||||
|
// Recovery will run when app launches or can be triggered immediately
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
try {
|
||||||
|
val reactivationManager = ReactivationManager(context)
|
||||||
|
reactivationManager.performRecovery()
|
||||||
|
Log.i(TAG, "Boot recovery queued to ReactivationManager")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to queue boot recovery", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**⚠️ REMOVED**: All direct rescheduling logic from BootReceiver. Recovery is now handled entirely by ReactivationManager.
|
||||||
|
|
||||||
|
### 2.4 How Boot Recovery Works
|
||||||
|
|
||||||
|
**Flow**:
|
||||||
|
1. Device reboots → `BootReceiver.onReceive()` called
|
||||||
|
2. BootReceiver sets `last_boot_at` flag in SharedPreferences
|
||||||
|
3. BootReceiver queues `ReactivationManager.performRecovery()`
|
||||||
|
4. ReactivationManager detects BOOT scenario via `isBootRecovery()`
|
||||||
|
5. ReactivationManager handles recovery (same logic as force stop - all alarms wiped)
|
||||||
|
|
||||||
|
**Key Points**:
|
||||||
|
- BootReceiver **never** implements recovery directly
|
||||||
|
- All recovery logic is in ReactivationManager
|
||||||
|
- Boot recovery uses same recovery path as force stop (all alarms wiped on reboot)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Data Integrity Checks
|
||||||
|
```kotlin
|
||||||
|
/**
|
||||||
|
* Reschedule notifications after device reboot
|
||||||
|
* Phase 3: Adds missed alarm detection and handling
|
||||||
|
*
|
||||||
|
* @param context Application context
|
||||||
|
*/
|
||||||
|
private suspend fun rescheduleNotifications(context: Context) {
|
||||||
|
val db = DailyNotificationDatabase.getDatabase(context)
|
||||||
|
val enabledSchedules = db.scheduleDao().getEnabled()
|
||||||
|
val currentTime = System.currentTimeMillis()
|
||||||
|
|
||||||
|
Log.i(TAG, "Boot recovery: Found ${enabledSchedules.size} enabled schedules to reschedule")
|
||||||
|
|
||||||
|
var futureRescheduled = 0
|
||||||
|
var missedDetected = 0
|
||||||
|
var missedRescheduled = 0
|
||||||
|
var errors = 0
|
||||||
|
|
||||||
|
enabledSchedules.forEach { schedule ->
|
||||||
|
try {
|
||||||
|
when (schedule.kind) {
|
||||||
|
"fetch" -> {
|
||||||
|
// Reschedule WorkManager fetch (unchanged)
|
||||||
|
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" -> {
|
||||||
|
// Phase 3: Handle both past and future alarms
|
||||||
|
val nextRunTime = calculateNextRunTime(schedule)
|
||||||
|
|
||||||
|
if (nextRunTime > currentTime) {
|
||||||
|
// Future alarm - reschedule normally
|
||||||
|
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 future notification: ${schedule.id} for $nextRunTime")
|
||||||
|
futureRescheduled++
|
||||||
|
} else {
|
||||||
|
// Past alarm - was missed during reboot
|
||||||
|
missedDetected++
|
||||||
|
Log.i(TAG, "Missed alarm detected on boot: ${schedule.id} scheduled for $nextRunTime")
|
||||||
|
|
||||||
|
// Mark as missed
|
||||||
|
handleMissedAlarmOnBoot(context, schedule, nextRunTime, db)
|
||||||
|
|
||||||
|
// Reschedule next occurrence if repeating
|
||||||
|
if (isRepeating(schedule)) {
|
||||||
|
try {
|
||||||
|
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} for $nextOccurrence")
|
||||||
|
missedRescheduled++
|
||||||
|
} catch (e: Exception) {
|
||||||
|
errors++
|
||||||
|
Log.e(TAG, "Failed to reschedule next occurrence: ${schedule.id}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Log.w(TAG, "Unknown schedule kind: ${schedule.kind}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
errors++
|
||||||
|
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 = if (errors == 0) "success" else "partial",
|
||||||
|
diagJson = """
|
||||||
|
{
|
||||||
|
"schedules_rescheduled": $futureRescheduled,
|
||||||
|
"missed_detected": $missedDetected,
|
||||||
|
"missed_rescheduled": $missedRescheduled,
|
||||||
|
"errors": $errors
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to record boot recovery history", e)
|
||||||
|
// Don't fail boot recovery if history recording fails
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.i(TAG, "Boot recovery complete: $futureRescheduled future, $missedDetected missed, $missedRescheduled next occurrences, $errors errors")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note**: All data integrity checks are handled by ReactivationManager (Phase 2). BootReceiver does not perform any data operations directly.
|
||||||
|
|
||||||
|
### 3.1 Missed Alarm Detection Validation
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
/**
|
||||||
|
* Handle missed alarm detected during boot recovery
|
||||||
|
* Phase 3: Marks missed alarm in database
|
||||||
|
*
|
||||||
|
* @param context Application context
|
||||||
|
* @param schedule Schedule that was missed
|
||||||
|
* @param scheduledTime When the alarm was scheduled
|
||||||
|
* @param db Database instance
|
||||||
|
*/
|
||||||
|
private suspend fun handleMissedAlarmOnBoot(
|
||||||
|
context: Context,
|
||||||
|
schedule: Schedule,
|
||||||
|
scheduledTime: Long,
|
||||||
|
db: DailyNotificationDatabase
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
// Data integrity check
|
||||||
|
if (schedule.id.isBlank()) {
|
||||||
|
Log.w(TAG, "Skipping invalid schedule: empty ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to find existing NotificationContentEntity
|
||||||
|
val notificationId = schedule.id
|
||||||
|
val existingNotification = db.notificationContentDao().getNotificationById(notificationId)
|
||||||
|
|
||||||
|
if (existingNotification != null) {
|
||||||
|
// Update existing notification
|
||||||
|
existingNotification.deliveryStatus = "missed"
|
||||||
|
existingNotification.lastDeliveryAttempt = System.currentTimeMillis()
|
||||||
|
existingNotification.deliveryAttempts = (existingNotification.deliveryAttempts ?: 0) + 1
|
||||||
|
db.notificationContentDao().updateNotification(existingNotification)
|
||||||
|
Log.d(TAG, "Marked missed notification on boot: $notificationId")
|
||||||
|
} else {
|
||||||
|
// No NotificationContentEntity found - this is okay for boot recovery
|
||||||
|
// The schedule exists but content may not have been fetched yet
|
||||||
|
Log.d(TAG, "No NotificationContentEntity found for missed schedule: $notificationId (expected for boot recovery)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record missed alarm in history
|
||||||
|
try {
|
||||||
|
db.historyDao().insert(
|
||||||
|
History(
|
||||||
|
refId = "missed_boot_${schedule.id}_${System.currentTimeMillis()}",
|
||||||
|
kind = "missed_alarm",
|
||||||
|
occurredAt = System.currentTimeMillis(),
|
||||||
|
outcome = "missed",
|
||||||
|
diagJson = """
|
||||||
|
{
|
||||||
|
"schedule_id": "${schedule.id}",
|
||||||
|
"scheduled_time": $scheduledTime,
|
||||||
|
"detected_at": ${System.currentTimeMillis()},
|
||||||
|
"scenario": "boot_recovery"
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Failed to record missed alarm history", e)
|
||||||
|
// Don't fail if history recording fails
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to handle missed alarm on boot: ${schedule.id}", e)
|
||||||
|
// Don't throw - continue with boot recovery
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.5 Helper Methods
|
||||||
|
|
||||||
|
**⚠️ Implementation Consistency**: These helpers must match the implementation used in `ReactivationManager` (Phase 2). Treat ReactivationManager as canonical and keep these in sync.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
/**
|
||||||
|
* Check if schedule is repeating
|
||||||
|
*
|
||||||
|
* **Implementation Note**: Must match `isRepeating()` in ReactivationManager (Phase 2).
|
||||||
|
* This is a duplication for now; treat ReactivationManager as canonical.
|
||||||
|
*
|
||||||
|
* @param schedule Schedule to check
|
||||||
|
* @return true if repeating, false if one-time
|
||||||
|
*/
|
||||||
|
private fun isRepeating(schedule: Schedule): Boolean {
|
||||||
|
// Schedules with cron or clockTime are repeating
|
||||||
|
return schedule.cron != null || schedule.clockTime != null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate next occurrence for repeating schedule
|
||||||
|
*
|
||||||
|
* **Implementation Note**: Must match `calculateNextOccurrence()` in ReactivationManager (Phase 2).
|
||||||
|
* This is a duplication for now; treat ReactivationManager as canonical.
|
||||||
|
*
|
||||||
|
* @param schedule Schedule to calculate for
|
||||||
|
* @param fromTime Calculate next occurrence after this time
|
||||||
|
* @return Next occurrence time in milliseconds
|
||||||
|
*/
|
||||||
|
private fun calculateNextOccurrence(schedule: Schedule, fromTime: Long): Long {
|
||||||
|
// TODO: Implement proper calculation based on cron/clockTime
|
||||||
|
// For now, simplified: daily schedules add 24 hours
|
||||||
|
// This should match the logic in ReactivationManager (Phase 2)
|
||||||
|
return when {
|
||||||
|
schedule.cron != null -> {
|
||||||
|
// Parse cron and calculate next occurrence
|
||||||
|
// For now, simplified: daily schedules
|
||||||
|
fromTime + (24 * 60 * 60 * 1000L)
|
||||||
|
}
|
||||||
|
schedule.clockTime != null -> {
|
||||||
|
// Parse HH:mm and calculate next occurrence
|
||||||
|
// For now, simplified: daily schedules
|
||||||
|
fromTime + (24 * 60 * 60 * 1000L)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
// Not repeating
|
||||||
|
fromTime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Data Integrity Checks
|
||||||
|
|
||||||
|
### 3.1 Missed Alarm Detection Validation
|
||||||
|
|
||||||
|
**Boot Flag Rules**:
|
||||||
|
- ✅ BootReceiver sets flag immediately on BOOT_COMPLETED
|
||||||
|
- ✅ Flag is valid for 60 seconds after boot
|
||||||
|
- ✅ ReactivationManager clears flag after reading
|
||||||
|
- ✅ Stale flags are ignored (prevents false positives)
|
||||||
|
|
||||||
|
**Edge Cases**:
|
||||||
|
- ✅ Multiple boot broadcasts: Flag is overwritten (last one wins)
|
||||||
|
- ✅ App not launched after boot: Flag expires after 60 seconds
|
||||||
|
- ✅ SharedPreferences errors: Log error, recovery continues
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Rollback Safety
|
||||||
|
|
||||||
|
### 4.1 No-Crash Guarantee
|
||||||
|
|
||||||
|
**All boot recovery operations must:**
|
||||||
|
|
||||||
|
1. **Catch all exceptions** - Never propagate exceptions
|
||||||
|
2. **Continue processing** - One schedule failure doesn't stop recovery
|
||||||
|
3. **Log errors** - All failures logged with context
|
||||||
|
4. **Timeout protection** - Boot receiver has 10-second timeout (Android limit)
|
||||||
|
|
||||||
|
### 4.2 Error Handling Strategy
|
||||||
|
|
||||||
|
| Error Type | Handling | Log Level |
|
||||||
|
|------------|----------|-----------|
|
||||||
|
| Schedule query failure | Return empty list, log error | ERROR |
|
||||||
|
| Invalid schedule data | Skip schedule, continue | WARN |
|
||||||
|
| Missed alarm marking failure | Log error, continue | ERROR |
|
||||||
|
| Next occurrence calculation failure | Log error, don't reschedule | ERROR |
|
||||||
|
| Alarm reschedule failure | Log error, continue | ERROR |
|
||||||
|
| History recording failure | Log warning, don't fail | WARN |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Testing Requirements
|
||||||
|
|
||||||
|
### 5.1 Test 1: Boot Recovery Missed Detection
|
||||||
|
|
||||||
|
**Purpose**: Verify boot receiver detects missed alarms.
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
1. Schedule notification for 5 minutes in future
|
||||||
|
2. Verify alarm scheduled: `adb shell dumpsys alarm | grep timesafari`
|
||||||
|
3. Reboot device: `adb reboot`
|
||||||
|
4. Wait for boot: `adb wait-for-device && adb shell getprop sys.boot_completed` (wait for "1")
|
||||||
|
5. Wait 10 minutes (past scheduled time)
|
||||||
|
6. Check boot logs: `adb logcat -d | grep DNP-BOOT`
|
||||||
|
|
||||||
|
**Expected**:
|
||||||
|
- ✅ Log shows "Boot recovery: Found X enabled schedules"
|
||||||
|
- ✅ Log shows "Missed alarm detected on boot: <id>"
|
||||||
|
- ✅ Database shows `delivery_status = 'missed'`
|
||||||
|
|
||||||
|
**Pass Criteria**: Missed alarm detected during boot recovery.
|
||||||
|
|
||||||
|
### 5.2 Test 2: Next Occurrence Rescheduling
|
||||||
|
|
||||||
|
**Purpose**: Verify repeating schedules calculate next occurrence correctly.
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
1. Schedule daily notification (cron: "0 9 * * *") for today at 9 AM
|
||||||
|
2. Reboot device
|
||||||
|
3. Wait until 10 AM (past scheduled time)
|
||||||
|
4. Check boot logs
|
||||||
|
5. Verify next occurrence scheduled: `adb shell dumpsys alarm | grep timesafari`
|
||||||
|
|
||||||
|
**Expected**:
|
||||||
|
- ✅ Log shows "Missed alarm detected on boot"
|
||||||
|
- ✅ Log shows "Rescheduled next occurrence for missed alarm"
|
||||||
|
- ✅ AlarmManager shows alarm scheduled for tomorrow 9 AM
|
||||||
|
|
||||||
|
**Pass Criteria**: Next occurrence correctly calculated and scheduled.
|
||||||
|
|
||||||
|
### 5.3 Test 3: Future Alarm Rescheduling
|
||||||
|
|
||||||
|
**Purpose**: Verify future alarms are still rescheduled normally.
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
1. Schedule notification for 1 hour in future
|
||||||
|
2. Reboot device
|
||||||
|
3. Wait for boot
|
||||||
|
4. Verify alarm rescheduled: `adb shell dumpsys alarm | grep timesafari`
|
||||||
|
|
||||||
|
**Expected**:
|
||||||
|
- ✅ Log shows "Rescheduled future notification"
|
||||||
|
- ✅ AlarmManager shows alarm scheduled for original time
|
||||||
|
- ✅ No missed alarm detection for future alarms
|
||||||
|
|
||||||
|
**Pass Criteria**: Future alarms rescheduled normally.
|
||||||
|
|
||||||
|
### 5.4 Test 4: Non-Repeating Schedule Handling
|
||||||
|
|
||||||
|
**Purpose**: Verify non-repeating schedules don't reschedule next occurrence.
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
1. Schedule one-time notification (no cron/clockTime) for 5 minutes in future
|
||||||
|
2. Reboot device
|
||||||
|
3. Wait 10 minutes (past scheduled time)
|
||||||
|
4. Check boot logs
|
||||||
|
5. Verify no next occurrence scheduled
|
||||||
|
|
||||||
|
**Expected**:
|
||||||
|
- ✅ Log shows "Missed alarm detected on boot"
|
||||||
|
- ✅ Log does NOT show "Rescheduled next occurrence"
|
||||||
|
- ✅ AlarmManager does NOT show new alarm
|
||||||
|
|
||||||
|
**Pass Criteria**: Non-repeating schedules don't reschedule.
|
||||||
|
|
||||||
|
### 5.5 Test 5: Boot Recovery Error Handling
|
||||||
|
|
||||||
|
**Purpose**: Verify boot recovery handles errors gracefully.
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
1. Manually corrupt database (insert invalid schedule)
|
||||||
|
2. Reboot device
|
||||||
|
3. Check boot logs
|
||||||
|
|
||||||
|
**Expected**:
|
||||||
|
- ✅ Invalid schedule skipped with warning
|
||||||
|
- ✅ Boot recovery continues normally
|
||||||
|
- ✅ Valid schedules still recovered
|
||||||
|
- ✅ No crash or exception
|
||||||
|
|
||||||
|
**Pass Criteria**: Errors handled gracefully, recovery continues.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Implementation Checklist
|
||||||
|
|
||||||
|
- [ ] Update `BootReceiver.onReceive()` to set boot flag
|
||||||
|
- [ ] Update `BootReceiver.onReceive()` to queue ReactivationManager
|
||||||
|
- [ ] Remove all direct rescheduling logic from BootReceiver
|
||||||
|
- [ ] Verify ReactivationManager detects BOOT scenario correctly
|
||||||
|
- [ ] Update history recording to include missed alarm counts
|
||||||
|
- [ ] Add data integrity checks
|
||||||
|
- [ ] Add error handling
|
||||||
|
- [ ] Test boot recovery missed detection
|
||||||
|
- [ ] Test next occurrence rescheduling
|
||||||
|
- [ ] Test future alarm rescheduling
|
||||||
|
- [ ] Test non-repeating schedule handling
|
||||||
|
- [ ] Test error handling
|
||||||
|
- [ ] Verify no duplicate alarms
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Code References
|
||||||
|
|
||||||
|
**Existing Code to Reuse**:
|
||||||
|
- `BootReceiver.rescheduleNotifications()` - Line 38 (update existing)
|
||||||
|
- `BootReceiver.calculateNextRunTime()` - Line 103 (already exists)
|
||||||
|
- `NotifyReceiver.scheduleExactNotification()` - Line 92
|
||||||
|
- `ScheduleDao.getEnabled()` - Line 298
|
||||||
|
- `NotificationContentDao.getNotificationById()` - Line 69
|
||||||
|
|
||||||
|
**New Code to Create**:
|
||||||
|
- `handleMissedAlarmOnBoot()` - Add to BootReceiver
|
||||||
|
- `isRepeating()` - Add to BootReceiver (or reuse from ReactivationManager)
|
||||||
|
- `calculateNextOccurrence()` - Add to BootReceiver (or reuse from ReactivationManager)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Success Criteria Summary
|
||||||
|
|
||||||
|
**Phase 3 is complete when:**
|
||||||
|
|
||||||
|
1. ✅ Boot receiver detects missed alarms
|
||||||
|
2. ✅ Missed alarms marked in database
|
||||||
|
3. ✅ Next occurrence rescheduled for repeating schedules
|
||||||
|
4. ✅ Future alarms rescheduled normally
|
||||||
|
5. ✅ Boot recovery never crashes
|
||||||
|
6. ✅ All tests pass
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [Phase 1: Cold Start Recovery](./android-implementation-directive-phase1.md) - Prerequisite
|
||||||
|
- [Phase 2: Force Stop Recovery](./android-implementation-directive-phase2.md) - Prerequisite
|
||||||
|
- [Full Implementation Directive](./android-implementation-directive.md) - Complete scope
|
||||||
|
- [Exploration Findings](./exploration-findings-initial.md) - Gap analysis
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- **Prerequisites**: Phase 1 and Phase 2 must be complete before starting Phase 3
|
||||||
|
- **Boot receiver timeout**: Android limits boot receiver execution to 10 seconds
|
||||||
|
- **Comprehensive recovery**: Boot recovery handles both missed and future alarms
|
||||||
|
- **Safety first**: All recovery operations are non-blocking and non-fatal
|
||||||
|
- **Code reuse**: Consider extracting helper methods to shared utility class
|
||||||
|
|
||||||
@@ -6,15 +6,29 @@
|
|||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
|
|
||||||
This directive provides **step-by-step implementation guidance** for Android-specific gaps identified in the exploration:
|
This directive provides **descriptive overview and integration guidance** for Android-specific gaps identified in the exploration:
|
||||||
|
|
||||||
1. App Launch Recovery (cold/warm/force-stop)
|
1. App Launch Recovery (cold/warm/force-stop)
|
||||||
2. Missed Alarm Detection
|
2. Missed Alarm Detection
|
||||||
3. Force Stop Detection
|
3. Force Stop Detection
|
||||||
4. Boot Receiver Missed Alarm Handling
|
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 [Exploration Findings](./exploration-findings-initial.md) for gap analysis.
|
**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
|
||||||
|
- 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
|
||||||
|
- Prerequisite: Phase 1 complete
|
||||||
|
|
||||||
|
- **[Phase 3: Boot Receiver Missed Alarm Handling](./android-implementation-directive-phase3.md)** - Boot recovery enhancement
|
||||||
|
- Prerequisites: Phase 1 and Phase 2 complete
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. Implementation Overview
|
## 1. Implementation Overview
|
||||||
@@ -30,24 +44,40 @@ This directive provides **step-by-step implementation guidance** for Android-spe
|
|||||||
|
|
||||||
### 1.2 Implementation Strategy
|
### 1.2 Implementation Strategy
|
||||||
|
|
||||||
**Phase 1**: Create `ReactivationManager` class
|
**Phase 1** – Cold start recovery only
|
||||||
- Centralizes all recovery logic
|
- Missed notification detection + future alarm verification
|
||||||
- Handles cold/warm/force-stop scenarios
|
- No force-stop detection, no boot handling
|
||||||
- Detects missed alarms
|
- **See [Phase 1 directive](./android-implementation-directive-phase1.md) for implementation**
|
||||||
- Reschedules future alarms
|
|
||||||
|
|
||||||
**Phase 2**: Integrate into `DailyNotificationPlugin.load()`
|
**Phase 2** – Force stop detection & full recovery
|
||||||
- Call recovery manager on app launch
|
- Force stop detection via AlarmManager state comparison
|
||||||
- Run asynchronously to avoid blocking
|
- 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**: Enhance `BootReceiver`
|
**Phase 3** – Boot receiver missed alarm detection & rescheduling
|
||||||
- Add missed alarm detection
|
- Boot receiver detects missed alarms during device reboot
|
||||||
- Handle alarms that were scheduled before reboot
|
- Next occurrence rescheduled for repeating schedules
|
||||||
|
- **See [Phase 3 directive](./android-implementation-directive-phase3.md) for implementation**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. Implementation: ReactivationManager
|
## 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
|
### 2.1 Create New File
|
||||||
|
|
||||||
**File**: `android/src/main/java/com/timesafari/dailynotification/ReactivationManager.kt`
|
**File**: `android/src/main/java/com/timesafari/dailynotification/ReactivationManager.kt`
|
||||||
@@ -56,6 +86,8 @@ This directive provides **step-by-step implementation guidance** for Android-spe
|
|||||||
|
|
||||||
### 2.2 Class Structure
|
### 2.2 Class Structure
|
||||||
|
|
||||||
|
**⚠️ Illustrative only** – See Phase 1 for canonical implementation.
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
package com.timesafari.dailynotification
|
package com.timesafari.dailynotification
|
||||||
|
|
||||||
@@ -113,6 +145,8 @@ class ReactivationManager(private val context: Context) {
|
|||||||
|
|
||||||
### 2.3 Scenario Detection
|
### 2.3 Scenario Detection
|
||||||
|
|
||||||
|
**⚠️ Illustrative only** – See Phase 2 for canonical scenario detection implementation.
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
/**
|
/**
|
||||||
* Detect recovery scenario based on AlarmManager state vs database
|
* Detect recovery scenario based on AlarmManager state vs database
|
||||||
@@ -176,6 +210,8 @@ enum class RecoveryScenario {
|
|||||||
|
|
||||||
### 2.4 Force Stop Recovery
|
### 2.4 Force Stop Recovery
|
||||||
|
|
||||||
|
**⚠️ Illustrative only** – See Phase 2 for canonical force stop recovery implementation.
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
/**
|
/**
|
||||||
* Handle force stop recovery
|
* Handle force stop recovery
|
||||||
@@ -234,6 +270,8 @@ private suspend fun handleForceStopRecovery() {
|
|||||||
|
|
||||||
### 2.5 Cold Start Recovery
|
### 2.5 Cold Start Recovery
|
||||||
|
|
||||||
|
**⚠️ Illustrative only** – See Phase 1 for canonical cold start recovery implementation.
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
/**
|
/**
|
||||||
* Handle cold start recovery
|
* Handle cold start recovery
|
||||||
@@ -571,6 +609,10 @@ override fun load() {
|
|||||||
|
|
||||||
## 4. Enhancement: BootReceiver
|
## 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
|
### 4.1 Update `rescheduleNotifications()` Method
|
||||||
|
|
||||||
**File**: `android/src/main/java/com/timesafari/dailynotification/BootReceiver.kt`
|
**File**: `android/src/main/java/com/timesafari/dailynotification/BootReceiver.kt`
|
||||||
@@ -705,6 +747,13 @@ private suspend fun handleMissedAlarmOnBoot(
|
|||||||
|
|
||||||
## 5. Testing Requirements
|
## 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
|
### 5.1 Test Setup
|
||||||
|
|
||||||
**Package Name**: `com.timesafari.dailynotification`
|
**Package Name**: `com.timesafari.dailynotification`
|
||||||
|
|||||||
Reference in New Issue
Block a user