halkyon/docs/adr/0037-train-movement-reminders.md
Tizian 「ティツィアーン」 e4585b478d
feat(train): reminders to move and workouts suggested from the steps (ADR 0037)
Settings' Moving more, on Android: a reminder to get up after an idle
time, and a suggested walk or run, each off until turned on (then every
30 minutes, weekdays 9:00 to 18:00, not 12:00 to 13:00, each value the
person's), turning on asking for the activity and notification
permissions.

A WorkManager job every 15 minutes reads the phone's step counter and
keeps two hours of readings. It reminds where fewer than 50 steps came
over the idle time, all of it within the hours and outside the break,
on the person's days, never while Train records or the phone is in a
car (Android's car mode, or a car's Bluetooth audio or hands-free). It
suggests a walk or a run where the last span averaged a walking or
running cadence, once an hour; tapped, the Record sheet for it opens.
The decisions are pure Kotlin (Movement.kt), tested on the JVM; the
notifications' words are the app's, written into the job's
configuration as the person's app opens.

Polar's SDK sets neither the band's inactivity alert nor tells of one
without an all-day connection, hence the phone's steps (ADR 0037).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-02 14:56:12 +01:00

4.6 KiB
Raw Blame History

ADR 0037: Train's reminders to move, workouts suggested, and the Loop's own detection

  • Status: decided 2 Nov 2026, by the developer (D1–D3, each the agent's recommendation), the rest proposed by the agent and flagged where it chose.
  • Builds on: ADR 0030 (Train: D19 no background location, D21 the recorder, D37 to D39 the Loop's days and the sessions it detects).

Context

The developer asked for two things around the Polar Loop: to be reminded to get up when idle for some 30 minutes (not while driving, not at lunch), and for Train to start or at least suggest a workout from movement, such as a walk when one is on a stroll.

Polar's SDK (8.3.0) was read for both. The band's own inactivity alert is an on/off flag in its general settings, which the SDK's public API doesn't set (PolarUserDeviceSettings lacks it; only a low-level write of the settings file would); the band tells a phone of an alert as a device-to-host notification (INACTIVITY_ALERT), over a live connection alone, which would mean a foreground service holding the band all day. The SDK does set the band's automatic training detection (setAutomaticTrainingDetectionSettings: on or off, a sensitivity, a minimum duration in seconds) and reads it back (getUserDeviceSettings); the sensitivity's range is documented nowhere (Polar Flow offers low, medium and high).

D19 rules out background location, and Train uses no Play services, so Android's activity recognition (a Play service) is out too.

Decision

  1. The reminder counts the phone's steps (D1, the recommendation taken). A WorkManager job every 15 minutes (its shortest period; MovementWorker.kt), while reminders or suggestions are on, reads Android's step counter (TYPE_STEP_COUNTER, kept by the phone's own low-power hardware, under the activity permission the recorder asks for already) and keeps the last two hours of readings in movement_state.json (the app's files, never synced, never backed up, D53). It reminds where fewer than 50 steps were counted over the idle time the person chose (30 minutes by default), all of it within their hours and outside their break, on their days, at most once each idle time, never while Train records, and never while the phone is in a car: Android's car mode (Android Auto) or a car's Bluetooth audio or hands-free connected (CarCheck; nothing else tells a car without location). A reminder can come up to 15 minutes late. Alternatives: the band's own alert (a foreground service connected all day, its setting written below the SDK's API, no car detection), or both.

  2. A workout is suggested from the steps, and the band detects its own (D2, the recommendation taken). The same job: where the last reading's span (10 to 25 minutes) averaged a walking cadence (80 steps a minute or more) or a running one (140 or more) while nothing records, a notification asks "Walking?" or "Running?", at most once an hour; tapped, it opens the Record sheet for that activity (the minutes before aren't recorded: the band's detection catches those). The Loop's own automatic training detection is switched and tuned on the band's screen (on or off, and the shortest session it keeps: 10, 15, 20 or 30 minutes), so a session nobody recorded still comes in afterwards as D39's. Its sensitivity stays as the band has it (agent's choice): the SDK documents no range, and a guessed value could set the band wrong. Alternatives: the phone's suggestion alone, or the band's detection alone.

  3. Off until turned on; then every 30 minutes, weekdays 9:00 to 18:00, not 12:00 to 13:00 (D3, the recommendation taken). Settings' Moving more screen holds both switches (the suggestion its own, also off) and every value: the idle time (15 to 120 minutes), the hours, the break, the days. Turning either on asks for the activity and notification permissions; refused, it stays off. The settings are the person's, on this device alone (movement.json in their folder, never synced), and hold for the person whose app is open: the job follows whoever was last open.

  4. The job's words come from the app (agent's choice): the app writes the notifications' titles and texts in the person's language into the job's configuration whenever it configures it, since the job runs while the app is closed and Android's resources hold none of Train's texts.

Consequences

  • No server change and no migration.
  • A phone without a step counter (rare; most since Android 4.4) offers neither: the screen says so.
  • The logic deciding a reminder or a suggestion is pure Kotlin (Movement.kt), tested on the JVM as the recorder's.