Skip to content

Clock themes design

  • Status: Implemented; dark-room tuning and overnight soak remain
  • Date: 2026-09-27
  • Scope: Android clock faces, theme selection, and clock-level burn-in and brightness behavior. No running API is required.

Goals

  1. The phone always renders time from its own clock, time zone, locale, and 12/24-hour setting. The server never sends a time to display.
  2. Provide two original, procedurally drawn clock themes:
  3. Nixie: warm amber tubes for the night, with occasional, varied "old laboratory" electrical imperfections;
  4. Split-flap: matte black flip cards with off-white numerals for the day.
  5. Select the theme from a server-provided schedule, with a Windows Night Light-style local override. Until the backend exists, a fake source says Nixie from 20:00 to 08:00 and split-flap otherwise.
  6. Protect the AMOLED panel and the sleeper: low luminance, subtle movement, no strobing, and very little continuous work on a 2013 GPU.
  7. Optionally, play soft, unobtrusive sounds that match the visuals: tube buzz and crackle, and the mechanical clack of a falling flap. Sound is off by default.

Non-goals for this phase

  • Real server integration, device authentication, or HTTPS trust work.
  • Dashboard slides, weather, calendar, or music UI.
  • Scheduled screen-off, ambient-light-sensor brightness, and settings screens.
  • Bundled fonts or bitmap artwork. Everything is drawn in code so that it is original, resolution-independent, and tunable. Sounds are synthesized in code for the same reason.

Comparison with the Gemini sketch

Topic Gemini sketch This design Reason
Glow Pre-baked glow in PNG/WebP assets Glow baked at runtime into offscreen bitmaps, once per size Same runtime cost as shipped assets, but original, tunable color and radius, and exact pixel size for the screen. No artwork pipeline or licensing question.
Views Two ImageViews per tube with hardware layers One custom View per theme drawing all tubes on one Canvas One invalidate per frame, shared bitmaps across tubes, and global effects (power-supply sag) that span tubes. Fewer layers and less memory.
BlurMaskFilter Avoid Use only while baking into a software Canvas; never in onDraw Blur is fine offscreen once; per-frame blur is the expensive part.
Support library android.support.v4 ContextCompat Platform Context.getDrawable The Support Library is end-of-life and superseded by AndroidX. ContextCompat.getDrawable exists to backfill Context.getDrawable below API 21, and minSdk is 22, so it adds a dependency for no benefit.
Time API java.util.Calendar, avoid java.time java.time through the configured desugaring, with an injected Clock Desugared java.time is supported on API 22 and is easier to test. One small allocation per tick is negligible.
Tick loop postDelayed(this, 1000) Align each tick to the next second or minute boundary A fixed 1000 ms delay drifts and can skip or double-display a second.
Effects Not covered A seeded, testable effect scheduler with bounded envelopes Required by the product and must be safe near a sleeping person.

Agreed with the sketch: do not use per-frame blur, keep digit images cached rather than inflated, and use a Handler on the main looper for ticks.

Architecture

MainActivity (lifecycle, window flags, overlay)
 ├─ ClockTicker ─────────────► ClockReading (pure: digits, 12/24h, PM)
 ├─ ThemeController ─────────► ThemeResolver (pure) ◄─ ThemeScheduleSource (fake now)
 │                              ▲                       ThemeOverrideStore (SharedPreferences)
 ├─ DisplayPolicy ───────────► BurnInShifter (pure), brightness levels
 └─ active ClockFace (View)
     ├─ NixieClockView ── NixieAssets (baked bitmaps) + NixieEffectScheduler (pure)
     └─ SplitFlapClockView ── FlipTimeline (pure)

Pure classes use only Java and java.time and have local JVM unit tests. Android classes stay thin: they draw, schedule callbacks, and persist state.

A small ClockFace interface is justified because there are two real implementations:

interface ClockFace {
    void showTime(ClockReading reading);   // called on each tick
    void setOffset(int dxPx, int dyPx);    // burn-in shift
    void setLevel(float contentLevel);     // 0..1 content dimming
    void setMotionEnabled(boolean enabled); // effects/flips on or off
}

Time and ticking

  • ClockReading is an immutable value built from LocalDateTime and the 12/24-hour flag. It contains hour and minute digits, whether the leading hour digit is blank, and a PM flag.
  • Clock faces always render Western digits 0 to 9. That is intrinsic to the physical objects being imitated. The accessible description uses the locale's spoken time format.
  • Ticks run once a minute when seconds are hidden, or once a second otherwise. Each tick is aligned to the next wall-clock boundary by converting it into the uptime domain: postAtTime(r, uptimeNow + (targetWallMillis - wallNowMillis)). The target is recalculated after every callback. Epoch milliseconds are never passed to postAtTime.
  • ClockTicker owns the ACTION_TIME_CHANGED and ACTION_TIMEZONE_CHANGED receivers. Its idempotent start() and stop() are called only from onResume and onPause. Those broadcasts, and a resume recheck of DateFormat.is24HourFormat, trigger an immediate tick.
  • Hours use no leading zero in 12-hour mode. The unused cell or card stays unlit or blank, as on real hardware. 24-hour mode shows the leading zero.

Nixie theme

Chosen direction

Four concept renders were compared: individual glass tubes, slim tubes under a shared glass canopy, exposed wire cathodes without glass, and flat cathode cells. They are references only and are not in the repository. They are not used as assets because licensing for AI-generated images is unclear, and the design draws everything in code.

The target is flat cathode cells. Of the four, it has:

  • the largest, most legible digits;
  • the least static, lit structure;
  • only strokes and masks, which bake once and draw cheaply on the Adreno 320.

The glass concepts put static rims, tips, pins, highlights, and a copper base on the same pixels all night. Convincing glass is also hard without shaders. From the exposed-wire concept, the design borrows the partially lit digit during a deep brownout, listed below as a later refinement.

The renders are showroom-bright, and at night the whole face runs at a small fraction of their luminance. The structure keeps the reference's balance relative to the lit wire: its framebuffer value is about 25–35% of the wire's.

The first device build made the structure about 5–10% as bright. At night window brightness the Moto X's AMOLED crushed those values to black, which hid the character the design depends on. At the quiet-hours content level, the structure now measures about 50–65/255 against about 190–200/255 for the wire.

Visual construction

Each cell is composed back to front:

  1. Cell interior: a very dark warm fill inside the frame.
  2. Cell frame: a thin copper rounded rectangle with no glass. It has:
  3. narrow, tall mounting clips and side tabs;
  4. round mounting rings at the top and bottom center, each joined to the frame by a short stem.

It is static, so it stays well below the wire's brightness, and burn-in shifting and composition drift spread its wear. 3. Unlit cathode stack: all ten digits, drawn as copper wire strokes, with a thin center support wire between the rings. Real tubes show this stack behind the lit digit. It adds depth and spreads pixel wear across the digit area. 4. Lit digit glow: a wide, soft, red-orange halo baked with a blur at half resolution and scaled up with bilinear filtering. 5. Lit digit core in two masks: - a narrow amber stroke; - a thinner, pale-yellow inner line for the hot center. 6. Anode mesh: a fine honeycomb over the digit, confined to the cell rather than spread across the whole background. It is drawn as dark lines over the glow, slightly tinted by it.

Digits are hand-authored centerline strokes in a unit box, in the style of IN-12 and IN-14 numerals, stroked with round caps. They are not font glyphs, so their look does not depend on the platform's fonts.

The glyphs are stored as pure-Java command lists of line, arc, and cubic segments with numeric points. They are adapted to android.graphics.Path only in the rendering code. Local unit tests can then check bounds and stroke geometry without Robolectric, because framework Path methods are not available in JVM tests.

The default layout is four cells (HH MM) because it is larger, calmer, and changes less at night. A six-cell layout with seconds is an option.

Separator dots

The two neon dots between hours and minutes are the only permanently lit pixels, so they are the highest burn-in risk.

  • They are dimmer than the digits.
  • They move with the pixel shift and composition drift.
  • They stay steady rather than blinking each second, to avoid motion at night.
  • They take part in global effects such as a brownout. They are not singled out as the only lit element in a single-cell effect.

Baking and memory

  • NixieAssets bakes the cell frame, cathode stack, mesh, and ten sets of glow, outer core, and inner core masks on a background executor. It bakes when the view size changes. The view shows only black until the assets are ready, which should take a few hundred milliseconds.
  • All cells share the same bitmaps.
  • The static layers (frame, back mesh, and unlit cathodes) are pre-tinted into one ARGB_8888 background per cell size, so each cell costs a single full-cell draw.
  • The per-digit layers (glow, outer and inner core, and the front mesh) are ALPHA_8 masks, cropped to the padded digit area and tinted at draw time.
  • Phase 4 measurements showed that fill rate, not draw calls, limited effect frames on the Adreno 320. This layout cut the frames over budget during effects from 29 of 128 to 1.
  • Budgets, to be measured with dumpsys meminfo during and after a bake:
  • Steady state is under 3 MB of bitmaps. For 260×433 px cells, the background plus 21 cropped masks and ten half-resolution glows take about 2.6 MB.
  • During a re-bake, the peak roughly doubles, because the old set stays in use until the new one is installed.
  • Graphics memory for GPU texture copies is counted separately.
  • Brightness and effects are applied only at draw time with Paint alpha and a ColorFilter. They never require a re-bake.
  • Stale bakes: every bake carries a generation number and target size. A result is installed only if its generation is still current, the size still matches, and the view is still attached. Rejected results are dropped. BackgroundBaker (in the clock package) owns the background-thread bake lifecycle for both faces: generation checks, delivery on the main thread only if still current, and stop on detach.
  • Replacement: the old assets stay in use until the new set is installed atomically.
  • No recycle(): on API 22, bitmap pixels live on the Java heap, so dropping references lets GC reclaim them. Calling recycle() could free pixels that a RenderThread display list still references.
  • Teardown: on detach, the generation is advanced, the bake executor is shut down, queued publications are removed, and the assets are dropped.

Digit change

Real Nixie tubes are cold-cathode neon lamps with no heated filament: the glow moves to the new cathode in microseconds. Many real Nixie clocks nevertheless fade from one digit to the next, whether by multiplexing ghosts or deliberately in firmware, and that overlap is what people recognize as the Nixie look. The switch therefore crossfades asymmetrically, like a hobby-clock fade:

  • The new digit rises over about 150 ms to a slight surge (5% above normal), then settles to normal by about 260 ms.
  • The old digit fades exponentially (time constant 100 ms) and is gone by 400 ms.

The first device build used a 30 ms ignition and a 150 ms fade. It read as a clean switch, so the overlap was lengthened to be visible.

The overlap also produces the ghost of the old cathode seen in the concept renders. It is a single monotonic transition, not a repeating flash. Effects are postponed around it rather than blended with it. It is the only animation on an ordinary minute change. With motion disabled, the digit switches instantly.

Electrical imperfections

The goal is a movie-lab lamp: rare, varied, and never a strobe.

Scheduling (NixieEffectScheduler, pure, seeded Random):

  • Gaps between events are random between 12 and 50 seconds, which averages about two events per minute.
  • With a 15% chance, an "aftershock" follows 0.5 to 2 seconds later.
  • The effect type comes from a shuffle bag. Each type appears once per bag, and a type never repeats back to back across bags.
  • Intensity is skewed toward subtle (min + (max - min) * r²), so strong events are uncommon.
  • About 60% of events are global (the power supply affects every cell). The rest affect one random cell, as a loose socket would.

Effect types. Each is a deterministic envelope that returns a brightness multiplier m(t) and a glow multiplier g(t):

Effect Shape Duration Amount
Buzz Brightness surges smoothly with a slow, irregular wobble of no more than 4 Hz and under 4% ripple. The glow blooms more than the core. 250–700 ms +8% to +20%
Brownout A quick sag, an uneven hold with slight wobble, then recovery with a 3–5% overshoot. The glow shrinks. 250–800 ms Down to 35–75% of normal, never below 25%
Stutter Two uneven dips, like a poor connection 200–700 ms Dips to 45–80%
Hum A barely visible slow swell and fade 1–2 s ±3% to ±6%

Later refinement: during deep brownouts, parts of the digit briefly lose their glow first, as happens at low current in real tubes.

Photosensitivity safety. The effects are designed to stay well below the WCAG 2.3.1 general and red flash thresholds. Compliance is not claimed until it has been measured. The clock is large, saturated red-orange, and viewed in darkness, so the margins are deliberately wide.

  • One luminance authority: every effect sample passes through a single LuminanceGuard per face before it is drawn. Other brightness changes are kept apart from effects rather than blended into the guard:
  • a digit switch is a single transition;
  • an effect that would overlap a digit switch, whether one in progress or the next minute boundary, is postponed until the switch has finished.
  • Rolling flash count: the guard counts flashes in every rolling one-second window. As in WCAG, a flash is a pair of opposing luminance changes of at least 10%. At most two flashes are allowed per window; anything beyond that is clamped or skipped. WCAG's own threshold is three.
  • No overlap: effects never overlap each other or a digit switch. Aftershocks start only after the previous envelope ends. Theme crossfades are monotonic and kept apart from effects. Hourly composition moves are currently instant jumps at :17. They are not yet coordinated with effects.
  • Limits: there is no oscillating modulation above 4 Hz, and brightness never drops below 25% of normal.
  • Disabling: effects are off when motion is disabled, either by the user toggle or when the system animator duration scale is 0. Quiet hours reduce their rate.
  • Tests exercise the guard with adversarial sequences of effects, aftershocks, digit switches, and crossfades, not just single envelopes.

Frame loop. When no effect or digit switch is active, the view draws only on ticks. During an event, onDraw calls postInvalidateOnAnimation() until the envelope ends. The GPU therefore works only a few seconds per minute.

Split-flap theme

Visual construction

  • Two cards, one for hours and one for minutes.
  • The matte card body (stacked edges, top and bottom half gradients, split gap, hinge notches, axle caps) is baked once per card size into one ARGB_8888 bitmap on a background thread by SplitFlapAssetBaker. Numerals are drawn live on top with drawText, clipped to each half, and the split gap is redrawn across the numerals.
  • Cards are 1.10:1 (width:height). Numerals are 74% of card height in sans-serif-medium, off-white #D6D4CC.
  • In 12-hour mode the hour card shows the locale's AM/PM string from DateFormatSymbols in its lower-left corner (for example "AM" or "a. m.").
  • Dimming scales colors (a LightingColorFilter on the body and scaled text colors) instead of alpha, so every layer stays opaque; alpha dimming made the flap translucent, so the card behind showed through it, which was caught on the device.

Flip animation

  • FlipTimeline gives a 400 ms gravity ease-in fall from 0 to 180 degrees and then a 100 ms, 7-degree settle bounce (500 ms in total). The first device version used 240 ms and 70 ms, which looked too fast. The flap darkens by up to 55% as it turns edge-on.
  • The flap is one rigid plate rotated with Camera about the hinge: its front shows the old upper half until 90 degrees and its back shows the new lower half landing. The camera sits at -24 (Camera units, 72 px per inch) for mild perspective.
  • FlipCard keeps at most one flip per card, always from the latest shown value to the newest; a change during a flip abandons the old flip instead of queuing.
  • If readings are more than 90 s apart (measured with SystemClock.elapsedRealtime, which includes sleep), the card jumps instead of flipping, so a paused app never animates stale values.
  • With motion disabled, changes are instant.

Theme selection

Schedule model

ThemeSchedule
  defaultTheme: SPLIT_FLAP
  windows: [ { theme: NIXIE, start: 20:00, end: 08:00 } ]   // local wall time
  • Start and end are local wall-clock times in the device's time zone. They are schedule boundaries, not displayed time, so wall-clock is the right model.
  • Windows are half-open, [start, end), and may wrap past midnight. At exactly 20:00 the schedule says Nixie; at exactly 08:00 it says split-flap.
  • DST-safe evaluation: the resolver never compares LocalTime values.
  • It builds the window's dated occurrences for the previous, current, and next local dates.
  • It resolves each boundary to an Instant with ZonedDateTime: a boundary in a DST gap moves forward, and one in an overlap uses the earlier offset.
  • It then tests now against those half-open instant intervals.
  • An occurrence that collapses or inverts after resolution (start at or after end) is skipped for that day.
  • Unknown theme identifiers from a future server are ignored.
  • ThemeScheduleSource is an interface because it is a real, variable boundary. FakeThemeScheduleSource returns the 20:00–08:00 Nixie window now. Later a manifest-backed source caches the last valid schedule, falls back to the built-in default, and supplies it through an additive manifest field (for example presentation.clockThemes). That contract change will get its own ADR.

Night Light-style override

Like Windows Night Light, a manual switch lasts until the schedule catches up with it.

  • Switch now selects the other theme. The override ends at the first scheduled boundary after it was set where the schedule itself enters the overridden theme. With a two-theme schedule, that is simply the next boundary.
  • Example: switching to Nixie at 16:00 merges into the 20:00 Nixie window. The display then changes at 08:00, so the status reads "Night clock until 8:00 AM".
  • Resume schedule clears the override immediately.
  • Stored form: the override is persisted as {theme, setAt}, with setAt a UTC instant, in SharedPreferences, so it survives restarts. Its end is recomputed from setAt in the current time zone on every resolve. After a time-zone change, the override therefore follows the new local boundaries.
  • A "hold until I change it" override is a possible later option.

ThemeResolver.resolve(now, zone, schedule, override) returns:

  • theme: the effective theme now;
  • overrideActive;
  • visibleChangeAt: the next instant the displayed theme will change, used for the status text;
  • reevaluateAt: the next instant anything changes, whether a raw schedule boundary or an override merge.

The theme is re-resolved on every minute tick, on resume, and on time and time-zone broadcasts. Boundaries are whole minutes and ticks are aligned to minute boundaries, so a separate long-delay callback is unnecessary and could not outlast a paused Activity anyway.

A merged override is cleared from storage only when resolution at the real time reports it inactive. The displayed theme follows the time the clock shows, including a Preview tools time preview, but a preview must never delete the user's choice. Showcase's +1 h once crossed an override's end and cleared it, so after the run the schedule replaced the user's manually chosen face.

MainActivity crossfades the two face views over 1.5 s with hardware-layer alpha animation and ramps the window brightness between the themes' levels over the same 1.5 s. Both faces' motion is off during the crossfade and the incoming face's motion resumes when it ends, so effects and flips never overlap a crossfade. It does not pass through LuminanceGuard: a single slow, monotonic fade is one luminance change, far below any flash limit.

Interaction

The screen stays immersive and control-free.

  • A single tap anywhere reveals a dim, temporary overlay that hides itself after about 6 seconds. The tap itself does nothing else.
  • The overlay's large buttons are single-line with ellipsis, in two rows (theme controls, then effects and sounds):
  • Switch clock, with a status line such as "Night clock until 8:00 AM";
  • Resume schedule, shown only while an override is active;
  • Effects on/off;
  • Sounds on/off.
  • The Spanish effects labels are "Efectos: sí" / "Efectos: no" because "Efectos: activados" wrapped and was clipped on the device.
  • The overlay is also the documented escape path from immersive mode.
  • Buttons have content descriptions and at least 64 dp targets. The text uses English and Colombian Spanish string resources.
  • A long press on the status line toggles Preview tools (see Preview tools).

Sound (optional)

Sound is decorative. It never carries information, it is off by default, and it must never startle a sleeper.

Hardware limits

The XT1058's built-in speaker reproduces little below about 400 Hz. Sound energy under that is effectively lost, however loud the clip is.

The first implementation put nearly all of the buzz and brownout energy at 50–360 Hz (a 120 Hz hum with 240 and 360 Hz overtones, and an 85→50 Hz thunk). On the device they were practically inaudible, while the flap clicks, which are broadband, were clearly audible.

The fix relies on the "missing fundamental" effect: a harmonic series of 120 Hz is still heard as a 120 Hz hum even when the speaker cannot reproduce 120 Hz itself. The clips therefore keep their pitch while moving their energy into the range from 400 Hz to 3 kHz.

Playback is also scaled by the phone's system-sound volume (STREAM_SYSTEM, 0–7 on the speaker), on top of the app volume.

Output latency is estimated at 60 ms from the audio_flinger configuration (two 1024-frame mixer buffers at 44.1 kHz plus wake-up from standby). It has not been measured acoustically and should be tuned by ear.

Android 5.1 screenrecord captures no audio, so sound can only be judged by ear or by filming the phone.

Character

Event Sound Notes
Buzz A 120 Hz mains buzz built from 25 harmonics, swelling and fading with the visual envelope The lowest three harmonics are kept faint (0.25) and partials 4–25 fall off as 1/√k, so most energy sits between 400 Hz and 3 kHz. Band-limited noise adds grit. 120 Hz is twice the 60 Hz mains frequency used in the United States and Colombia.
Brownout The same harmonic buzz, sagging in pitch (to 75%) and swelling with the depth of the sag, plus a midrange clunk at the start The clunk is a short noise burst with a damped 380 Hz tone; it replaced a sub-bass thunk the speaker could not play. A DC-blocking filter keeps the clip zero-mean.
Stutter One dry crackle per dip, like a marginal contact Each crackle is an 18–30 ms decaying noise burst with 5–9 clicks, placed at the envelope's dips. Crackles are broadband, so they carry well on the speaker.
Hum Silent Kept silent so the room is not filled with a constant drone.
Flap A crisp plastic tick as the flap releases, then a slightly heavier clack as it lands, with a faint flutter Evokes a desk flip clock (the Groundhog Day clock radio) or a single Solari-board card. Movie audio is never copied.
  • When the hour changes, both cards flip. Their clacks are offset by 20–40 ms, so they sound mechanical rather than doubled.
  • An optional airport-board flourish at the top of the hour is deferred. It would briefly cascade the cards through intermediate values with a rapid clatter.

Generation and playback

  • SoundSynth (pure Java) synthesizes 16-bit mono PCM at 22,050 Hz. Each clip is normalized so its peak is 0.6 of full scale times (0.4 + 0.6 × intensity); flap clips always peak at 0.6.
  • Tests assert that at least 70% of the buzz and brownout energy, and 80% of the stutter energy, lies at or above 400 Hz, and that no partial aliases above 5 kHz.
  • Nixie effect sounds are synthesized per event, not picked from a variant pool: when the view plans an effect (usually 12–50 s ahead), ClockSounds synthesizes a clip driven by that effect's own brightness envelope on a background thread, so level, length, and timing match the visual exactly. Stutter crackles fall on the envelope's dips (brightness below 0.97). The hum effect stays silent.
  • Flap sounds are four pre-synthesized variants, loaded when sounds are enabled: a release tick at the flip start and a landing clack at FlipTimeline.FALL_MILLIS (400 ms). Each play picks a random variant with ±4% rate and ±10% volume. When both cards flip together, the second plays 20–40 ms later.
  • Clips are written as WAV files to the private cache directory clock-sounds, loaded into one SoundPool (USAGE_ASSISTANCE_SONIFICATION, no audio focus), and each file is deleted as soon as its load completes. The directory is also cleared on start and stop.
  • Sync: visuals start ClockSounds.AUDIO_LEAD_MILLIS (60 ms) after their sound is triggered, applied only while sounds are enabled.
  • Why synthesize: generated sounds are original, license-free, tiny, and tunable. If a synthesized flap sounds unconvincing, a self-recorded or CC0 sample can replace it, with the source and license recorded in the repository.

Audio policy

  • Clips play with USAGE_ASSISTANCE_SONIFICATION. They do not request audio focus, so they never pause or duck other audio.
  • Mute reasons are checked in this order: disabled, motion disabled, ringer set to vibrate or silent, Bluetooth A2DP audio connected (AudioManager.isBluetoothA2dpOn, which needs no permission), any app playing music (AudioManager.isMusicActive, standing in for the future music player), and quiet hours unless the user opted in. Sounds are off by default.
  • SoundPreferences stores volume 0.25 and a cap of 0.5; only the on/off switch has UI today. The default volume may be raised (for example to 0.4 with a 0.7 cap) after listening at bedside distance with the speaker-friendly clips. Sonification playback is also scaled by the phone's system volume.
  • SoundPool is created in onResume and released in onPause. No sound plays after a pause, and nothing queues.

Burn-in and brightness (shared)

  • Pixel shift: BurnInShifter maps elapsed time to an offset along a slow, non-repeating path within ±14 px. It moves 1–2 px every 2–3 minutes, so there is no visible animation.
  • Composition drift: every 45–90 minutes, the clock moves between a few anchor positions. The Nixie theme fades to its new position, and the split-flap theme moves on its next flip.
  • Window brightness: WindowManager.LayoutParams.screenBrightness is set per theme. Nixie starts near the panel minimum (about 0.02 to 0.05), and split-flap at a moderate level (about 0.3).
  • This deliberately overrides the Android brightness slider and auto brightness while the app is in the foreground, so the night clock is predictably dark wherever the slider was left.
  • The system setting applies again when the user leaves the app.
  • Following room light would need the app to read the light sensor itself, which is low-priority later work.
  • Content level: a paint-level multiplier goes darker than the hardware minimum at night.
  • These values are initial guesses to be tuned on the physical device. Ambient-light brightness and scheduled screen-off are later work.
  • The window keeps FLAG_KEEP_SCREEN_ON and the current immersive flags.

Lifecycle and threading

  • onResume calls ClockTicker.start() (ticks and time receivers), then starts the theme callback and effect scheduling. onPause reverses these: it stops effects, removes the theme callback, and calls ClockTicker.stop().
  • The bake executor is owned by the view host and shut down in onDestroy. Results are published under the generation rules in Baking and memory.
  • No disk or bitmap work runs on the main thread except drawing. SharedPreferences writes use apply().

Testing strategy

  • Local unit tests (JUnit 4):
  • ClockReading: 12/24h, midnight, noon, and blanking.
  • ThemeResolver:
    • exact half-open boundaries and wrapping;
    • override merge and expiry, and visibleChangeAt versus reevaluateAt;
    • an active override across a time-zone change;
    • DST cases where both the boundary and now fall inside a gap or the second occurrence of an overlap;
    • collapsed windows;
    • both a DST zone and America/Bogota.
  • NixieGlyphs: command-list bounds.
  • NixieEffectScheduler: gap bounds, no repeats, and intensity bounds with a seeded Random.
  • Envelopes: the brightness floor, that each envelope ends, and the ripple frequency limit.
  • LuminanceGuard: the rolling flash limit under adversarial combinations of effects, aftershocks, digit switches, and crossfades.
  • BurnInShifter: bounds and coverage.
  • FlipTimeline: timing and the no-queue rule.
  • SoundSynth: clip durations, peak levels with no clipping, and deterministic output per seed.
  • SoundPolicy: every mute condition and the volume cap.
  • Device checks on the XT1058:
  • adb.exe shell screencap for visual review of each theme and state;
  • dumpsys gfxinfo during effects and flips;
  • dumpsys meminfo during and after baking;
  • dumpsys battery temperature during an overnight soak.
  • Lint must stay clean with API-22 checks. Instrumented tests are added only if a regression cannot be caught by unit tests and device checks.

Risks

  • Additive glow blending was not adopted. Normal alpha compositing over near-black looks right on the Adreno 320 and avoids blend-mode differences.
  • Effects can annoy at night. Defaults are conservative, and quiet hours and the effects toggle provide a way out.
  • Panel minimum brightness may still be too bright in a dark room. Content dimming covers the gap.
  • Split-flap static white is the worst burn-in case. It is mitigated by off-white numerals, pixel shift, composition drift, and daytime-only use by default.
  • Sonification routing on API 22 may duplicate to both the speaker and a connected Bluetooth device. The automatic mute while Bluetooth audio is connected avoids depending on routing details. Routing must still be verified on the device.
  • Synthesized flap realism may fall short. The fallback is a licensed or self-recorded sample.

Open questions (defaults chosen)

  1. Should the Nixie theme show seconds? Default: no (four cells). Six cells is an option.
  2. Should split-flap use hour and minute cards or per-digit cards? Default: hour and minute cards.
  3. Should effects continue in the deep night (for example 00:00–06:00)? Default: yes, at half the rate. They can be disabled.
  4. Is "switch until next boundary" enough, or is a pinned override needed? Default: next boundary only.
  5. Should sounds play during quiet hours when enabled? Default: no, unless the user separately opts in.