Skip to content

Clock themes implementation plan

Companion to the clock themes design. Each phase ends with a working APK. Every phase must pass ./gradlew testDebugUnitTest lintDebug assembleDebug and have a physical XT1058 check before the next phase starts.

Safety and display policy come before any effect or long animation, so that no intermediate APK runs unbounded effects overnight.

Package root: com.masojus.motoxdashboard.

Status

Phase State
1. Time, theme resolution, and host Done
2. Burn-in and brightness Done; brightness levels still need dark-room tuning
3. Nixie rendering, static Done; colors and levels still need dark-room tuning
4. Nixie electrical effects Done; the feel of the effects still needs a watch in a dark room
5. Split-flap rendering and flip Done; card tones and text size still need a daytime look
6. Optional sound Done; loudness still needs a bedside listen after the speaker rework
7. Soak Not started

Phase 1–2 device checks on the XT1058 covered the following:

  • the scheduled Nixie theme at night;
  • the overlay status line, Switch clock, and Resume schedule;
  • the override persisting across an app restart;
  • 24-hour mode applied on resume;
  • the burn-in offset and quiet-hours dimming.

A live time-zone change was not exercised on the device, because changing the zone needs system privileges. It is covered by the resolver unit tests and by the ClockTicker broadcast handling.

Phase 3 device checks on the XT1058 covered the following:

  • all ten digits, reviewed from screenshots using a temporary, uncommitted preview hook;
  • the blank leading hour cell in 12-hour mode;
  • about 2.9 MB of baked masks for 260×433 px cells;
  • about 1.1 MB of GPU texture cache and no offscreen layers;
  • frame time on a minute change, with the switch animating, at no more than 13.6 ms. Only the first frames after a bake, which upload textures, exceed 16.7 ms.

Phase 4 device checks on the XT1058 covered the following:

  • Frame history. SurfaceFlinger showed effect bursts of 0.5 to 1.8 s at irregular gaps of about 47 to 93 s, consistent with the quiet-hours rate. The minute digit switches were bursts of about 150 ms, exactly 60 s apart. Nothing redrew between events.
  • Frame time during effects. The first build had 29 of 128 frames over 16.7 ms, because it was limited by fill rate. After compositing the static layers and cropping the digit masks, 1 of 128 frames was over budget (18.9 ms), with an 8.9 ms average.
  • Effects toggle. It switched the label and persisted the setting.
  • Glow blending. Normal alpha compositing was kept; additive blending was not needed.

Phase 5 device checks on the XT1058 covered the following:

  • flip direction and opacity, from frames of a temporary, uncommitted 24-second flip build;
  • flips at 11.6–13.9 ms per frame, with one frame per flip over 16.7 ms (17.5 and 26.4 ms);
  • the theme crossfade at 95 frames, 3 over budget (up to 22.2 ms);
  • the "a. m." label and the Spanish overlay, from a temporary, uncommitted locale-forcing build;
  • Switch clock and Resume schedule into and out of the split-flap theme.

Phase 6 device checks on the XT1058 covered the following:

  • the primary audio output became active at the minute flip in the split-flap theme (from dumpsys media.audio_flinger);
  • Nixie effect sounds played at irregular gaps of roughly 40–50 s, matching the effect schedule;
  • clip files were removed from the cache after loading;
  • sounds were returned to off after testing.

Not yet checked on the device: loudness at bedside distance, audio-to-visual sync by ear, the Bluetooth mute (no speaker was connected), and the ringer mute, which is covered by unit tests.

Speaker rework. On the device the flap sounds were audible but the Nixie effects were not, because their energy was below the speaker's range. The buzz and brownout were rebuilt from harmonics and a midrange clunk. The stutter had never played, because comparing a dip time against a Long.MIN_VALUE sentinel overflowed and skipped every dip, and the tests had only compared array lengths. Regression tests now check spectral balance, burst count, and seed variation. The split-flap flip was also slowed to a 400 ms fall and 100 ms bounce after the first device viewing.

Phase 1: Time, theme resolution, and host

Build the foundation that both themes need, and make it visible with a temporary face.

  • clock/ClockReading: immutable digits, blank leading hour, and PM flag, built from LocalDateTime and the 12/24h setting.
  • clock/ClockTicker: minute- or second-aligned Handler ticks with an injected java.time.Clock, using wall-to-uptime conversion. It owns the time-set and time-zone receivers through idempotent start() and stop().
  • theme/ClockTheme, ThemeSchedule, ThemeOverride ({theme, setAt}), ThemeResolver (pure; instant-based, half-open), ThemeScheduleSource with FakeThemeScheduleSource (Nixie 20:00–08:00), and ThemeOverrideStore backed by SharedPreferences.
  • display/MotionPolicy: a user effects toggle plus the system animator duration scale. Every later animation depends on it.
  • ClockFace interface. Two temporary faces reuse the current TextView, in amber for Nixie and off-white for split-flap, so switching can be seen.
  • A tap overlay with Switch clock, Resume schedule, and the status line, auto-hidden. It uses English and values-es strings.
  • Remove DashboardGreeting, which becomes unused.
  • Tests:
  • ClockReadingTest;
  • ThemeResolverTest:
    • half-open boundaries and wrapping;
    • merge and expiry, and visibleChangeAt versus reevaluateAt;
    • an active override across a time-zone change;
    • DST gap and overlap with now inside them;
    • collapsed windows;
    • America/Bogota;
  • tests of the ticker delay math.
  • Device check: the override survives an app restart, and changing the time zone or the 12/24h setting updates the clock immediately.

Phase 2: Burn-in and brightness

  • display/BurnInShifter: a bounded, slow, non-repeating offset path plus composition drift anchors.
  • display/DisplayPolicy: per-theme window brightness and content level, applied on theme change. It also defines quiet hours, 00:00–06:00 by default, which later reduce the effect rate.
  • Apply both to the temporary faces.
  • Tests: shifter bounds, step size, and coverage over 24 simulated hours; the policy mapping and quiet-hour boundaries.
  • Device check: initial brightness levels in a dark room, and confirm that shifting is not noticeable.

Phase 3: Nixie rendering, static

  • clock/nixie/NixieGlyphs: pure-Java command lists for ten digits, built on GlyphPath with cubic arc approximation, plus a small adapter to Path. NixieLayout places the separator dots, and the view draws them as circles.
  • clock/nixie/NixieAssets: bakes the flat-cell layers as ALPHA_8 masks where possible on a background executor. The layers are frame, cathodes, mesh, glow, and outer and inner core. It follows the generation and size publication rules and replaces sets atomically. It drops references instead of calling recycle().
  • clock/nixie/NixieClockView: draws four cells and dimmed, steady separator dots. It supports the level multiplier and offset. On a digit change it plays the asymmetric switch (first built as a 30 ms ignition and a 150 ms decay, later lengthened to a 150 ms rise with a 5% surge and a 400 ms fade). The switch is instant when motion is disabled.
  • Replace the temporary Nixie face and retune its night brightness.
  • Tests: glyph command bounds; the layout math that places cells within the shift and drift margins; and digit-switch timing (ignition, overshoot, decay, and ending).
  • Device check:
  • screenshots at night level and full level, compared with the flat-cell concept;
  • dumpsys meminfo during and after baking, against the budgets;
  • no dropped frames on a minute change.

Phase 4: Nixie electrical effects

  • clock/nixie/NixieEffect envelopes for buzz, brownout, stutter, and hum. Each maps a seed and a duration to deterministic brightness and glow samples. No envelope oscillates above 4 Hz.
  • clock/nixie/NixieEffectScheduler:
  • gaps and aftershocks, which never overlap;
  • the shuffle bag and skewed intensity;
  • global or single-cell targets;
  • the quiet-hours rate.
  • display/LuminanceGuard: filters every effect sample. It holds a floor of 0.25 and allows at most four opposing changes (two flashes) per rolling second.
  • The view postpones any effect that would overlap a digit switch.
  • View integration: frames run only while an effect is active, and effects respect MotionPolicy.
  • Overlay: an Effects on/off toggle, persisted.
  • Tests:
  • scheduler statistics with a seeded Random;
  • the envelope floor, ending, and ripple limit;
  • adversarial LuminanceGuard sequences.
  • Device check:
  • watch for 10 minutes to confirm the effects feel varied and rare;
  • dumpsys gfxinfo during effects;
  • check the additive-blend appearance.

Phase 5: Split-flap rendering and flip

  • clock/flap/FlipTimeline: phase, angle, easing, settle bounce, and the rule that missed changes jump without queuing.
  • clock/flap/SplitFlapClockView: hour and minute cards, split line, hinge, card edges, and a localized AM/PM label. The flip uses Camera and Matrix, and changes are instant when motion is disabled.
  • Replace the temporary split-flap face. Theme changes crossfade as a single monotonic luminance change, kept apart from effects without passing through LuminanceGuard. BackgroundBaker (in the clock package) is shared with the Nixie face, owning the background-thread bake lifecycle for both.
  • Tests: FlipTimeline timing and no queuing; AM/PM selection.
  • Device check: flip smoothness from gfxinfo, legibility from across the room, and screenshots in en and es-CO. See device checks list above.

Phase 6: Optional sound

  • audio/SoundSynth (pure Java): buzz, brownout, and stutter clips at several intensities, plus flap tick-and-clack variants, as 16-bit mono PCM with a small WAV writer.
  • audio/SoundPolicy (pure):
  • default off, with an app volume and a maximum cap;
  • mute while Bluetooth audio is connected or the music player is active;
  • mute for silent or vibrate ringer, quiet hours, and motion disabled.
  • audio/ClockSounds:
  • generates clips off the main thread into the private cache directory;
  • loads them into SoundPool with sonification AudioAttributes and no audio focus;
  • randomizes variant, rate, and volume per play;
  • is created in onResume and released in onPause.
  • Nixie effect sounds are synthesized per planned effect from its envelope rather than chosen from variants. Hook into the Nixie effect envelopes and the FlipTimeline landing time, compensating for output latency estimated from audio_flinger buffer sizes.
  • Overlay: a Sounds on/off toggle, persisted.
  • Tests: SoundSynth duration, peak, and no clipping, and determinism per seed; every SoundPolicy mute rule.
  • Device check:
  • listen at bedside distance at the default level;
  • measure audio-to-visual sync;
  • confirm nothing plays through a connected Bluetooth speaker.

Phase 7: Soak

  • Run an overnight soak on the charger. Record temperature from dumpsys battery every 15 minutes, check memory stability, and confirm that no ticks are missed after pause and resume or screen off and on.
  • Check rotation and configuration handling, and app restart on each theme.
  • Update the README.md status and architecture. Record an ADR for the theme-schedule manifest field when the backend work starts.

Deferred

  • Backend schedule field and manifest-backed ThemeScheduleSource.
  • Seconds cells, per-digit split-flap boards, and a pinned override.
  • An airport-board cascade flourish with clatter at the top of the hour.
  • Partial cathode glow during deep brownouts.
  • Ambient-light brightness and scheduled screen-off.
  • An original app icon and logo. The app currently uses Android's default icon and appears in the launcher as "Moto X Dashboard".
  • An optional home-screen (appliance) mode, so Home and reboots return to the clock. It is a product decision because it changes the phone's behavior.
  • Runtime compensation for asymmetric navigation-bar content insets. On the Moto X Play with LineageOS 17.1 / Android 10, a measured 144 px right inset centers the clock about 72 px left of the physical display center. Read the actual left and right WindowInsets and apply half their difference; do not hard-code the Android version or the measured pixel value. See Android compatibility.