Commit Graph

2 Commits

Author SHA1 Message Date
null 99dbc36802 feat: reminders that stay quiet on a lock screen
§28, §29, §30 and §31. NotificationCopy is a pure function — privacy mode plus
kind plus day count in, two versions of the text out — so every combination is
tested exhaustively without an emulator. This is the one surface whose mistakes
are visible to somebody who is not the user, so the tests are exhaustive rather
than representative: every kind × every mode asserts that no health word reaches
a lock screen outside Direct, and that includes the ACTION LABELS, which §31
points out are visible text too. A perfectly discreet body under a button
reading "Started my period" leaks anyway.

TWO ANDROID BEHAVIOURS THAT LEAK IF YOU TRUST THE DOCS

A private notification with no public version does not blank the lock screen —
it shows the private text. NotificationText therefore has no nullable title and
an instrumented test asserts every kind attaches one.

And a notification channel is IMMUTABLE after creation: importance and
lock-screen visibility cannot be changed. One shared channel would have kept
whatever the user's first privacy mode set, forever — switching from Direct to
Maximum privacy would have appeared to work and changed nothing. There is now
one channel per mode. Found by an instrumented test on a device; nothing in the
unit tests could have seen it.

§30's stopping rule is a test of its own: the app asks a bounded number of times,
says "We'll stop checking for now. Log your period whenever it begins.", and
then says nothing more — while the engine keeps learning, which is the sentence
§30 puts right after it.

WorkManager, and no exact alarms. §31 rules them out and the new checkPermissions
task fails the build if one ever appears in the merged manifest — from here or
from a dependency. That guard also failed its own first proof, reading a stale
manifest because it did not depend on the task that writes one.

ReminderCoordinator reschedules whenever the forecast moves, which §31 asks for
and is the requirement most likely to be missed: a "Not yet" moves the forecast,
so work queued against the old one is aimed at a day that no longer means
anything.

188 unit tests and 6 instrumented, all passing. ./gradlew check green.

closes #24
closes #25
closes #26
closes #27
2026-08-18 15:26:59 -05:00
null 2479a1ddf4 feat: onboarding, and the Surface that dark mode was missing
Seven screens, §19 and §56 verbatim: welcome, last period, period end, previous
history, the privacy promise, notification privacy, first forecast. Verified end
to end on a device — the flow produces a forecast, the record persists, and a
relaunch goes straight to Today.

Three decisions with tests behind them:

  - Nothing is written until the final step. Somebody who abandons onboarding
    halfway has not asked this app to remember anything about them.
  - Notification privacy is Discreet before the user touches anything (§28), and
    Direct is last and never pre-selected. Checked on the device, not only in a
    unit test.
  - "Still going" and "I'm not sure" both mean no end date. §24: never invent
    one. The date picker refuses future dates by not offering them rather than
    by rejecting a tap it allowed.

DARK MODE WAS BROKEN FOR ALL OF BATCH 01

PeriodTheme never wrapped its content in a Surface, so every Text without an
explicit colour inherited Material's default — black — and the app background
never painted. In light mode that looked correct by accident, because dark text
on cream is what was wanted anyway. In dark mode the onboarding headings
rendered near-black on charcoal.

No test caught it and no test easily would have. It was found by opening the
app on a device and looking at it.

The Surface now lives in the theme, so a screen without a Scaffold cannot
forget, and every illustration has a light/dark preview pair. A preview is not
a test, but it is the cheapest thing that puts the failure in front of whoever
is editing the screen.

closes #16
2026-08-18 03:44:46 -05:00