docs(test): add Phase 3 boot recovery testing infrastructure
Adds documentation and test harness for Phase 3 (Boot-Time Recovery). Changes: - Update android-implementation-directive-phase3.md with concise boot recovery flow - Add PHASE3-EMULATOR-TESTING.md with detailed test procedures - Add PHASE3-VERIFICATION.md with test matrix and verification template - Add test-phase3.sh automated test harness Test harness features: - 4 test cases: future alarms, past alarms, no schedules, silent recovery - Automatic emulator reboot handling - Log parsing for boot recovery scenario and results - UI prompts for plugin configuration and scheduling - Verifies silent recovery without app launch Related: - Directive: android-implementation-directive-phase3.md - Requirements: docs/alarms/03-plugin-requirements.md §3.1.1 - Testing: docs/alarms/PHASE3-EMULATOR-TESTING.md - Verification: docs/alarms/PHASE3-VERIFICATION.md
This commit is contained in:
325
docs/alarms/PHASE3-EMULATOR-TESTING.md
Normal file
325
docs/alarms/PHASE3-EMULATOR-TESTING.md
Normal file
@@ -0,0 +1,325 @@
|
||||
# PHASE 3 – EMULATOR TESTING
|
||||
|
||||
**Boot-Time Recovery (Device Reboot / System Restart)**
|
||||
|
||||
---
|
||||
|
||||
## 1. Purpose
|
||||
|
||||
Phase 3 verifies that the Daily Notification Plugin correctly:
|
||||
|
||||
1. Reconstructs **AlarmManager** alarms after a full device/emulator reboot.
|
||||
2. Handles **past** scheduled times by marking them as missed and scheduling the next occurrence.
|
||||
3. Handles **empty DB / no schedules** without misfiring recovery.
|
||||
4. Performs **silent boot recovery** (recreate alarms) even when the app is never opened after reboot.
|
||||
|
||||
This testing is driven by the script:
|
||||
|
||||
```bash
|
||||
test-apps/android-test-app/test-phase3.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Prerequisites
|
||||
|
||||
* Android emulator or device, e.g.:
|
||||
* Pixel 8 / API 34 (recommended baseline)
|
||||
* ADB available in `PATH` (or `ADB_BIN` exported)
|
||||
* Project with:
|
||||
* Daily Notification Plugin integrated
|
||||
* Test app at `test-apps/android-test-app`
|
||||
* Debug APK path:
|
||||
* `app/build/outputs/apk/debug/app-debug.apk`
|
||||
* Phase 1 and Phase 2 behaviors already implemented:
|
||||
* Cold start detection
|
||||
* Force-stop detection
|
||||
* Missed / rescheduled / verified / errors summary fields
|
||||
|
||||
> ⚠️ **Important:**
|
||||
> This script will reboot the emulator multiple times. Each reboot may take 30–60 seconds.
|
||||
|
||||
---
|
||||
|
||||
## 3. How to Run
|
||||
|
||||
From the `android-test-app` directory:
|
||||
|
||||
```bash
|
||||
cd test-apps/android-test-app
|
||||
chmod +x test-phase3.sh # first time only
|
||||
./test-phase3.sh
|
||||
```
|
||||
|
||||
The script will:
|
||||
|
||||
1. Run pre-flight checks (ADB / emulator readiness).
|
||||
2. Build and install the debug APK.
|
||||
3. Guide you through four tests:
|
||||
* **TEST 1:** Boot with Future Alarms
|
||||
* **TEST 2:** Boot with Past Alarms
|
||||
* **TEST 3:** Boot with No Schedules
|
||||
* **TEST 4:** Silent Boot Recovery (App Never Opened)
|
||||
4. Parse and display `DNP-REACTIVATION` logs, including:
|
||||
* `scenario`
|
||||
* `missed`
|
||||
* `rescheduled`
|
||||
* `verified`
|
||||
* `errors`
|
||||
|
||||
---
|
||||
|
||||
## 4. Test Cases (Script-Driven Flow)
|
||||
|
||||
### 4.1 TEST 1 – Boot with Future Alarms
|
||||
|
||||
**Goal:**
|
||||
|
||||
Verify alarms are recreated on boot when schedules have **future run times**.
|
||||
|
||||
**Script flow:**
|
||||
|
||||
1. **Launch app & check plugin status**
|
||||
* Script calls `launch_app`.
|
||||
* UI prompt: Confirm plugin status shows:
|
||||
* `⚙️ Plugin Settings: ✅ Configured`
|
||||
* `🔌 Native Fetcher: ✅ Configured`
|
||||
* If not, click **Configure Plugin**, wait until both show ✅, then continue.
|
||||
|
||||
2. **Schedule at least one future notification**
|
||||
* UI prompt: Click e.g. **Test Notification** to schedule a notification a few minutes in the future.
|
||||
|
||||
3. **Verify alarms are scheduled (pre-boot)**
|
||||
* Script calls `show_alarms` and `count_alarms`.
|
||||
* You should see at least one `RTC_WAKEUP` entry for `com.timesafari.dailynotification`.
|
||||
|
||||
4. **Reboot emulator**
|
||||
* Script calls `reboot_emulator`:
|
||||
* `adb reboot`
|
||||
* `adb wait-for-device`
|
||||
* Polls `getprop sys.boot_completed` until `1`.
|
||||
* You are warned that reboot will take 30–60 seconds.
|
||||
|
||||
5. **Collect boot recovery logs**
|
||||
* Script calls `get_recovery_logs`:
|
||||
```bash
|
||||
adb logcat -d | grep "DNP-REACTIVATION"
|
||||
```
|
||||
* It parses:
|
||||
* `missed`, `rescheduled`, `verified`, `errors`
|
||||
* `scenario` via:
|
||||
* `Starting boot recovery`/`boot recovery` → `scenario=BOOT`
|
||||
* or `Detected scenario: <VALUE>`
|
||||
|
||||
6. **Verify alarms were recreated (post-boot)**
|
||||
* Script calls `show_alarms` and `count_alarms` again.
|
||||
* Checks `scenario` and `rescheduled`.
|
||||
|
||||
**Expected log patterns:**
|
||||
|
||||
```text
|
||||
DNP-REACTIVATION: Starting boot recovery
|
||||
DNP-REACTIVATION: Loaded <N> schedules from DB
|
||||
DNP-REACTIVATION: Rescheduled alarm: daily_<id> for <time>
|
||||
DNP-REACTIVATION: Boot recovery complete: missed=0, rescheduled>=1, verified=0, errors=0
|
||||
```
|
||||
|
||||
**Pass criteria (as per script):**
|
||||
|
||||
* `errors = 0`
|
||||
* `scenario = BOOT` (or boot detected via log text)
|
||||
* `rescheduled > 0`
|
||||
* Script prints:
|
||||
> `✅ TEST 1 PASSED: Boot recovery detected and alarms rescheduled (scenario=BOOT, rescheduled=<n>).`
|
||||
|
||||
If boot recovery runs but `rescheduled=0`, script warns and suggests checking boot logic.
|
||||
|
||||
---
|
||||
|
||||
### 4.2 TEST 2 – Boot with Past Alarms
|
||||
|
||||
**Goal:**
|
||||
|
||||
Verify past alarms are marked as missed and **next occurrences are scheduled** after boot.
|
||||
|
||||
**Script flow:**
|
||||
|
||||
1. **Launch app & ensure plugin configured**
|
||||
* Same plugin status check as TEST 1.
|
||||
|
||||
2. **Schedule a notification in the near future**
|
||||
* UI prompt: Schedule such that **by the time you reboot and the device comes back, the planned notification time is in the past**.
|
||||
|
||||
3. **Wait or adjust so the alarm is effectively "in the past" at boot**
|
||||
* The script may instruct you to wait, or you can coordinate timing manually.
|
||||
|
||||
4. **Reboot emulator**
|
||||
* Same `reboot_emulator` path as TEST 1.
|
||||
|
||||
5. **Collect boot recovery logs**
|
||||
* Script parses:
|
||||
* `missed`, `rescheduled`, `errors`, `scenario`.
|
||||
|
||||
**Expected log patterns:**
|
||||
|
||||
```text
|
||||
DNP-REACTIVATION: Starting boot recovery
|
||||
DNP-REACTIVATION: Loaded <N> schedules from DB
|
||||
DNP-REACTIVATION: Marked missed notification: daily_<id>
|
||||
DNP-REACTIVATION: Rescheduled alarm: daily_<id> for <next_time>
|
||||
DNP-REACTIVATION: Boot recovery complete: missed>=1, rescheduled>=1, errors=0
|
||||
```
|
||||
|
||||
**Pass criteria:**
|
||||
|
||||
* `errors = 0`
|
||||
* `missed >= 1`
|
||||
* `rescheduled >= 1`
|
||||
* Script prints:
|
||||
> `✅ TEST 2 PASSED: Past alarms detected and next occurrence scheduled (missed=<m>, rescheduled=<r>).`
|
||||
|
||||
If `missed >= 1` but `rescheduled = 0`, script warns that reschedule logic may be incomplete.
|
||||
|
||||
---
|
||||
|
||||
### 4.3 TEST 3 – Boot with No Schedules
|
||||
|
||||
**Goal:**
|
||||
|
||||
Verify boot recovery handles an **empty DB / no schedules** safely and does **not** schedule anything.
|
||||
|
||||
**Script flow:**
|
||||
|
||||
1. **Uninstall app to clear DB/state**
|
||||
* Script calls:
|
||||
```bash
|
||||
adb uninstall com.timesafari.dailynotification
|
||||
```
|
||||
|
||||
2. **Reinstall APK**
|
||||
* Script reinstalls `app-debug.apk`.
|
||||
|
||||
3. **Launch app WITHOUT scheduling anything**
|
||||
* Script launches app; you do not configure or schedule.
|
||||
|
||||
4. **Collect boot/logs**
|
||||
* Script reads `DNP-REACTIVATION` logs and checks:
|
||||
* if there are no logs, or
|
||||
* if there's a "No schedules found / present" message, or
|
||||
* if `scenario=NONE` and `rescheduled=0`.
|
||||
|
||||
**Expected patterns:**
|
||||
|
||||
* *Ideal simple case:* **No** `DNP-REACTIVATION` logs at all, or:
|
||||
* Explicit message in logs:
|
||||
```text
|
||||
DNP-REACTIVATION: ... No schedules found ...
|
||||
```
|
||||
|
||||
**Pass criteria (as per script):**
|
||||
|
||||
* If **no logs**:
|
||||
* Pass: `TEST 3 PASSED: No recovery logs when there are no schedules (safe behavior).`
|
||||
* If logs exist:
|
||||
* Contains `No schedules found` / `No schedules present` **and** `rescheduled=0`, or
|
||||
* `scenario = NONE` and `rescheduled = 0`.
|
||||
|
||||
Any case where `rescheduled > 0` with an empty DB is flagged as a warning (boot recovery misfiring).
|
||||
|
||||
---
|
||||
|
||||
### 4.4 TEST 4 – Silent Boot Recovery (App Never Opened)
|
||||
|
||||
**Goal:**
|
||||
|
||||
Verify that boot recovery **occurs silently**, recreating alarms **without opening the app** after reboot.
|
||||
|
||||
**Script flow:**
|
||||
|
||||
1. **Launch app and configure plugin**
|
||||
* Same plugin status flow:
|
||||
* Ensure both plugin checks are ✅.
|
||||
* Schedule a future notification via UI.
|
||||
|
||||
2. **Verify alarms are scheduled**
|
||||
* Script shows alarms and counts (`before_count`).
|
||||
|
||||
3. **Reboot emulator**
|
||||
* Script runs `reboot_emulator` and explicitly warns:
|
||||
* Do **not** open the app after reboot.
|
||||
* After emulator returns, script instructs you to **not touch the app UI**.
|
||||
|
||||
4. **Collect boot recovery logs**
|
||||
* Script gathers and parses `DNP-REACTIVATION` lines.
|
||||
|
||||
5. **Verify alarms were recreated without app launch**
|
||||
* Script calls `show_alarms` and `count_alarms` again.
|
||||
* Uses `rescheduled` + alarm count to decide.
|
||||
|
||||
**Pass criteria (as per script):**
|
||||
|
||||
* `rescheduled > 0` after boot, and
|
||||
* Alarm count after boot is > 0, and
|
||||
* App was **never** launched by the user after reboot.
|
||||
|
||||
Script prints one of:
|
||||
|
||||
```text
|
||||
✅ TEST 4 PASSED: Boot recovery occurred silently and alarms were recreated (rescheduled=<n>) without app launch.
|
||||
|
||||
✅ TEST 4 PASSED: Boot recovery occurred silently (rescheduled=<n>), but alarm count check unclear.
|
||||
```
|
||||
|
||||
If boot recovery logs are present but no alarms appear, script warns; if no boot-recovery logs are found at all, script suggests verifying the boot receiver and BOOT_COMPLETED permission.
|
||||
|
||||
---
|
||||
|
||||
## 5. Overall Summary Section (from Script)
|
||||
|
||||
At the end, the script prints:
|
||||
|
||||
```text
|
||||
TEST 1: Boot with Future Alarms
|
||||
- Check logs for boot recovery and rescheduled>0
|
||||
|
||||
TEST 2: Boot with Past Alarms
|
||||
- Check logs for missed>=1 and rescheduled>=1
|
||||
|
||||
TEST 3: Boot with No Schedules
|
||||
- Check that no recovery runs or that an explicit 'No schedules found' is logged without rescheduling
|
||||
|
||||
TEST 4: Silent Boot Recovery
|
||||
- Check that boot recovery occurred and alarms were recreated without app launch
|
||||
```
|
||||
|
||||
Use this as a quick checklist after a run.
|
||||
|
||||
---
|
||||
|
||||
## 6. Troubleshooting Notes
|
||||
|
||||
* If **no boot recovery logs** ever appear:
|
||||
* Check that `BootReceiver` is declared and `RECEIVE_BOOT_COMPLETED` permission is set.
|
||||
* Ensure the app is installed in internal storage (not moved to SD).
|
||||
|
||||
* If **errors > 0** in summary:
|
||||
* Inspect the full `DNP-REACTIVATION` logs printed by the script.
|
||||
|
||||
* If **alarming duplication** is observed:
|
||||
* Review `runBootRecovery` and dedupe logic around re-scheduling.
|
||||
|
||||
---
|
||||
|
||||
## 7. Related Documentation
|
||||
|
||||
- [Phase 3 Directive](../android-implementation-directive-phase3.md) - Implementation details
|
||||
- [Phase 3 Verification](./PHASE3-VERIFICATION.md) - Verification report
|
||||
- [Phase 1 Testing Guide](./PHASE1-EMULATOR-TESTING.md) - Prerequisite testing
|
||||
- [Phase 2 Testing Guide](./PHASE2-EMULATOR-TESTING.md) - Prerequisite testing
|
||||
- [Activation Guide](./ACTIVATION-GUIDE.md) - How to use directives
|
||||
- [Plugin Requirements](./03-plugin-requirements.md) - Requirements Phase 3 implements
|
||||
|
||||
---
|
||||
|
||||
**Status**: Ready for testing (Phase 3 implementation pending)
|
||||
**Last Updated**: November 2025
|
||||
201
docs/alarms/PHASE3-VERIFICATION.md
Normal file
201
docs/alarms/PHASE3-VERIFICATION.md
Normal file
@@ -0,0 +1,201 @@
|
||||
# Phase 3 – Boot-Time Recovery Verification
|
||||
|
||||
**Plugin:** Daily Notification Plugin
|
||||
**Scope:** Boot-Time Recovery (Recreate Alarms After Reboot)
|
||||
**Related Docs:**
|
||||
- `android-implementation-directive-phase3.md`
|
||||
- `PHASE3-EMULATOR-TESTING.md`
|
||||
- `test-phase3.sh`
|
||||
- `000-UNIFIED-ALARM-DIRECTIVE.md`
|
||||
|
||||
---
|
||||
|
||||
## 1. Objective
|
||||
|
||||
Phase 3 confirms that the Daily Notification Plugin:
|
||||
|
||||
1. Reconstructs all daily notification alarms after a **full device reboot**.
|
||||
2. Correctly handles **past** vs **future** schedules:
|
||||
- Past: mark as missed, schedule next occurrence
|
||||
- Future: simply recreate alarms
|
||||
3. Handles **empty DB / no schedules** without misfiring recovery.
|
||||
4. Performs **silent boot recovery** (no app launch required) when schedules exist.
|
||||
5. Logs a consistent, machine-readable summary:
|
||||
- `scenario`
|
||||
- `missed`
|
||||
- `rescheduled`
|
||||
- `verified`
|
||||
- `errors`
|
||||
|
||||
Verification is performed via the emulator harness:
|
||||
|
||||
```bash
|
||||
cd test-apps/android-test-app
|
||||
./test-phase3.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Test Matrix (From Script)
|
||||
|
||||
| ID | Scenario | Script Test | Expected Behavior | Result | Notes |
|
||||
| --- | --------------------------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ | ------ | ----- |
|
||||
| 3.1 | Boot with Future Alarms | TEST 1 – Boot with Future Alarms | `scenario=BOOT`, `rescheduled>0`, `errors=0`; alarms present after boot | ☐ | |
|
||||
| 3.2 | Boot with Past Alarms | TEST 2 – Boot with Past Alarms | `missed>=1` and `rescheduled>=1`, `errors=0`; past schedules detected and next occurrences scheduled | ☐ | |
|
||||
| 3.3 | Boot with No Schedules (Empty DB) | TEST 3 – Boot with No Schedules | Either no recovery logs **or** explicit "No schedules found/present" or `scenario=NONE` with `rescheduled=0`, `errors=0` | ☐ | |
|
||||
| 3.4 | Silent Boot Recovery (App Never Opened) | TEST 4 – Silent Boot Recovery (App Never Opened) | `rescheduled>0`, alarms present after boot, and no user launch required; `errors=0` | ☐ | |
|
||||
|
||||
Fill **Result** and **Notes** after running `test-phase3.sh` on your baseline emulator/device.
|
||||
|
||||
---
|
||||
|
||||
## 3. Expected Log Patterns
|
||||
|
||||
The script filters logs with:
|
||||
|
||||
```bash
|
||||
adb logcat -d | grep "DNP-REACTIVATION"
|
||||
```
|
||||
|
||||
### 3.1 Boot with Future Alarms (3.1 / TEST 1)
|
||||
|
||||
* Typical logs:
|
||||
|
||||
```text
|
||||
DNP-REACTIVATION: Starting boot recovery
|
||||
DNP-REACTIVATION: Loaded <N> schedules from DB
|
||||
DNP-REACTIVATION: Rescheduled alarm: daily_<id> for <time>
|
||||
DNP-REACTIVATION: Boot recovery complete: missed=0, rescheduled=<r>, verified=0, errors=0
|
||||
```
|
||||
|
||||
* The script interprets this as:
|
||||
* `scenario = BOOT` (via "Starting boot recovery" or "boot recovery" text or `Detected scenario: BOOT`)
|
||||
* `rescheduled > 0`
|
||||
* `errors = 0`
|
||||
|
||||
### 3.2 Boot with Past Alarms (3.2 / TEST 2)
|
||||
|
||||
* Typical logs:
|
||||
|
||||
```text
|
||||
DNP-REACTIVATION: Starting boot recovery
|
||||
DNP-REACTIVATION: Loaded <N> schedules from DB
|
||||
DNP-REACTIVATION: Marked missed notification: daily_<id>
|
||||
DNP-REACTIVATION: Rescheduled alarm: daily_<id> for <next_time>
|
||||
DNP-REACTIVATION: Boot recovery complete: missed=<m>, rescheduled=<r>, verified=0, errors=0
|
||||
```
|
||||
|
||||
* The script parses `missed` and `rescheduled` and passes when:
|
||||
* `missed >= 1`
|
||||
* `rescheduled >= 1`
|
||||
* `errors = 0`
|
||||
|
||||
### 3.3 Boot with No Schedules (3.3 / TEST 3)
|
||||
|
||||
Two acceptable patterns:
|
||||
|
||||
1. **No `DNP-REACTIVATION` logs at all** → safe behavior
|
||||
2. Explicit "no schedules" logs:
|
||||
|
||||
```text
|
||||
DNP-REACTIVATION: ... No schedules found ...
|
||||
```
|
||||
|
||||
or a neutral scenario:
|
||||
|
||||
```text
|
||||
DNP-REACTIVATION: ... scenario=NONE ...
|
||||
DNP-REACTIVATION: Boot recovery complete: missed=0, rescheduled=0, verified=0, errors=0
|
||||
```
|
||||
|
||||
The script passes when:
|
||||
|
||||
* Either `logs` are empty, or
|
||||
* Logs contain "No schedules found / present" with `rescheduled=0`, or
|
||||
* `scenario=NONE` and `rescheduled=0`.
|
||||
|
||||
Any `rescheduled>0` in this state is flagged as a potential boot-recovery misfire.
|
||||
|
||||
### 3.4 Silent Boot Recovery (3.4 / TEST 4)
|
||||
|
||||
* Expected:
|
||||
|
||||
```text
|
||||
DNP-REACTIVATION: Starting boot recovery
|
||||
DNP-REACTIVATION: Loaded <N> schedules from DB
|
||||
DNP-REACTIVATION: Rescheduled alarm: daily_<id> for <time>
|
||||
DNP-REACTIVATION: Boot recovery complete: missed=0, rescheduled=<r>, verified=0, errors=0
|
||||
```
|
||||
|
||||
* After reboot:
|
||||
* `count_alarms` > 0
|
||||
* User **did not** relaunch the app manually
|
||||
|
||||
Script passes if:
|
||||
|
||||
* `rescheduled>0`, and
|
||||
* Alarm count after boot is > 0, and
|
||||
* Boot recovery is detected from logs (via "Starting boot recovery"/"boot recovery" or scenario).
|
||||
|
||||
---
|
||||
|
||||
## 4. Latest Known Good Run (Template)
|
||||
|
||||
> Fill this in after your first clean emulator run.
|
||||
|
||||
**Environment**
|
||||
|
||||
* Device: Pixel 8 API 34 (Android 14)
|
||||
* App ID: `com.timesafari.dailynotification`
|
||||
* Build: Debug `app-debug.apk` from commit `<GIT_HASH>`
|
||||
* Script: `./test-phase3.sh`
|
||||
* Date: 2025-11-XX
|
||||
|
||||
**Observed Results**
|
||||
|
||||
* ☐ **3.1 – Boot with Future Alarms**
|
||||
* `scenario=BOOT`
|
||||
* `missed=0, rescheduled=<r>, errors=0`
|
||||
|
||||
* ☐ **3.2 – Boot with Past Alarms**
|
||||
* `missed=<m>=1`, `rescheduled=<r>≥1`, `errors=0`
|
||||
|
||||
* ☐ **3.3 – Boot with No Schedules**
|
||||
* Either no logs, or explicit "No schedules found" with `rescheduled=0`
|
||||
|
||||
* ☐ **3.4 – Silent Boot Recovery**
|
||||
* `rescheduled>0`, alarms present after boot, app not opened
|
||||
|
||||
**Conclusion:**
|
||||
|
||||
> Phase 3 **Boot-Time Recovery** is successfully verified on emulator using `test-phase3.sh`. This is the canonical baseline for future regression testing and refactors to `ReactivationManager` and `BootReceiver`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Overall Status
|
||||
|
||||
> Update once the first emulator run is complete.
|
||||
|
||||
* **Implementation Status:** ☐ Pending / ✅ Implemented (Boot receiver + `runBootRecovery`)
|
||||
* **Test Harness:** ✅ `test-phase3.sh` in `test-apps/android-test-app`
|
||||
* **Emulator Verification:** ☐ Pending / ✅ Completed
|
||||
|
||||
Once all test cases pass:
|
||||
|
||||
> **Overall Status:** ✅ **VERIFIED** – Phase 3 boot-time recovery is implemented and emulator-tested, aligned with `android-implementation-directive-phase3.md` and the unified alarm directive.
|
||||
|
||||
---
|
||||
|
||||
## 6. Related Documentation
|
||||
|
||||
- [Phase 3 Directive](../android-implementation-directive-phase3.md) - Implementation details
|
||||
- [Phase 3 Emulator Testing](./PHASE3-EMULATOR-TESTING.md) - Test procedures
|
||||
- [Phase 1 Verification](./PHASE1-VERIFICATION.md) - Prerequisite verification
|
||||
- [Phase 2 Verification](./PHASE2-VERIFICATION.md) - Prerequisite verification
|
||||
- [Plugin Requirements](./03-plugin-requirements.md) - Requirements this phase implements
|
||||
- [Platform Capability Reference](./01-platform-capability-reference.md) - OS-level facts
|
||||
|
||||
---
|
||||
|
||||
**Status**: ☐ **PENDING** – Phase 3 implementation and testing pending
|
||||
**Last Updated**: November 2025
|
||||
@@ -1,627 +1,221 @@
|
||||
# Android Implementation Directive: Phase 3 - Boot Receiver Missed Alarm Handling
|
||||
# Android Implementation Directive – Phase 3
|
||||
## Boot-Time Recovery (Device Reboot / System Restart)
|
||||
|
||||
**Author**: Matthew Raymer
|
||||
**Date**: November 2025
|
||||
**Status**: Phase 3 - Boot Recovery Enhancement
|
||||
**Version**: 1.0.0
|
||||
**Last Synced With Plugin Version**: v1.1.0
|
||||
|
||||
**Implements**: [Plugin Requirements §3.1.1 - Boot Event](./alarms/03-plugin-requirements.md#311-boot-event-android-only)
|
||||
|
||||
## 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**:
|
||||
- [Plugin Requirements](./alarms/03-plugin-requirements.md) - Requirements this phase implements
|
||||
- [Platform Capability Reference](./alarms/01-platform-capability-reference.md) - OS-level facts
|
||||
- [Phase 1](./android-implementation-directive-phase1.md) - Prerequisite
|
||||
- [Phase 2](./android-implementation-directive-phase2.md) - Prerequisite
|
||||
- [Full Implementation Directive](./android-implementation-directive.md) - 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).
|
||||
**Plugin:** Daily Notification Plugin
|
||||
**Author:** Matthew Raymer
|
||||
**Applies to:** Android Plugin (Kotlin), Capacitor Bridge
|
||||
**Related Docs:**
|
||||
- `03-plugin-requirements.md`
|
||||
- `000-UNIFIED-ALARM-DIRECTIVE.md`
|
||||
- `android-implementation-directive-phase1.md`
|
||||
- `android-implementation-directive-phase2.md`
|
||||
- `ACTIVATION-GUIDE.md`
|
||||
|
||||
---
|
||||
|
||||
## 1. Acceptance Criteria
|
||||
## 1. Purpose
|
||||
|
||||
### 1.1 Definition of Done
|
||||
Phase 3 introduces **Boot-Time Recovery**, which restores daily notifications after:
|
||||
|
||||
**Phase 3 is complete when:**
|
||||
- Device reboot
|
||||
- OS restart
|
||||
- Update-related restart
|
||||
- App not opened after reboot (silent recovery)
|
||||
|
||||
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)
|
||||
Android clears **all alarms** on reboot.
|
||||
Therefore, if our plugin is not actively rescheduling on boot, the user will miss all daily notifications until they manually launch the app.
|
||||
|
||||
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
|
||||
Phase 3 ensures:
|
||||
|
||||
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)
|
||||
1. Schedules stored in SQLite survive reboot
|
||||
2. Alarms are fully reconstructed
|
||||
3. No duplication / double-scheduling
|
||||
4. Boot behavior avoids unnecessary heavy recovery
|
||||
5. Recovery occurs even if the user does **not** manually open the app
|
||||
|
||||
---
|
||||
|
||||
## 2. Implementation: BootReceiver Enhancement
|
||||
## 2. Boot-Time Recovery Flow
|
||||
|
||||
### 2.1 Canonical Source of Truth
|
||||
### Trigger:
|
||||
|
||||
**⚠️ CRITICAL CORRECTION**: BootReceiver must **NOT** implement recovery logic directly. It must **only queue** ReactivationManager.performRecovery() with a BOOT flag.
|
||||
`BOOT_COMPLETED` broadcast received
|
||||
→ Plugin's Boot Receiver invoked
|
||||
→ Recovery logic executed with `scenario=BOOT`
|
||||
|
||||
**ReactivationManager.kt** is the **only** file allowed to:
|
||||
- Perform scenario detection
|
||||
- Initiate recovery logic
|
||||
- Branch execution per phase
|
||||
### Recovery Steps
|
||||
|
||||
### 2.2 Update BootReceiver
|
||||
1. **Load all schedules** from SQLite (`NotificationRepository.getAllSchedules()`)
|
||||
|
||||
**File**: `android/src/main/java/com/timesafari/dailynotification/BootReceiver.kt`
|
||||
2. **For each schedule:**
|
||||
- Calculate next runtime based on cron expression
|
||||
- Compare with current time
|
||||
|
||||
**Location**: `onReceive()` method
|
||||
3. **If the next scheduled time is in the future:**
|
||||
- Recreate alarm with `setAlarmClock`
|
||||
- Log:
|
||||
`Rescheduled alarm: <id> for <ts>`
|
||||
|
||||
### 2.3 Corrected Implementation
|
||||
4. **If schedule was *in the past* at boot time:**
|
||||
- Mark as missed
|
||||
- Schedule next run according to cron rules
|
||||
|
||||
5. **If no schedules found:**
|
||||
- Quiet exit, log only one line:
|
||||
`BOOT: No schedules found`
|
||||
|
||||
6. **Safeties:**
|
||||
- Boot recovery must **not** modify Plugin Settings
|
||||
- Must not regenerate Fetcher configuration
|
||||
- Must not overwrite database records
|
||||
|
||||
---
|
||||
|
||||
## 3. Required Android Components
|
||||
|
||||
### 3.1 Boot Receiver
|
||||
|
||||
```xml
|
||||
<receiver
|
||||
android:name=".BootReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
```
|
||||
|
||||
### 3.2 Kotlin Class
|
||||
|
||||
**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)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (intent?.action != Intent.ACTION_BOOT_COMPLETED) return
|
||||
|
||||
ReactivationManager.runBootRecovery(context)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**⚠️ 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")
|
||||
}
|
||||
```
|
||||
## 4. ReactivationManager – Boot Logic
|
||||
|
||||
**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
|
||||
### Method Signature
|
||||
|
||||
```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
|
||||
}
|
||||
}
|
||||
fun runBootRecovery(context: Context)
|
||||
```
|
||||
|
||||
### 2.5 Helper Methods
|
||||
### Required Logging (canonical)
|
||||
|
||||
**⚠️ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
DNP-REACTIVATION: Starting boot recovery
|
||||
DNP-REACTIVATION: Loaded <N> schedules from DB
|
||||
DNP-REACTIVATION: Rescheduled alarm: <id> for <ts>
|
||||
DNP-REACTIVATION: Marked missed notification: <id>
|
||||
DNP-REACTIVATION: Boot recovery complete: missed=X, rescheduled=Y, errors=Z
|
||||
```
|
||||
|
||||
---
|
||||
### Required Fields
|
||||
|
||||
## 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
|
||||
* `scenario=BOOT`
|
||||
* `missed`
|
||||
* `rescheduled`
|
||||
* `verified` **MUST BE 0** (boot has no verification phase)
|
||||
|
||||
---
|
||||
|
||||
## 4. Rollback Safety
|
||||
## 5. Constraints & Guardrails
|
||||
|
||||
### 4.1 No-Crash Guarantee
|
||||
1. **No plugin initialization**
|
||||
Boot must *not* require running the app UI.
|
||||
|
||||
**All boot recovery operations must:**
|
||||
2. **No heavy processing**
|
||||
* limit to 2 seconds
|
||||
* use the same timeout guard as Phase 2
|
||||
|
||||
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)
|
||||
3. **No scheduling duplicates**
|
||||
* Must detect existing AlarmManager entries
|
||||
* Boot always clears them, so all reschedules should be fresh
|
||||
|
||||
### 4.2 Error Handling Strategy
|
||||
4. **App does not need to be opened**
|
||||
* Entire recovery must run in background context
|
||||
|
||||
| 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.
|
||||
5. **Idempotency**
|
||||
* Running twice should produce identical logs
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
### Mandatory
|
||||
|
||||
* [ ] BootReceiver included
|
||||
* [ ] Manifest entry added
|
||||
* [ ] `runBootRecovery()` implemented
|
||||
* [ ] Scenario logged as `BOOT`
|
||||
* [ ] All alarms recreated
|
||||
* [ ] Timeout protection
|
||||
* [ ] No modifications to preferences or plugin settings
|
||||
|
||||
### Optional
|
||||
|
||||
* [ ] Additional telemetry for analytics
|
||||
* [ ] Optional debug toast for dev builds only
|
||||
|
||||
---
|
||||
|
||||
## 7. Code References
|
||||
## 7. Expected Output Examples
|
||||
|
||||
**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
|
||||
### Example 1 – Normal Boot (future alarms exist)
|
||||
|
||||
**New Code to Create**:
|
||||
- `handleMissedAlarmOnBoot()` - Add to BootReceiver
|
||||
- `isRepeating()` - Add to BootReceiver (or reuse from ReactivationManager)
|
||||
- `calculateNextOccurrence()` - Add to BootReceiver (or reuse from ReactivationManager)
|
||||
```
|
||||
DNP-REACTIVATION: Starting boot recovery
|
||||
DNP-REACTIVATION: Loaded 2 schedules from DB
|
||||
DNP-REACTIVATION: Rescheduled alarm: daily_1764233911265 for 1764236120000
|
||||
DNP-REACTIVATION: Rescheduled alarm: daily_1764233465343 for 1764233700000
|
||||
DNP-REACTIVATION: Boot recovery complete: missed=0, rescheduled=2, errors=0
|
||||
```
|
||||
|
||||
### Example 2 – Schedules present but some in past
|
||||
|
||||
```
|
||||
Marked missed notification: daily_1764233300000
|
||||
Rescheduled alarm: daily_1764233300000 for next day
|
||||
```
|
||||
|
||||
### Example 3 – No schedules
|
||||
|
||||
```
|
||||
DNP-REACTIVATION: BOOT: No schedules found
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Success Criteria Summary
|
||||
## 8. Status
|
||||
|
||||
**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
|
||||
| Item | Status |
|
||||
| -------------------- | -------------------------------- |
|
||||
| Directive | **Complete** |
|
||||
| Implementation | ☐ Pending / ✅ **Complete** (plugin v1.2+) |
|
||||
| Emulator Test Script | Ready (`test-phase3.sh`) |
|
||||
| Verification Doc | Ready (`PHASE3-VERIFICATION.md`) |
|
||||
|
||||
---
|
||||
|
||||
## Related Documentation
|
||||
## 9. 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
|
||||
- [Unified Alarm Directive](./alarms/000-UNIFIED-ALARM-DIRECTIVE.md) - Master coordination document
|
||||
- [Plugin Requirements](./alarms/03-plugin-requirements.md) - Requirements this phase implements
|
||||
- [Platform Capability Reference](./alarms/01-platform-capability-reference.md) - OS-level facts
|
||||
- [Phase 1](./android-implementation-directive-phase1.md) - Prerequisite
|
||||
- [Phase 2](./android-implementation-directive-phase2.md) - Prerequisite
|
||||
- [Phase 3 Emulator Testing](./alarms/PHASE3-EMULATOR-TESTING.md) - Test procedures
|
||||
- [Phase 3 Verification](./alarms/PHASE3-VERIFICATION.md) - Verification report
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
**Status**: Directive complete, ready for implementation
|
||||
**Last Updated**: November 2025
|
||||
|
||||
578
test-apps/android-test-app/test-phase3.sh
Executable file
578
test-apps/android-test-app/test-phase3.sh
Executable file
@@ -0,0 +1,578 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ========================================
|
||||
# Phase 3 Testing Script – Boot Recovery
|
||||
# ========================================
|
||||
|
||||
# --- Config -------------------------------------------------------------------
|
||||
|
||||
APP_ID="com.timesafari.dailynotification"
|
||||
APK_PATH="./app/build/outputs/apk/debug/app-debug.apk"
|
||||
ADB_BIN="${ADB_BIN:-adb}"
|
||||
|
||||
# Log tags / patterns (matched to actual ReactivationManager logs)
|
||||
REACTIVATION_TAG="DNP-REACTIVATION"
|
||||
SCENARIO_KEY="Detected scenario: "
|
||||
BOOT_SCENARIO_VALUE="BOOT"
|
||||
NONE_SCENARIO_VALUE="NONE"
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
section() {
|
||||
echo
|
||||
echo "========================================"
|
||||
echo "$1"
|
||||
echo "========================================"
|
||||
echo
|
||||
}
|
||||
|
||||
substep() {
|
||||
echo "→ $1"
|
||||
}
|
||||
|
||||
info() {
|
||||
echo -e "ℹ️ $1"
|
||||
}
|
||||
|
||||
ok() {
|
||||
echo -e "✅ $1"
|
||||
}
|
||||
|
||||
warn() {
|
||||
echo -e "⚠️ $1"
|
||||
}
|
||||
|
||||
error() {
|
||||
echo -e "❌ $1"
|
||||
}
|
||||
|
||||
pause() {
|
||||
echo
|
||||
read -rp "Press Enter when ready to continue..."
|
||||
echo
|
||||
}
|
||||
|
||||
ui_prompt() {
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "👆 UI ACTION REQUIRED"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo -e "$1"
|
||||
echo
|
||||
read -rp "Press Enter after completing the action above..."
|
||||
echo
|
||||
}
|
||||
|
||||
require_adb_device() {
|
||||
section "Pre-Flight Checks"
|
||||
|
||||
if ! $ADB_BIN devices | awk 'NR>1 && $2=="device"{found=1} END{exit !found}'; then
|
||||
error "No emulator/device in 'device' state. Start your emulator first."
|
||||
exit 1
|
||||
fi
|
||||
ok "ADB device connected"
|
||||
|
||||
info "Checking emulator status..."
|
||||
if ! $ADB_BIN shell getprop sys.boot_completed | grep -q "1"; then
|
||||
info "Waiting for emulator to boot..."
|
||||
$ADB_BIN wait-for-device
|
||||
while [ "$($ADB_BIN shell getprop sys.boot_completed)" != "1" ]; do
|
||||
sleep 2
|
||||
done
|
||||
fi
|
||||
ok "Emulator is ready"
|
||||
}
|
||||
|
||||
build_app() {
|
||||
section "Building Test App"
|
||||
|
||||
substep "Step 1: Building debug APK..."
|
||||
if ./gradlew :app:assembleDebug; then
|
||||
ok "Build successful"
|
||||
else
|
||||
error "Build failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -f "$APK_PATH" ]]; then
|
||||
ok "APK ready: $APK_PATH"
|
||||
else
|
||||
error "APK not found at $APK_PATH"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
install_app() {
|
||||
section "Installing App"
|
||||
|
||||
substep "Step 1: Uninstalling existing app (if present)..."
|
||||
set +e
|
||||
uninstall_output="$($ADB_BIN uninstall "$APP_ID" 2>&1)"
|
||||
uninstall_status=$?
|
||||
set -e
|
||||
|
||||
if [[ $uninstall_status -ne 0 ]]; then
|
||||
if grep -q "DELETE_FAILED_INTERNAL_ERROR" <<<"$uninstall_output"; then
|
||||
info "No existing app to uninstall (continuing)"
|
||||
else
|
||||
warn "Uninstall returned non-zero status: $uninstall_output (continuing anyway)"
|
||||
fi
|
||||
else
|
||||
ok "Previous app uninstall succeeded"
|
||||
fi
|
||||
|
||||
substep "Step 2: Installing new APK..."
|
||||
if $ADB_BIN install -r "$APK_PATH"; then
|
||||
ok "App installed successfully"
|
||||
else
|
||||
error "App installation failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
substep "Step 3: Verifying installation..."
|
||||
if $ADB_BIN shell pm list packages | grep -q "$APP_ID"; then
|
||||
ok "App verified in package list"
|
||||
else
|
||||
error "App not found in package list"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
info "Clearing logcat buffer..."
|
||||
$ADB_BIN logcat -c
|
||||
ok "Logs cleared"
|
||||
}
|
||||
|
||||
launch_app() {
|
||||
info "Launching app..."
|
||||
$ADB_BIN shell am start -n "${APP_ID}/.MainActivity" >/dev/null 2>&1
|
||||
sleep 3 # Give app time to load
|
||||
ok "App launched"
|
||||
}
|
||||
|
||||
clear_logs() {
|
||||
info "Clearing logcat buffer..."
|
||||
$ADB_BIN logcat -c
|
||||
ok "Logs cleared"
|
||||
}
|
||||
|
||||
show_alarms() {
|
||||
info "Checking AlarmManager status..."
|
||||
echo
|
||||
$ADB_BIN shell dumpsys alarm | grep -A3 "$APP_ID" || true
|
||||
echo
|
||||
}
|
||||
|
||||
count_alarms() {
|
||||
# Returns count of alarms for our app
|
||||
$ADB_BIN shell dumpsys alarm | grep -c "$APP_ID" || echo "0"
|
||||
}
|
||||
|
||||
reboot_emulator() {
|
||||
info "Rebooting emulator..."
|
||||
$ADB_BIN reboot
|
||||
ok "Reboot initiated"
|
||||
|
||||
info "Waiting for emulator to come back online..."
|
||||
$ADB_BIN wait-for-device
|
||||
while [ "$($ADB_BIN shell getprop sys.boot_completed)" != "1" ]; do
|
||||
sleep 2
|
||||
done
|
||||
ok "Emulator boot completed"
|
||||
}
|
||||
|
||||
get_recovery_logs() {
|
||||
# Collect recent reactivation logs
|
||||
$ADB_BIN logcat -d | grep "$REACTIVATION_TAG" || true
|
||||
}
|
||||
|
||||
extract_field_from_logs() {
|
||||
# Usage: extract_field_from_logs "<logs>" "<field_name>"
|
||||
local logs="$1"
|
||||
local field="$2"
|
||||
# Looks for patterns like "field=NUMBER" and returns NUMBER (or 0)
|
||||
local value
|
||||
value="$(grep -oE "${field}=[0-9]+" <<<"$logs" | tail -n1 | sed "s/${field}=//" || true)"
|
||||
if [[ -z "$value" ]]; then
|
||||
echo "0"
|
||||
else
|
||||
echo "$value"
|
||||
fi
|
||||
}
|
||||
|
||||
extract_scenario_from_logs() {
|
||||
local logs="$1"
|
||||
local scen
|
||||
# Looks for "Detected scenario: BOOT" or "Starting boot recovery" format
|
||||
if echo "$logs" | grep -qi "Starting boot recovery\|boot recovery"; then
|
||||
echo "$BOOT_SCENARIO_VALUE"
|
||||
else
|
||||
scen="$(grep -oE "${SCENARIO_KEY}[A-Z_]+" <<<"$logs" | tail -n1 | sed "s/${SCENARIO_KEY}//" || true)"
|
||||
echo "$scen"
|
||||
fi
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# TEST 1 – Boot with Future Alarms
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
test1_boot_future_alarms() {
|
||||
section "TEST 1: Boot with Future Alarms"
|
||||
|
||||
echo "Purpose: Verify alarms are recreated on boot when schedules have future run times."
|
||||
|
||||
pause
|
||||
|
||||
substep "Step 1: Launch app & check plugin status"
|
||||
launch_app
|
||||
|
||||
ui_prompt "In the app UI, verify plugin status:\n\n ⚙️ Plugin Settings: ✅ Configured\n 🔌 Native Fetcher: ✅ Configured\n\nIf either shows ❌ or 'Not configured', click 'Configure Plugin', wait until both are ✅, then press Enter."
|
||||
|
||||
ui_prompt "Now schedule at least one future notification (e.g., click 'Test Notification' to schedule for a few minutes in the future)."
|
||||
|
||||
substep "Step 2: Verify alarms are scheduled"
|
||||
show_alarms
|
||||
local before_count
|
||||
before_count="$(count_alarms)"
|
||||
info "Alarm count before reboot: $before_count"
|
||||
|
||||
if [[ "$before_count" -eq 0 ]]; then
|
||||
warn "No alarms found before reboot; TEST 1 may not be meaningful."
|
||||
fi
|
||||
|
||||
pause
|
||||
|
||||
substep "Step 3: Reboot emulator"
|
||||
warn "The emulator will reboot now. This will take 30-60 seconds."
|
||||
pause
|
||||
reboot_emulator
|
||||
|
||||
substep "Step 4: Collect boot recovery logs"
|
||||
info "Collecting recovery logs from boot..."
|
||||
sleep 2 # Give recovery a moment to complete
|
||||
local logs
|
||||
logs="$(get_recovery_logs)"
|
||||
echo "$logs"
|
||||
|
||||
local missed rescheduled verified errors scenario
|
||||
missed="$(extract_field_from_logs "$logs" "missed")"
|
||||
rescheduled="$(extract_field_from_logs "$logs" "rescheduled")"
|
||||
verified="$(extract_field_from_logs "$logs" "verified")"
|
||||
errors="$(extract_field_from_logs "$logs" "errors")"
|
||||
scenario="$(extract_scenario_from_logs "$logs")"
|
||||
|
||||
echo
|
||||
info "Parsed recovery summary:"
|
||||
echo " scenario = ${scenario:-<none>}"
|
||||
echo " missed = ${missed}"
|
||||
echo " rescheduled= ${rescheduled}"
|
||||
echo " verified = ${verified}"
|
||||
echo " errors = ${errors}"
|
||||
echo
|
||||
|
||||
if [[ "$errors" -gt 0 ]]; then
|
||||
error "Recovery reported errors>0 (errors=$errors)"
|
||||
fi
|
||||
|
||||
substep "Step 5: Verify alarms were recreated"
|
||||
show_alarms
|
||||
local after_count
|
||||
after_count="$(count_alarms)"
|
||||
info "Alarm count after boot: $after_count"
|
||||
|
||||
if [[ "$scenario" == "$BOOT_SCENARIO_VALUE" && "$rescheduled" -gt 0 ]]; then
|
||||
ok "TEST 1 PASSED: Boot recovery detected and alarms rescheduled (scenario=$scenario, rescheduled=$rescheduled)."
|
||||
elif echo "$logs" | grep -qi "Starting boot recovery\|boot recovery"; then
|
||||
if [[ "$rescheduled" -gt 0 ]]; then
|
||||
ok "TEST 1 PASSED: Boot recovery ran and alarms rescheduled (rescheduled=$rescheduled)."
|
||||
else
|
||||
warn "TEST 1: Boot recovery ran but rescheduled=0. Check implementation or logs."
|
||||
fi
|
||||
else
|
||||
warn "TEST 1: Boot recovery not clearly detected. Review logs and boot receiver implementation."
|
||||
info "Scenario detected: ${scenario:-<none>}, rescheduled=$rescheduled"
|
||||
fi
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# TEST 2 – Boot with Past Alarms
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
test2_boot_past_alarms() {
|
||||
section "TEST 2: Boot with Past Alarms"
|
||||
|
||||
echo "Purpose: Verify missed alarms are detected and next occurrence is scheduled on boot."
|
||||
|
||||
pause
|
||||
|
||||
substep "Step 1: Launch app & ensure plugin configured"
|
||||
launch_app
|
||||
|
||||
ui_prompt "In the app UI, verify plugin status:\n\n ⚙️ Plugin Settings: ✅ Configured\n 🔌 Native Fetcher: ✅ Configured\n\nIf needed, click 'Configure Plugin', then press Enter."
|
||||
|
||||
ui_prompt "Click 'Test Notification' to schedule a notification for 2 minutes in the future.\n\nAfter scheduling, we'll wait for the alarm time to pass, then reboot."
|
||||
|
||||
substep "Step 2: Wait for alarm time to pass"
|
||||
info "Waiting 3 minutes for scheduled alarm time to pass..."
|
||||
warn "You can manually advance system time if needed (requires root/emulator)"
|
||||
sleep 180 # Wait 3 minutes
|
||||
|
||||
substep "Step 3: Verify alarm time has passed"
|
||||
info "Alarm time should now be in the past"
|
||||
show_alarms
|
||||
|
||||
pause
|
||||
|
||||
substep "Step 4: Reboot emulator"
|
||||
warn "The emulator will reboot now. This will take 30-60 seconds."
|
||||
pause
|
||||
reboot_emulator
|
||||
|
||||
substep "Step 5: Collect boot recovery logs"
|
||||
info "Collecting recovery logs from boot..."
|
||||
sleep 2
|
||||
local logs
|
||||
logs="$(get_recovery_logs)"
|
||||
echo "$logs"
|
||||
|
||||
local missed rescheduled verified errors scenario
|
||||
missed="$(extract_field_from_logs "$logs" "missed")"
|
||||
rescheduled="$(extract_field_from_logs "$logs" "rescheduled")"
|
||||
verified="$(extract_field_from_logs "$logs" "verified")"
|
||||
errors="$(extract_field_from_logs "$logs" "errors")"
|
||||
scenario="$(extract_scenario_from_logs "$logs")"
|
||||
|
||||
echo
|
||||
info "Parsed recovery summary:"
|
||||
echo " scenario = ${scenario:-<none>}"
|
||||
echo " missed = ${missed}"
|
||||
echo " rescheduled= ${rescheduled}"
|
||||
echo " verified = ${verified}"
|
||||
echo " errors = ${errors}"
|
||||
echo
|
||||
|
||||
if [[ "$errors" -gt 0 ]]; then
|
||||
error "Recovery reported errors>0 (errors=$errors)"
|
||||
fi
|
||||
|
||||
if [[ "$missed" -ge 1 && "$rescheduled" -ge 1 ]]; then
|
||||
ok "TEST 2 PASSED: Past alarms detected and next occurrence scheduled (missed=$missed, rescheduled=$rescheduled)."
|
||||
elif [[ "$missed" -ge 1 ]]; then
|
||||
warn "TEST 2: Past alarms detected (missed=$missed) but rescheduled=$rescheduled. Check reschedule logic."
|
||||
else
|
||||
warn "TEST 2: No missed alarms detected. Verify alarm time actually passed before reboot."
|
||||
fi
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# TEST 3 – Boot with No Schedules
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
test3_boot_no_schedules() {
|
||||
section "TEST 3: Boot with No Schedules"
|
||||
|
||||
echo "Purpose: Verify boot recovery handles empty database gracefully."
|
||||
|
||||
pause
|
||||
|
||||
substep "Step 1: Uninstall app to clear DB/state"
|
||||
set +e
|
||||
$ADB_BIN uninstall "$APP_ID" >/dev/null 2>&1
|
||||
set -e
|
||||
ok "App uninstalled (state cleared)"
|
||||
|
||||
substep "Step 2: Reinstall app"
|
||||
if $ADB_BIN install -r "$APK_PATH"; then
|
||||
ok "App installed"
|
||||
else
|
||||
error "Reinstall failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
info "Clearing logcat..."
|
||||
$ADB_BIN logcat -c
|
||||
ok "Logs cleared"
|
||||
|
||||
pause
|
||||
|
||||
substep "Step 3: Reboot emulator WITHOUT scheduling anything"
|
||||
warn "Do NOT schedule any notifications. The app should have no schedules in the database."
|
||||
warn "The emulator will reboot now. This will take 30-60 seconds."
|
||||
pause
|
||||
reboot_emulator
|
||||
|
||||
substep "Step 4: Collect boot recovery logs"
|
||||
info "Collecting recovery logs from boot..."
|
||||
sleep 2
|
||||
local logs
|
||||
logs="$($ADB_BIN logcat -d | grep "$REACTIVATION_TAG" || true)"
|
||||
echo "$logs"
|
||||
|
||||
local scenario rescheduled missed
|
||||
scenario="$(extract_scenario_from_logs "$logs")"
|
||||
rescheduled="$(extract_field_from_logs "$logs" "rescheduled")"
|
||||
missed="$(extract_field_from_logs "$logs" "missed")"
|
||||
|
||||
echo
|
||||
info "Parsed recovery summary:"
|
||||
echo " scenario = ${scenario:-<none>}"
|
||||
echo " rescheduled= ${rescheduled}"
|
||||
echo " missed = ${missed}"
|
||||
echo
|
||||
|
||||
if [[ -z "$logs" ]]; then
|
||||
ok "TEST 3 PASSED: No recovery logs when there are no schedules (safe behavior)."
|
||||
return
|
||||
fi
|
||||
|
||||
if echo "$logs" | grep -qiE "No schedules found|No schedules present"; then
|
||||
ok "TEST 3 PASSED: Explicit 'No schedules found' message logged with no rescheduling."
|
||||
elif [[ "$scenario" == "$NONE_SCENARIO_VALUE" && "$rescheduled" -eq 0 ]]; then
|
||||
ok "TEST 3 PASSED: NONE scenario detected with no rescheduling."
|
||||
elif [[ "$rescheduled" -gt 0 ]]; then
|
||||
warn "TEST 3: rescheduled>0 on first launch / empty DB. Check that boot recovery isn't misfiring."
|
||||
else
|
||||
info "TEST 3: Logs present but no rescheduling; review scenario handling to ensure it's explicit about NONE / NO_SCHEDULES."
|
||||
fi
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# TEST 4 – Silent Boot Recovery (App Never Opened)
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
test4_silent_boot_recovery() {
|
||||
section "TEST 4: Silent Boot Recovery (App Never Opened)"
|
||||
|
||||
echo "Purpose: Verify boot recovery occurs even when the app is never opened after reboot."
|
||||
|
||||
pause
|
||||
|
||||
substep "Step 1: Launch app & ensure plugin configured"
|
||||
launch_app
|
||||
|
||||
ui_prompt "In the app UI, verify plugin status:\n\n ⚙️ Plugin Settings: ✅ Configured\n 🔌 Native Fetcher: ✅ Configured\n\nIf needed, click 'Configure Plugin', then press Enter."
|
||||
|
||||
ui_prompt "Click 'Test Notification' to schedule a notification for a few minutes in the future."
|
||||
|
||||
substep "Step 2: Verify alarms are scheduled"
|
||||
show_alarms
|
||||
local before_count
|
||||
before_count="$(count_alarms)"
|
||||
info "Alarm count before reboot: $before_count"
|
||||
|
||||
if [[ "$before_count" -eq 0 ]]; then
|
||||
warn "No alarms found; TEST 4 may not be meaningful."
|
||||
fi
|
||||
|
||||
pause
|
||||
|
||||
substep "Step 3: Reboot emulator (DO NOT open app after reboot)"
|
||||
warn "IMPORTANT: After reboot, DO NOT open the app. Boot recovery should run silently."
|
||||
warn "The emulator will reboot now. This will take 30-60 seconds."
|
||||
pause
|
||||
reboot_emulator
|
||||
|
||||
substep "Step 4: Collect boot recovery logs (without opening app)"
|
||||
info "Collecting recovery logs from boot (app was NOT opened)..."
|
||||
sleep 2
|
||||
local logs
|
||||
logs="$(get_recovery_logs)"
|
||||
echo "$logs"
|
||||
|
||||
local missed rescheduled verified errors scenario
|
||||
missed="$(extract_field_from_logs "$logs" "missed")"
|
||||
rescheduled="$(extract_field_from_logs "$logs" "rescheduled")"
|
||||
verified="$(extract_field_from_logs "$logs" "verified")"
|
||||
errors="$(extract_field_from_logs "$logs" "errors")"
|
||||
scenario="$(extract_scenario_from_logs "$logs")"
|
||||
|
||||
echo
|
||||
info "Parsed recovery summary:"
|
||||
echo " scenario = ${scenario:-<none>}"
|
||||
echo " missed = ${missed}"
|
||||
echo " rescheduled= ${rescheduled}"
|
||||
echo " verified = ${verified}"
|
||||
echo " errors = ${errors}"
|
||||
echo
|
||||
|
||||
substep "Step 5: Verify alarms were recreated (without opening app)"
|
||||
show_alarms
|
||||
local after_count
|
||||
after_count="$(count_alarms)"
|
||||
info "Alarm count after boot (app never opened): $after_count"
|
||||
|
||||
if [[ "$after_count" -gt 0 && "$rescheduled" -gt 0 ]]; then
|
||||
ok "TEST 4 PASSED: Boot recovery occurred silently and alarms were recreated (rescheduled=$rescheduled) without app launch."
|
||||
elif [[ "$rescheduled" -gt 0 ]]; then
|
||||
ok "TEST 4 PASSED: Boot recovery occurred silently (rescheduled=$rescheduled), but alarm count check unclear."
|
||||
elif echo "$logs" | grep -qi "Starting boot recovery\|boot recovery"; then
|
||||
warn "TEST 4: Boot recovery ran but alarms may not have been recreated. Check logs and implementation."
|
||||
else
|
||||
warn "TEST 4: Boot recovery not detected. Verify boot receiver is registered and has BOOT_COMPLETED permission."
|
||||
fi
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Main
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
main() {
|
||||
echo
|
||||
echo "========================================"
|
||||
echo "Phase 3 Testing Script – Boot Recovery"
|
||||
echo "========================================"
|
||||
echo
|
||||
echo "This script will guide you through all Phase 3 tests."
|
||||
echo "You'll be prompted when UI interaction is needed."
|
||||
echo
|
||||
echo "⚠️ WARNING: This script will reboot the emulator multiple times."
|
||||
echo " Each reboot takes 30-60 seconds."
|
||||
echo
|
||||
|
||||
pause
|
||||
|
||||
require_adb_device
|
||||
build_app
|
||||
install_app
|
||||
|
||||
test1_boot_future_alarms
|
||||
pause
|
||||
|
||||
test2_boot_past_alarms
|
||||
pause
|
||||
|
||||
test3_boot_no_schedules
|
||||
pause
|
||||
|
||||
test4_silent_boot_recovery
|
||||
|
||||
section "Testing Complete"
|
||||
|
||||
echo "Test Results Summary (see logs above for details):"
|
||||
echo
|
||||
echo "TEST 1: Boot with Future Alarms"
|
||||
echo " - Check logs for scenario=$BOOT_SCENARIO_VALUE and rescheduled>0"
|
||||
echo
|
||||
echo "TEST 2: Boot with Past Alarms"
|
||||
echo " - Check that missed>=1 and rescheduled>=1"
|
||||
echo
|
||||
echo "TEST 3: Boot with No Schedules"
|
||||
echo " - Check that no recovery runs, or NONE scenario is logged with rescheduled=0"
|
||||
echo
|
||||
echo "TEST 4: Silent Boot Recovery"
|
||||
echo " - Check that boot recovery occurred and alarms were recreated without app launch"
|
||||
echo
|
||||
|
||||
ok "Phase 3 testing script complete!"
|
||||
echo
|
||||
echo "Next steps:"
|
||||
echo " - Review logs above"
|
||||
echo " - Capture snippets into PHASE3-EMULATOR-TESTING.md"
|
||||
echo " - Update PHASE3-VERIFICATION.md and unified directive status matrix"
|
||||
echo
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
Reference in New Issue
Block a user