The check-in counter resets on every process start, so the app may never stop asking #70

Closed
opened 2026-08-20 21:24:13 -05:00 by null · 0 comments
Owner

What is true now. ReminderCoordinator.checkInResets (app/src/main/kotlin/dev/privacyllc/period/notifications/ReminderCoordinator.kt:116-120) is confirmedStarts.distinctUntilChanged().onEach { if (latest != null) onPeriodConfirmed(latest) }, and onPeriodConfirmed calls preferences.resetCheckIns() (:126-128). distinctUntilChanged only dedups within ONE collection: a new process starts a new flow, so the first emission always passes. PeriodApplication.onCreate:42 starts the coordinator in every process — including the one WorkManager spawns to run ReminderWorker, which then reads preferences.checkInCount.first() (core/notifications/.../ReminderWorker.kt:91).

So with any confirmed history present, the count is reset at effectively every wake-up, MAX_CHECK_INS = 3 (ReminderRules.kt:59) may never be reached, and the StopAsking branch (ReminderRules.kt:113) may never fire.

What it costs. PRODUCT_PLAN §30 (docs/planning/PRODUCT_PLAN.md:1348) says do not nag forever. A tracker that keeps asking 'Did your period start?' every day is exactly the behaviour §30 exists to prevent, and it is the kind a user uninstalls over. No test catches it: ReminderCoordinatorTest.kt:86 asserts only that distinctUntilChanged collapses repeats inside one flow.

What to do. Stop resetting; scope the count instead. Store the count alongside the row id of the period it was counted against, and read it as zero when the ids differ. The worker is the only reader, so no coordinator chain is needed at all — delete checkInResets/onPeriodConfirmed and the handler's reset call.

Traps. Store an id, never a date — the same reasoning that keeps dates out of PeriodRecord.toString(). Delete My Data leaves an id matching nothing, which correctly reads as zero. Count a check-in only when the notification was actually posted (see the permission issue) or the app can stop asking having never asked.

Verify: check-ins are counted against the period they were asked about in UserPreferencesRepositoryTest; starting the coordinator twice on the same history leaves the check-in tally alone in ReminderCoordinatorTest; the fourth unanswered check-in is the stop-asking notice and the fifth is silence in the new ReminderWorkerTest.

**What is true now.** `ReminderCoordinator.checkInResets` (`app/src/main/kotlin/dev/privacyllc/period/notifications/ReminderCoordinator.kt:116-120`) is `confirmedStarts.distinctUntilChanged().onEach { if (latest != null) onPeriodConfirmed(latest) }`, and `onPeriodConfirmed` calls `preferences.resetCheckIns()` (`:126-128`). `distinctUntilChanged` only dedups within ONE collection: a new process starts a new flow, so the first emission always passes. `PeriodApplication.onCreate:42` starts the coordinator in every process — including the one WorkManager spawns to run `ReminderWorker`, which then reads `preferences.checkInCount.first()` (`core/notifications/.../ReminderWorker.kt:91`). So with any confirmed history present, the count is reset at effectively every wake-up, `MAX_CHECK_INS = 3` (`ReminderRules.kt:59`) may never be reached, and the `StopAsking` branch (`ReminderRules.kt:113`) may never fire. **What it costs.** PRODUCT_PLAN §30 (`docs/planning/PRODUCT_PLAN.md:1348`) says do not nag forever. A tracker that keeps asking 'Did your period start?' every day is exactly the behaviour §30 exists to prevent, and it is the kind a user uninstalls over. No test catches it: `ReminderCoordinatorTest.kt:86` asserts only that `distinctUntilChanged` collapses repeats inside one flow. **What to do.** Stop resetting; scope the count instead. Store the count alongside the row id of the period it was counted against, and read it as zero when the ids differ. The worker is the only reader, so no coordinator chain is needed at all — delete `checkInResets`/`onPeriodConfirmed` and the handler's reset call. **Traps.** Store an id, never a date — the same reasoning that keeps dates out of `PeriodRecord.toString()`. Delete My Data leaves an id matching nothing, which correctly reads as zero. Count a check-in only when the notification was actually posted (see the permission issue) or the app can stop asking having never asked. Verify: `check-ins are counted against the period they were asked about` in `UserPreferencesRepositoryTest`; `starting the coordinator twice on the same history leaves the check-in tally alone` in `ReminderCoordinatorTest`; `the fourth unanswered check-in is the stop-asking notice and the fifth is silence` in the new `ReminderWorkerTest`.
null added this to the Batch 12 — Reminders that tell the truth milestone 2026-08-20 21:24:13 -05:00
null added the
P1
label 2026-08-20 21:24:13 -05:00
null closed this issue 2026-08-20 21:59:21 -05:00
Sign in to join this conversation.
No Label
P0
P1
P2
release-blocker
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: null/Privacy-Period-Tracker#70
No description provided.