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.
18 KiB
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), inAlarmScheduler.kt - Mirror
armNextSnoozeCountdownTicksignature/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.getBroadcastwithaction = 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 extrasschedulePreNoticealready sends (L155-162) - Use
requestCode(id, 9)— slot 9, MUST be inAlarmScheduler.requestCode(31*hash+slot formula, L864) per design's critical gotcha - Call
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, nextBoundary, pending)wrapped intry/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), inAlarmScheduler.kt - Mirror
cancelSnoozeCountdownshape: buildPendingIntent.getBroadcastwithrequestCode(id, 9),action = PluriWaveAlarmReceiver.ACTION_PRE_NOTICE,PendingIntent.FLAG_NO_CREATE or PendingIntent.FLAG_IMMUTABLE - Call
cancelPending("preNoticeCountdown", pending)(reuse existingcancelPendinghelper, 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;ceilMinutesis 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):
armNextPreNoticeCountdownTickis declared inAlarmScheduler.kt, NOT inPluriWaveAlarmReceiver.ktcancelPreNoticeCountdownis declared inAlarmScheduler.kt, NOT inPluriWaveAlarmReceiver.kt- Both use
requestCode(id, 9)resolving throughAlarmScheduler.requestCode(the31 * id.hashCode() + slotformula at L864) — NOTPluriWaveAlarmReceiver.requestCode(47 * id.hashCode() + slot) armNextPreNoticeCountdownTickearly-returns (no-op) whenremaining <= 1LarmNextPreNoticeCountdownTickandcancelPreNoticeCountdownare bothpublic(callable fromPluriWaveAlarmReceiver)cancelPreNoticeCountdownusesPendingIntent.FLAG_NO_CREATE(cancel-only, does not recreate)- No new
ACTION_*constant was introduced — both functions referencePluriWaveAlarmReceiver.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.ktL554-568, alongside the existingcancelSnoozeCountdown(id)call at L561 - Add
cancelPreNoticeCountdown(id)directly aftercancelSnoozeCountdown(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.ktL87-93, alongside the existingcancelPending("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.ktL140-144 (theif (spec.snoozeUntilMillis != null)early-return insideschedulePreNotice, which currently only cancels the single-shot preNotice PI) - Add
cancelPreNoticeCountdown(spec.id)in this branch, alongside the existingcancelPending("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:
cancelAlarm(id)(L554-568 region) callscancelPreNoticeCountdown(id)scheduleSpecno-trigger branch (L87-93 region) callscancelPreNoticeCountdown(spec.id)schedulePreNoticesnooze-transition branch (L140-144 region) callscancelPreNoticeCountdown(spec.id)grep -n "cancelPreNoticeCountdown" AlarmScheduler.ktreturns 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.ktshowPreNoticeNotification(L131-200), called from theACTION_PRE_NOTICEbranch (L47-56) - After successfully posting/updating the notification (after the
NotificationManagerCompat...notify(...)call at L195), computeremainingviaceilMinutes()-based logic and callAlarmScheduler(context).armNextPreNoticeCountdownTick(alarmId, triggerAtMillis, title, snoozeMinutes, occurrenceAtMillis, remaining) - Design's open question: pass
remainingcomputed once (avoid double clock-read causing off-by-one between displayed text and next-boundary math) — computeremaininga single time inshowPreNoticeNotificationand use that same value both forAlarmNotificationStrings.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.ktL206-207 (computeRemainingMinutes, floor-based:(triggerAtMillis - now) / 60_000L) - Replace the floor-based computation with
ceilMinutes()semantics (maxOf(1L, (deltaMillis + 59_999L) / 60_000L)), matchingAlarmScheduler.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 —
ceilMinutesinAlarmScheduleris 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 ofAlarmScheduler, the receiver still needs its own copy of the formula since it's a different class — duplicate the one-lineceilMinutesformula 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.ktL77-92 (ACTION_SKIP_NEXTbranch) - Add
AlarmScheduler(context).cancelPreNoticeCountdown(alarmId)BEFORE the call toAlarmScheduler(context).skipNext(alarmId)(L79) — design specifies cancel-then-reschedule ordering sinceskipNext->scheduleSpecwill 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.ktL57-76 (ACTION_POSTPONE_NEXTbranch) - Add
AlarmScheduler(context).cancelPreNoticeCountdown(alarmId)BEFORE the call toAlarmScheduler(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:
showPreNoticeNotificationcomputesremainingexactly once and reuses that single value for both notification text and thearmNextPreNoticeCountdownTickcall (no secondSystem.currentTimeMillis()read causing drift)ACTION_PRE_NOTICEbranch results in a call toAlarmScheduler(context).armNextPreNoticeCountdownTick(...)after the notification is postedACTION_SKIP_NEXTbranch callscancelPreNoticeCountdown(alarmId)BEFOREskipNext(alarmId)ACTION_POSTPONE_NEXTbranch callscancelPreNoticeCountdown(alarmId)BEFOREpostponeNext(alarmId, snoozeMinutes)- 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) - No new
ACTION_*constant added to thecompanion 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:
AlarmScheduler.cancelAlarm(id)(L554-568 region) — callscancelPreNoticeCountdown(id)(task 2.1)AlarmScheduler.scheduleSpecno-next-trigger branch (L87-93 region) — callscancelPreNoticeCountdown(spec.id)(task 2.2)AlarmScheduler.schedulePreNoticesnooze-transition branch (L140-144 region) — callscancelPreNoticeCountdown(spec.id)(task 2.3)PluriWaveAlarmReceiverACTION_SKIP_NEXThandler (L77-92 region) — callscancelPreNoticeCountdown(alarmId)(task 3.3)PluriWaveAlarmReceiverACTION_POSTPONE_NEXThandler (L57-76 region) — callscancelPreNoticeCountdown(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 inAlarmScheduler.kt, 2 inPluriWaveAlarmReceiver.kt) = 6 total matches - Confirm both
armNextPreNoticeCountdownTickandcancelPreNoticeCountdownresolverequestCodethroughAlarmScheduler's ownrequestCode(id, slot) = 31 * id.hashCode() + slot— re-confirm this did NOT silently get called viaPluriWaveAlarmReceiver's47 * id.hashCode() + slotformula 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_MILLISconstant 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_NOTICEtick fires whenremaining <= 1; confirm the realsetAlarmClockfire 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 logcatalarm.snoozeCountdown/alarm.schedule preNoticetags) that no staleACTION_PRE_NOTICEslot-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_COUNTDOWNpath 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-riskas a single PR; no risk threshold triggered - Primary review focus: the requestCode formula (slot 9,
AlarmScheduler's31*hash+slot, NOT the receiver's47*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