Skip to content

v4.52 — Android Widget Motif Parity

A user on Sprout, Mountain, or Waves now sees their own motif's glyph and their own motif's copy on the Android home-screen widget — exactly as they already did on iOS and in-app.

Summary

Before this milestone, choosing Sprout, Mountain, or Waves — three of the five streak motifs, sprout being a free-tier choice — rendered as Stones on the Android home-screen widget. Not a cosmetic mismatch: both the glyph and the copy were wrong. resolveMetaphor mapped any unrecognized value to STONES by design, and captionWidthDp silently mis-measured any caption key it didn't already know via a bare default:. Neither was a bug in isolation, but together they meant the gap could only be found by reading the source — which is what a prior widget-copy census (OBJ-3243) did, surfacing this as its own tracked issue (OBJ-3248).

The gap was narrower than it looked: the shared-storage bridge, the snapshot write path, and the Coach line templates already carried all five motifs correctly to Android — the JS side had nothing to fix. This was an Android-render-only gap, confirmed from source rather than assumed at kickoff, which ruled out phantom JS/bridge work before it could be scoped in.

Five phases, all direct to master, kickoff to final merge in under 24 hours: Phase 1 (Desi) wrote the motif design contract — glyph artwork, caption/motivator/accessibility copy, and the caption-glyph rule for all three new motifs — catching along the way that the ROADMAP's own stated Phase 2 interim would have shipped a visible regression if built as originally described. Phase 2 (Codi) shipped the five-value StreakMetaphor enum and converted every copy-selection path to an exhaustive five-way switch. Phase 3 (Codi) shipped nine new drawables and a third icon slot, proving the milestone's one genuinely novel technical risk — a stroked-form gradient under RemoteViews — on one drawable standalone before building the other eight on top of it. Phase 4 (Codi) closed the silent captionWidthDp fall-through that would have mis-measured every new caption's Large-widget header rung. Phase 5 (Codi + Dori) completed a deliberately-scoped coverage sweep, pinned the milestone's central safety claim in pixels for the first time, and corrected the docs — including reconciling a separately-queued docs ticket (OBJ-3247) whose central instruction this milestone directly falsified.

Zero routebacks, zero blocking review findings across all five phases. The one loose end at close was mechanical, not code: the milestone's own ROADMAP-tracking file was authored on a long-lived branch (docs/v4.52-roadmap, PR #2837) that never merged, and by the time this milestone closed that branch had fallen far enough behind master that merging it as-is would have reverted two later milestones' (v4.47, v4.48) already-shipped ledger entries. This close rescues that content directly onto a fresh branch instead of merging the stale one — the same recovery pattern this repo used for its v4.30 and v4.35 stranded planning artifacts.

Goal

Close the Android home-screen widget's motif parity gap: a user on Sprout, Mountain, or Waves should see their own motif's glyph and their own motif's copy on the Android widget, exactly as they already do on iOS and in-app.

Scope — What Shipped

  • enum StreakMetaphor { STONES, FLAME, SPROUT, MOUNTAIN, WAVES } in HomeWidgetProvider.java, replacing the prior two-value STONES/FLAME model — resolveMetaphor maps all five raw values, keeping STONES as the documented fail-open default for absent/unrecognized input.
  • Nine new Android vector drawables — widget_sprout/_lit/_muted, widget_mountain/_lit/_muted, widget_waves/_lit/_muted — and a third icon slot (motif_icon) added to all three widget layouts (small/medium/large), replacing the prior two-icon-view (flame_icon/stone_icon) hardcode.
  • Every copy-selection path in HomeWidgetProvider.javamotivatorStringRes, motivatorSentence, reactiveCaptionKey, reactiveAccessibilitySuffix, and the caption selection inside resolveReactiveTreatment — converted from a two-way boolean stones branch to an exhaustive five-way switch with a throwing default, so a future sixth motif can never be silently absorbed.
  • 24 new strings.xml entries (18 motivator strings, 6 caption strings) and six new CAPTION_*_WIDTH_DP measurement tables (18 measured values) for the Large-widget header layout logic.
  • captionWidthDp made exhaustive, closing a silent default: → add_stone fall-through that would have mis-measured any new caption's header rung.
  • A completed WidgetState × five-metaphor × size × light/dark Roborazzi coverage matrix, capped deliberately at the axis that actually branches (27 new PNGs) rather than the literal cartesian product, plus two new goldens proving the fail-closed contract in pixels for the first time.
  • docs/features/home-widgets.md corrected to describe the five-motif Android reality, including the one honest remaining platform gap (iOS's largeMotivatorText is still two-way) and a reconciliation of OBJ-3247's now-superseded two-metaphor Android claim.

Phases

PhaseNameStatusPlansHighlights
1Motif design contractShipped1UI-SPEC + two measurement benches + six stills (Desi); widened two cell counts on independently-verified evidence, caught a would-be regression in the ROADMAP's own stated interim
2Enum + copy layerShipped1Five-value enum, exhaustive copy switches, coverage 62→72 tests (Codi)
3Motif glyph + layoutShipped1Nine drawables, third icon slot, stroked-gradient risk proved standalone before scaling (Codi)
4Large-widget measurementShipped118 measured values, exhaustive captionWidthDp, rung-widening-only direction confirmed (Codi)
5Coverage sweep + docsShipped1Scoped coverage matrix, fail-closed pixel proof, docs correction + OBJ-3247 reconciliation (Codi + Dori)

Key Decisions

  • Milestone numbered v4.52, not v4.51 — v4.51 was already claimed by a separate live milestone issue (OBJ-3225, Design Convention Enforcement) whose ROADMAP had not yet merged and was invisible to the file-based version derivation. The issue claim was checked before committing to the number.
  • Two cell counts widened at Phase 1, both accepted — 18 glyph cells, not 12 (Android's glyph branches on a real three-value ReactiveFlame enum with BASELINE reachable); 9 drawables, not 18 (only Stones needs size-variant drawables; the new motifs follow Flame's single scalable vector). Both widen coverage; neither drops anything.
  • A ROADMAP-stated interim was corrected before it could ship — tracing the original "leaf copy with a stone glyph" Phase 2 interim against live source showed it would have shipped a blank glyph and a motif swap to Flame instead. A two-line guard (MOTIF-GLYPH-0) exists because this was caught by reading the branch, not by trusting the phase boundary.
  • The stroked-form gradient risk was proved standalone before scaling — the sprout stem and both wave crests need an aapt:attr gradient on strokeColor, not fillColor, with no precedent in this repo. One drawable was built and proved through a real views.apply() call before the other eight were authored on the same pattern.
  • The coverage sweep was capped at the axis that actually branches — the render branches on (metaphor, ReactiveFlame); WidgetState only selects the flame, and that selection is already pinned exhaustively by JVM tests from Phases 2–4. The literal "state × metaphor × size × mode" reading would have produced 180 cells on top of 413 existing goldens, nearly all duplicates; the shipped shape (45-cell light matrix, 15-cell dark spot check, 2-cell fail-closed proof) named its ruled-out axes in the PR body rather than silently omitting them.
  • OBJ-3247 was reconciled, not left queued — that issue instructed "Android is genuinely still two-metaphor … must NOT be fixed to five," which this milestone directly falsified. Run as written it would have shipped a wrong carve-out the day it merged; it was set in_review and assigned for a human done instead.
  • The milestone's own planning artifact was rescued, not merged stale — the ROADMAP-tracking branch (PR #2837) fell behind two later milestones' shipped ledger entries while it sat open; this close copies its finalized content onto a fresh branch off current master rather than merging the stale one.

Requirements Coverage

21/21 phase-checklist items satisfied (this milestone has no standalone REQUIREMENTS.md; see the Milestone Audit for the full item-by-item verification).

CategoryCountStatus
MOTIF-SPEC-* (design contract)6All satisfied
MOTIF-ENUM-/MOTIF-COPY-/MOTIF-TEST-1/MOTIF-GLYPH-0 (Phase 2)5All satisfied
MOTIF-GLYPH-1/2/3, MOTIF-TEST-2 (Phase 3)4All satisfied
MOTIF-WIDTH-1/2, MOTIF-TEST-3 (Phase 4)3All satisfied
MOTIF-COVERAGE-1, DOCS-1/2 (Phase 5)3All satisfied

Full phase-checklist detail: v4.52-android-widget-motif-parity-ROADMAP.md on GitHub.

Outcomes

A free-tier user on Sprout, or a Supporter on Mountain or Waves, now sees their own glyph, caption, motivator line, and accessibility label on the Android home-screen widget — not a stone tower rendered regardless of their actual choice. Stones and Flame render byte-identical to before the milestone. An unconfirmed streak extension (extendedToday absent) can never fabricate a motif-specific celebratory render — that fail-closed contract is now proven in pixels, not just asserted in JVM tests. The Large-widget header can no longer silently mis-measure a new caption's width. The docs describe the five-motif Android reality, with the one remaining platform gap (iOS's largeMotivatorText) named honestly rather than silently inherited.

Tech Debt

  • (Phase 5) HomeWidgetProvider.java:143's code comment still cites the archived measurement bench's "cap_*" row-naming convention; the committed bench (measure-motif-captions.mjs) keys rows by plain caption key instead. Cosmetic, non-blocking, deferred to a future touch of the file.
  • (scoped out) iOS's largeMotivatorText (HomeWidgetViews.swift:1606-1636) is still two-way — Sprout/Mountain/Waves fall through to Stones' copy on iOS for this one field. The one place in home-widgets.md where Android is now ahead of iOS, rather than behind. Extending it to five-way is unscoped iOS work.
  • (flagged, not filed as of this close) The optimistic-tap celebration-before-confirmation mismatch (g32) was pre-existing and shared across both platforms at this milestone's kickoff; a separate fix (OBJ-3256) has since corrected it on iOS only, making Android the sole remaining platform with the mismatch. Explicitly out of this milestone's scope; carried forward for re-triage on the new asymmetric basis.
  • f013815 — Phase 1: v4.52 Android widget motif design contract (PR #2838)
  • 63a26a28a — Phase 2: five-way StreakMetaphor + motif copy layer (PR #2840)
  • 280d36998 — Phase 3: motif glyph + layout, third icon slot (PR #2857)
  • c84e9cb36 — Phase 4: captionWidthDp exhaustiveness fix (PR #2858)
  • 84e0baed — Phase 5: metaphor-axis coverage matrix (PR #2861)
  • 609ed9c4c — Phase 5: home-widgets.md correction for Android motif parity (PR #2860)

Loading…