docs(openspec): add SDD artifact trail for recent alarm and EQ changes
Persist the exploration, proposal, spec, design, tasks, and verify/archive reports produced during the multi-device EQ, alarm-countdown, and notification-visual-polish SDD cycles.
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
# Tasks: Pre-notice Live Countdown
|
||||
|
||||
Change: `pre-notice-live-countdown`
|
||||
Spec: `sdd/pre-notice-live-countdown/spec`
|
||||
Design: `sdd/pre-notice-live-countdown/design`
|
||||
|
||||
## Notes on Verification Approach
|
||||
|
||||
This is a **Kotlin-only** change (`AlarmScheduler.kt`, `PluriWaveAlarmReceiver.kt`) inside an Android host module with **no Kotlin unit test framework configured** (confirmed in prior verify reports for this project — JUnit/Robolectric/instrumentation harness absent). Strict TDD Mode applies to this repo's Dart/Flutter code only; it does **not** apply here because there is no RED step available (no test runner to fail first).
|
||||
|
||||
Tasks below therefore follow **implement -> manual/code-inspection verify** instead of RED-GREEN-REFACTOR:
|
||||
- Each implementation task has a paired verification task that is a concrete, checkable inspection (read the diff, trace the call graph, confirm requestCode arithmetic matches, confirm grep counts) — not "looks good".
|
||||
- Where a real device/emulator check is feasible (notification updates, Doze jump behavior) it is called out explicitly as manual QA, separate from code inspection.
|
||||
|
||||
## 1. AlarmScheduler.kt — Core Tick Engine (Sequential, single file)
|
||||
|
||||
### 1.1 [x] Add `armNextPreNoticeCountdownTick` to `AlarmScheduler.kt`
|
||||
- Satisfies: Requirement "Per-Minute Tick Re-Arm", Requirement "Self-Stop at Final Minute"
|
||||
- Location: new private/internal function near `armNextSnoozeCountdownTick` (around L435), in `AlarmScheduler.kt`
|
||||
- Mirror `armNextSnoozeCountdownTick` signature/shape per design: `armNextPreNoticeCountdownTick(id, triggerAtMillis, title, snoozeMinutes, occurrenceAtMillis, remaining)`
|
||||
- Early-return when `remaining <= 1L` (no re-arm on final minute — design "Open Questions" + spec "Self-Stop at Final Minute")
|
||||
- Compute `nextBoundary = triggerAtMillis - (remaining - 1L) * 60_000L`
|
||||
- Build `PendingIntent.getBroadcast` with `action = PluriWaveAlarmReceiver.ACTION_PRE_NOTICE` (REUSE, no new action constant) and full extras (`EXTRA_ALARM_ID`, `EXTRA_ALARM_TITLE`, `EXTRA_SNOOZE_MINUTES`, `EXTRA_TRIGGER_AT`, `EXTRA_OCCURRENCE_AT`) — same extras `schedulePreNotice` already sends (L155-162)
|
||||
- Use `requestCode(id, 9)` — slot 9, MUST be in `AlarmScheduler.requestCode` (31*hash+slot formula, L864) per design's critical gotcha
|
||||
- Call `alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, nextBoundary, pending)` wrapped in `try/catch (SecurityException)`, mirroring L450-459
|
||||
- Function MUST be `public` (design: receiver calls it directly)
|
||||
- Parallel-safe: NO — must land before 1.2 (cancel function needs to exist alongside, both reviewed together) and before 2.x (receiver depends on this signature)
|
||||
|
||||
### 1.2 [x] Add `cancelPreNoticeCountdown(id)` to `AlarmScheduler.kt`
|
||||
- Satisfies: Requirement "Tick Chain Cancellation" (defines the primitive used by all 5 cancellation sites)
|
||||
- Location: new public function near `cancelSnoozeCountdown` (around L462), in `AlarmScheduler.kt`
|
||||
- Mirror `cancelSnoozeCountdown` shape: build `PendingIntent.getBroadcast` with `requestCode(id, 9)`, `action = PluriWaveAlarmReceiver.ACTION_PRE_NOTICE`, `PendingIntent.FLAG_NO_CREATE or PendingIntent.FLAG_IMMUTABLE`
|
||||
- Call `cancelPending("preNoticeCountdown", pending)` (reuse existing `cancelPending` helper, same as L471)
|
||||
- Function MUST be `public` (design: receiver's SKIP_NEXT/POSTPONE_NEXT handlers call it directly)
|
||||
- Parallel-safe: NO — same file/region as 1.1, sequential
|
||||
|
||||
### 1.3 [x] Switch `armNextPreNoticeCountdownTick` minute math to reuse existing `ceilMinutes()`
|
||||
- Satisfies: Requirement "Consistent Rounding via ceilMinutes"
|
||||
- Verify `ceilMinutes()` at L551-552 is reachable from the new function (confirmed class-level private in design — no duplication needed, same class)
|
||||
- This task is really a checkpoint folded into 1.1's implementation: confirm 1.1 uses `ceilMinutes()` for any remaining-minutes math it does (the boundary math itself uses raw arithmetic per design; `ceilMinutes` is invoked at the call site / by the receiver, not inside the arm function — see task 2.1)
|
||||
- Parallel-safe: NO — depends on 1.1
|
||||
|
||||
### 1.4 [x] [VERIFY] Code-inspect `armNextPreNoticeCountdownTick` + `cancelPreNoticeCountdown`
|
||||
- Inspection checklist (no test runner available, must be done by reading the diff):
|
||||
- [x] `armNextPreNoticeCountdownTick` is declared in `AlarmScheduler.kt`, NOT in `PluriWaveAlarmReceiver.kt`
|
||||
- [x] `cancelPreNoticeCountdown` is declared in `AlarmScheduler.kt`, NOT in `PluriWaveAlarmReceiver.kt`
|
||||
- [x] Both use `requestCode(id, 9)` resolving through `AlarmScheduler.requestCode` (the `31 * id.hashCode() + slot` formula at L864) — NOT `PluriWaveAlarmReceiver.requestCode` (`47 * id.hashCode() + slot`)
|
||||
- [x] `armNextPreNoticeCountdownTick` early-returns (no-op) when `remaining <= 1L`
|
||||
- [x] `armNextPreNoticeCountdownTick` and `cancelPreNoticeCountdown` are both `public` (callable from `PluriWaveAlarmReceiver`)
|
||||
- [x] `cancelPreNoticeCountdown` uses `PendingIntent.FLAG_NO_CREATE` (cancel-only, does not recreate)
|
||||
- [x] No new `ACTION_*` constant was introduced — both functions reference `PluriWaveAlarmReceiver.ACTION_PRE_NOTICE`
|
||||
- Parallel-safe: NO — gate before proceeding to section 2
|
||||
|
||||
## 2. AlarmScheduler.kt — Wire 3 of the 5 Cancellation Sites (Sequential, same file as section 1)
|
||||
|
||||
### 2.1 [x] Wire cancellation site 1/5: `cancelAlarm(id)`
|
||||
- Satisfies: Requirement "Tick Chain Cancellation" — Scenario "Full alarm cancellation tears down tick chain"
|
||||
- Location: `AlarmScheduler.kt` L554-568, alongside the existing `cancelSnoozeCountdown(id)` call at L561
|
||||
- Add `cancelPreNoticeCountdown(id)` directly after `cancelSnoozeCountdown(id)`
|
||||
- Parallel-safe: YES (with 2.2, 2.3 — distinct branches in the same file, no shared local state; serialize the actual edit application to avoid diff collisions, but design/review can happen in parallel)
|
||||
|
||||
### 2.2 [x] Wire cancellation site 2/5: `scheduleSpec` no-next-trigger branch
|
||||
- Satisfies: Requirement "Tick Chain Cancellation" — Scenario "No-next-trigger reschedule cancels tick chain"
|
||||
- Location: `AlarmScheduler.kt` L87-93, alongside the existing `cancelPending("preNotice", ...)` call at L92
|
||||
- Add `cancelPreNoticeCountdown(spec.id)` in this branch (after the existing preNotice single-shot PI cancel)
|
||||
- Parallel-safe: YES (with 2.1, 2.3 — distinct branch)
|
||||
|
||||
### 2.3 [x] Wire cancellation site 3/5: `schedulePreNotice` snooze-transition branch
|
||||
- Satisfies: Requirement "Tick Chain Cancellation" — Scenario "Snooze transition cancels pre-notice tick chain"
|
||||
- Location: `AlarmScheduler.kt` L140-144 (the `if (spec.snoozeUntilMillis != null)` early-return inside `schedulePreNotice`, which currently only cancels the single-shot preNotice PI)
|
||||
- Add `cancelPreNoticeCountdown(spec.id)` in this branch, alongside the existing `cancelPending("preNotice", ...)` call
|
||||
- Parallel-safe: YES (with 2.1, 2.2 — distinct branch)
|
||||
|
||||
### 2.4 [x] [VERIFY] Code-inspect the 3 in-scheduler cancellation sites
|
||||
- Inspection checklist:
|
||||
- [x] `cancelAlarm(id)` (L554-568 region) calls `cancelPreNoticeCountdown(id)`
|
||||
- [x] `scheduleSpec` no-trigger branch (L87-93 region) calls `cancelPreNoticeCountdown(spec.id)`
|
||||
- [x] `schedulePreNotice` snooze-transition branch (L140-144 region) calls `cancelPreNoticeCountdown(spec.id)`
|
||||
- [x] `grep -n "cancelPreNoticeCountdown" AlarmScheduler.kt` returns exactly: 1 declaration + 3 call sites so far (more added in section 3 from the receiver side) — confirms no site was missed or duplicated
|
||||
- Parallel-safe: NO — gate before section 3
|
||||
|
||||
## 3. PluriWaveAlarmReceiver.kt — Re-arm on Tick + Remaining 2 Cancellation Sites (Sequential, single file)
|
||||
|
||||
### 3.1 [x] Make `ACTION_PRE_NOTICE` handler re-arm the next tick after posting
|
||||
- Satisfies: Requirement "First Pre-Notice Post", Requirement "Per-Minute Tick Re-Arm", Requirement "Tick updates notification content"
|
||||
- Location: `PluriWaveAlarmReceiver.kt` `showPreNoticeNotification` (L131-200), called from the `ACTION_PRE_NOTICE` branch (L47-56)
|
||||
- After successfully posting/updating the notification (after the `NotificationManagerCompat...notify(...)` call at L195), compute `remaining` via `ceilMinutes()`-based logic and call `AlarmScheduler(context).armNextPreNoticeCountdownTick(alarmId, triggerAtMillis, title, snoozeMinutes, occurrenceAtMillis, remaining)`
|
||||
- Design's open question: pass `remaining` computed once (avoid double clock-read causing off-by-one between displayed text and next-boundary math) — compute `remaining` a single time in `showPreNoticeNotification` and use that same value both for `AlarmNotificationStrings.preNoticeText(...)` and for the arm call
|
||||
- Parallel-safe: NO — must land before 3.2/3.3 are meaningfully testable together, but see note below
|
||||
|
||||
### 3.2 [x] Replace `computeRemainingMinutes()` with `ceilMinutes()` semantics in the receiver
|
||||
- Satisfies: Requirement "Consistent Rounding via ceilMinutes"
|
||||
- Location: `PluriWaveAlarmReceiver.kt` L206-207 (`computeRemainingMinutes`, floor-based: `(triggerAtMillis - now) / 60_000L`)
|
||||
- Replace the floor-based computation with `ceilMinutes()` semantics (`maxOf(1L, (deltaMillis + 59_999L) / 60_000L)`), matching `AlarmScheduler.ceilMinutes()` at L551-552
|
||||
- Decide and apply consistently per design: either (a) inline the ceil formula in the receiver (duplication, but receiver and scheduler are different classes — `ceilMinutes` in `AlarmScheduler` is class-private per design notes, "directly reusable" refers to scheduler-internal reuse, not cross-class), or (b) expose a small shared helper. Given design explicitly says "class-level private, NOT snooze-private — directly reusable" in the context of `AlarmScheduler`, the receiver still needs its own copy of the formula since it's a different class — duplicate the one-line `ceilMinutes` formula in the receiver, matching the scheduler's exactly, OR have the receiver call into the scheduler instance it already constructs (`AlarmScheduler(context)`) if that's promoted to public. Pick the option that does not require new public surface beyond what's already needed (prefer inlining the formula to avoid scope creep)
|
||||
- Parallel-safe: NO — same function area as 3.1 (both touch `showPreNoticeNotification` / its remaining-minutes computation), sequential
|
||||
|
||||
### 3.3 [x] Wire cancellation site 4/5: `ACTION_SKIP_NEXT` handler
|
||||
- Satisfies: Requirement "Tick Chain Cancellation" — Scenario "Skip-next action cancels tick chain"
|
||||
- Location: `PluriWaveAlarmReceiver.kt` L77-92 (`ACTION_SKIP_NEXT` branch)
|
||||
- Add `AlarmScheduler(context).cancelPreNoticeCountdown(alarmId)` BEFORE the call to `AlarmScheduler(context).skipNext(alarmId)` (L79) — design specifies cancel-then-reschedule ordering since `skipNext` -> `scheduleSpec` will arm a fresh chain
|
||||
- Parallel-safe: YES (with 3.4 — distinct branch in same file; serialize edit application)
|
||||
|
||||
### 3.4 [x] Wire cancellation site 5/5: `ACTION_POSTPONE_NEXT` handler
|
||||
- Satisfies: Requirement "Tick Chain Cancellation" — Scenario "Postpone-next action cancels tick chain"
|
||||
- Location: `PluriWaveAlarmReceiver.kt` L57-76 (`ACTION_POSTPONE_NEXT` branch)
|
||||
- Add `AlarmScheduler(context).cancelPreNoticeCountdown(alarmId)` BEFORE the call to `AlarmScheduler(context).postponeNext(alarmId, snoozeMinutes)` (L59) — postpone transitions into snooze, where snooze-countdown takes over per the mutual-exclusivity invariant
|
||||
- Parallel-safe: YES (with 3.3 — distinct branch in same file; serialize edit application)
|
||||
|
||||
### 3.5 [x] [VERIFY] Code-inspect receiver changes
|
||||
- Inspection checklist:
|
||||
- [x] `showPreNoticeNotification` computes `remaining` exactly once and reuses that single value for both notification text and the `armNextPreNoticeCountdownTick` call (no second `System.currentTimeMillis()` read causing drift)
|
||||
- [x] `ACTION_PRE_NOTICE` branch results in a call to `AlarmScheduler(context).armNextPreNoticeCountdownTick(...)` after the notification is posted
|
||||
- [x] `ACTION_SKIP_NEXT` branch calls `cancelPreNoticeCountdown(alarmId)` BEFORE `skipNext(alarmId)`
|
||||
- [x] `ACTION_POSTPONE_NEXT` branch calls `cancelPreNoticeCountdown(alarmId)` BEFORE `postponeNext(alarmId, snoozeMinutes)`
|
||||
- [x] Old floor-based `computeRemainingMinutes()` is no longer used for pre-notice display — replaced in place with ceil-based logic (kept as the single computation function, now ceiling-rounded, still the only call site for pre-notice remaining-minutes)
|
||||
- [x] No new `ACTION_*` constant added to the `companion object` (L231+)
|
||||
- Parallel-safe: NO — gate before section 4
|
||||
|
||||
## 4. Full 5-Site Cancellation Cross-Check (Sequential, spans both files)
|
||||
|
||||
### 4.1 [x] [VERIFY] Enumerate and confirm all 5 cancellation sites are wired
|
||||
This is the change's single highest-risk item per the design's "CRITICAL GOTCHA" — list each site explicitly and confirm:
|
||||
|
||||
1. [x] `AlarmScheduler.cancelAlarm(id)` (L554-568 region) — calls `cancelPreNoticeCountdown(id)` (task 2.1)
|
||||
2. [x] `AlarmScheduler.scheduleSpec` no-next-trigger branch (L87-93 region) — calls `cancelPreNoticeCountdown(spec.id)` (task 2.2)
|
||||
3. [x] `AlarmScheduler.schedulePreNotice` snooze-transition branch (L140-144 region) — calls `cancelPreNoticeCountdown(spec.id)` (task 2.3)
|
||||
4. [x] `PluriWaveAlarmReceiver` `ACTION_SKIP_NEXT` handler (L77-92 region) — calls `cancelPreNoticeCountdown(alarmId)` (task 3.3)
|
||||
5. [x] `PluriWaveAlarmReceiver` `ACTION_POSTPONE_NEXT` handler (L57-76 region) — calls `cancelPreNoticeCountdown(alarmId)` (task 3.4)
|
||||
|
||||
- Confirm via `grep -rn "cancelPreNoticeCountdown" android/app/src/main/kotlin/es/freetimelab/pluriwave/` that the count is exactly: 1 declaration (`AlarmScheduler.kt`) + 5 call sites (3 in `AlarmScheduler.kt`, 2 in `PluriWaveAlarmReceiver.kt`) = 6 total matches
|
||||
- Confirm both `armNextPreNoticeCountdownTick` and `cancelPreNoticeCountdown` resolve `requestCode` through `AlarmScheduler`'s own `requestCode(id, slot) = 31 * id.hashCode() + slot` — re-confirm this did NOT silently get called via `PluriWaveAlarmReceiver`'s `47 * id.hashCode() + slot` formula anywhere (that would make arm/cancel PendingIntents mismatch and leak the chain)
|
||||
- Parallel-safe: NO — single gating checkpoint, blocks section 5
|
||||
|
||||
## 5. Manual / Device QA (Sequential, requires emulator or physical device — no automated harness available)
|
||||
|
||||
### 5.1 [MANUAL QA] Happy-path countdown on real/emulated device
|
||||
- Satisfies: Proposal Success Criteria "Pre-notice updates each minute 29->1 with device idle/screen-off"
|
||||
- Schedule a test alarm ~3-5 minutes out (shrink the 30-min window for practical testing by temporarily adjusting `PRE_NOTICE_MILLIS` constant value locally, or schedule far enough out and observe the last few ticks before fire)
|
||||
- Confirm notification updates in place (same notification ID, no duplicate entries) each minute boundary
|
||||
- Parallel-safe: NO
|
||||
|
||||
### 5.2 [MANUAL QA] Self-stop at final minute, fire alarm takes over
|
||||
- Satisfies: Requirement "Self-Stop at Final Minute"
|
||||
- Confirm no `ACTION_PRE_NOTICE` tick fires when `remaining <= 1`; confirm the real `setAlarmClock` fire alarm rings on schedule
|
||||
- Parallel-safe: YES (with 5.3, 5.4 — independent device sessions, but practically run sequentially on one test device)
|
||||
|
||||
### 5.3 [MANUAL QA] Skip/Postpone/Snooze-transition tear down chain, no orphaned alarm
|
||||
- Satisfies: Requirement "Tick Chain Cancellation" (all 5 scenarios), Proposal Success Criteria "Skip-next/postpone-next cancel the tick chain", "Snooze transition cancels pre-notice tick chain; no double-notification"
|
||||
- Trigger skip, postpone, and snooze mid-chain on separate test runs; confirm via `adb shell dumpsys alarm | grep pluriwave` (or logcat `alarm.snoozeCountdown` / `alarm.schedule preNotice` tags) that no stale `ACTION_PRE_NOTICE` slot-9 PendingIntent remains armed after each transition
|
||||
- Parallel-safe: YES (with 5.2, 5.4)
|
||||
|
||||
### 5.4 [MANUAL QA] Doze-delayed tick jumps forward, does not crash/loop
|
||||
- Satisfies: Requirement "Self-Healing Minute Computation" — Scenario "Missed tick self-heals by jumping, not crashing"
|
||||
- Use `adb shell dumpsys deviceidle force-idle` (or equivalent Doze simulation) to delay a tick; confirm the next tick recomputes remaining minutes from wall clock and displays a forward jump (e.g. 15 -> 12) rather than a stale or negative value
|
||||
- Parallel-safe: YES (with 5.2, 5.3)
|
||||
|
||||
### 5.5 [MANUAL QA] Snooze-countdown regression check
|
||||
- Satisfies: Proposal Success Criteria "Snooze countdown unchanged (no regression)"
|
||||
- Run the existing snooze-countdown flow (snooze an alarm, observe per-minute countdown) and confirm it behaves identically to pre-change behavior — slot 8 / `ACTION_SNOOZE_COUNTDOWN` path untouched by this change
|
||||
- Parallel-safe: YES (with 5.2, 5.3, 5.4)
|
||||
|
||||
## Dependency Graph
|
||||
|
||||
```
|
||||
1.1 -> 1.2 -> 1.3 -> 1.4 [VERIFY GATE]
|
||||
|
|
||||
v
|
||||
2.1, 2.2, 2.3 (parallel design, serial apply) -> 2.4 [VERIFY GATE]
|
||||
|
|
||||
v
|
||||
3.1 -> 3.2 -> 3.3, 3.4 (parallel design, serial apply) -> 3.5 [VERIFY GATE]
|
||||
|
|
||||
v
|
||||
4.1 [VERIFY GATE — 5-site cross-check]
|
||||
|
|
||||
v
|
||||
5.1 -> 5.2, 5.3, 5.4, 5.5 (parallel manual QA sessions)
|
||||
```
|
||||
|
||||
No task in this change can run fully independently of the others — both files are small and the two new functions (`armNextPreNoticeCountdownTick`, `cancelPreNoticeCountdown`) are shared dependencies for every cancellation-site task and the receiver re-arm task. "Parallel-safe" above means parallel in *review/design reasoning*; the actual file edits should still be applied serially to avoid diff collisions in two small files.
|
||||
|
||||
## Review Workload Forecast
|
||||
|
||||
- Files touched: 2 (`AlarmScheduler.kt`, `PluriWaveAlarmReceiver.kt`)
|
||||
- Estimated changed lines: ~90-130 (2 new functions ~25-35 lines each in `AlarmScheduler.kt`; 5 small call-site insertions of 1-2 lines each; receiver re-arm wiring + ceilMinutes swap ~15-25 lines)
|
||||
- **400-line budget risk: Low** — well under threshold, single small PR is appropriate
|
||||
- **Chained PRs recommended: No**
|
||||
- **Decision needed before apply: No** — proceed with `delivery_strategy: ask-on-risk` as a single PR; no risk threshold triggered
|
||||
- Primary review focus: the requestCode formula (slot 9, `AlarmScheduler`'s `31*hash+slot`, NOT the receiver's `47*hash+slot`) and the 5-site cancellation cross-check (section 4.1) — these are the two failure modes called out explicitly in the design as silent/non-crashing (PendingIntent mismatch leaks a repeating alarm with no visible error)
|
||||
- Suggested reviewer pass order: section 1 (engine) first in isolation, then section 4.1's grep-based cross-check as the acceptance gate before merging, manual QA (section 5) can follow merge if device access is constrained at review time but MUST complete before this change is considered done
|
||||
Reference in New Issue
Block a user