Commit Graph
86 Commits
Author SHA1 Message Date
makiolaj 71875e95e4 fix(worldclock): fit the seconds readouts without touching the default
The row's time is fitted only with seconds on, against both day periods
since an AM marker can be the wider one, and the style is built once by
the screen rather than per row. With seconds off the row is exactly as
before. The home readout stays on one line with seconds, shrinking to
fit instead of wrapping. SCREEN_ON is dropped: the tab cannot be seen
again without a resume, which already refreshes. §5 no longer claims a
clock change heals within one tick for free.
2026-10-21 19:32:44 +02:00
makiolaj 0b7af0d57b fix(alarms): let the backstop always close the ring it was armed for
Fourth round of review fixes for the silence-after setting.

onAutoSilence no longer re-arms a ring it measures as inside the window.
After a clock moved back, that check could push the backstop out again
and stretch the ring well past its window. Every writer of the window and
every clock change already re-arms the backstop under the engine lock, so
a backstop that fires has had its window, and it closes the ring as in v1.

A one-shot alarm the resolver switches off as missed also clears a
pending skip flag, as the resolver's other one-shot disable does.
SNOOZE_GRACE is its own ten-minute constant again, not tied to the
setting's default.

Refs #17
2026-10-21 19:28:09 +02:00
makiolaj 820317f9d9 fix(timers): tell a fresh install by its boot id and repair a garbled timer vibration
An empty store was a racy sign of a fresh install: the UI can write the store (the notification-permission flag) before the startup settle runs, and a fresh install would then vibrate its timers. The boot id is written only by the engines' pass, which runs after the settle in the same coroutine, and an Auto Backup restore brings the old device's.

Once settled, the startup pass costs one read and no package-manager call. A garbled stored value is replaced rather than left following the alarm default.

Refs #22
2026-10-21 19:24:52 +02:00
makiolaj 7a86e5488e fix(worldclock): refresh on resume and size the row's time once
With the tick a minute long, a language change left the city names in
the old language until the next minute; the screen now sends a refresh
on every resume. The row's time is sized once per format against a
wide sample instead of searched for every second, and ellipsizes as a
last resort rather than clipping. BackupSettings takes the setting
without a default, so a snapshot cannot drop it silently.
2026-10-21 19:22:47 +02:00
makiolaj 4cb4b6b6eb fix(alarms): keep clock changes and setting writes inside one window
Third round of review fixes for the silence-after setting.

A backstop that fires while the clock is still behind the ring's start
now closes the ring instead of re-arming in a loop until the wall clock
catches up. A clock change re-measures a ring in progress before it
reschedules: a clock moved back re-arms the backstop at most one window
from the moved clock, and one moved forward past the window closes the
ring.

The engine now owns the alarm setting's write. setAlarmSilenceAfter
stores it and re-measures the ring under one lock and without
cancellation, so a write can no longer land without the re-measure.

A snooze the resolver drops takes its "Snoozed until" notification with
it. SNOOZE_GRACE is derived from the default window, and the silence-after
picker moves to ui/settings, since it is not a per-alarm override.

Refs #17
2026-10-21 19:20:25 +02:00
makiolaj d2bf4349c5 fix(alarms): never announce an alarm that has since stopped ringing
A notice is now re-checked against the engine before a screen shows it,
so an alarm switched off or deleted by any path - the ring screen, a
notification, an intent, a restore - is never announced as set. A
retract takes the publish lock, so a publish already past its read
cannot put the notice back.

Settling is per alarm: switching one alarm off in the list no longer
cancels the repeat days still settling on another. The identical-words
dedupe is gone, since it could swallow a real confirmation.

The editor's writes that move a fire time, with their reschedule and
announcement, run on the application scope. Pick, OK, back could
otherwise cancel the reschedule half-way and leave a stored time the
system had not been given. upcomingFor() reads alarm and state under
the engine lock, so a fire cannot land between the two reads.

Refs #19
2026-10-21 19:18:19 +02:00
makiolaj adc5a3bdd6 fix(worldclock): read the whole home readout and catch up after sleep
The home readout gave TalkBack its minute-precision time as a
description, which replaced "Home", the city and the zone name; it is
the node's text now. The clock-change receiver swaps TIME_TICK, which
doubled every minute's work, for SCREEN_ON, which covers a delay
stretched by deep sleep, and resets the cached default zone on
TIMEZONE_CHANGED so the rebuild cannot read the old one. Doc and KDoc
drift fixed.
2026-10-21 19:14:08 +02:00
makiolaj fb875ca71c fix(alarms): measure every ring against one window rule
Second round of review fixes for the silence-after setting.

A fire now clears any stale ringingSince before it re-resolves the slot.
Before, the resolver's new one-shot close could switch off, or delete, the
very alarm that was about to ring.

A one-shot alarm displaced by another alarm's fire is switched off, as on
every other close of an undismissed cycle, instead of being armed for the
same time tomorrow.

onAutoSilence, onSilenceAfterChanged and the reboot resume now share one
rule. A ring still inside the window has its backstop re-armed, measured
from when it started, or from now if the clock has moved back behind that
start, so a clock change cannot stretch a ring past one window.

Refs #17
2026-10-21 19:12:18 +02:00
makiolaj fa5ce55503 fix(alarms): settle and show the alarm-set notice where it can't be lost
The repeat-day settle ran on the editor's scope, so leaving mid-settle
needed a flush that a cleared scope could still race. The announcer now
owns the settle on the application scope. Screens show the notice only
while resumed, so a cross-fade never shows it twice and a backgrounded
app does not use it up unseen. The editor shows only notices about its
own alarm, since the sentence names none.

Deleting an alarm, or the AlarmClock chooser dismissing it, takes its
notice back. Freshness is measured on the monotonic clock, one instant
is used for both ends of the countdown, and an identical countdown
keeps the standing notice instead of flickering it.

Refs #19
2026-10-21 19:10:42 +02:00
makiolaj 65c86af313 fix(worldclock): keep seconds quiet for TalkBack and apart from the defaults
The time each row and the home readout give TalkBack stays at minute
precision, and the ticking text is kept out of the tree, so a focused
row is not re-read every second. The row's time is capped in width and
shrinks to fit, so a large font scale cannot crowd out the city name.

The setting is its own SettingsPrefs flow rather than a ClockDefaults
field, so flipping it re-emits nothing alarms and timers watch. The
clock-change receiver sits outside the flatMapLatest and restarts the
tick, registering once per subscription and re-measuring the wait
against the clock as it now reads.
2026-10-21 19:07:19 +02:00
makiolaj 84d31fe7f8 fix(timers): settle timer vibration outside the store's init and resume a ramp
The DataStore migration ran its write inside the store's initialisation, so a failed write (a full disk) would have failed every preference read, the alarm path's included. The timer-vibrate key is now nullable instead: unsettled reads as the alarm default, which is what timers did before, and ClockulaApp settles it once at the head of its startup pass, off only for a never-updated package with an empty store. A failed settle leaves the fallback in place.

A timer ring restarted mid-session (handed to a second expired timer, back after an alarm held it, a recreated service) now picks its ramp up from the session's elapsed time instead of either ramping again from a whisper or jumping straight to full volume. RingAudioPlayer takes the offset; alarms pass none.

The Vibrate row's switch is display-only, so a screen reader stops on the row once.

Refs #22
2026-10-21 19:06:50 +02:00
makiolaj 1c323d5b4c fix(alarms): apply silence-after mid-ring and close missed one-shots
Review fixes for the silence-after setting.

Changing the alarm's silence-after now re-measures a ring in progress at
once, through AlarmEngine.onSilenceAfterChanged: past the new window it
closes as missed, inside it the backstop moves. onAutoSilence also re-arms
instead of closing a ring that is still inside the current window, so a
backstop armed for a shorter window cannot end a ring early.

A cycle the resolver closes as missed (a ring past its window, or a
snooze that never fired) now disables a one-shot alarm, as the engine's
own close does. A short window made that path much easier to reach, and
the alarm was being armed for the same time the next day.

A late snooze is kept for the window or ten minutes, whichever is longer,
so a short window never makes a promised snooze easier to lose than in v1.

A timer that has gone quiet stays quiet when the setting is raised: the
lapsed anchor moves back past the hard stop. The timer's heads-up is now
keyed to the session anchor rather than the window's end, so editing the
setting mid-ring no longer counts as a new session and alerts again.

A negative stored value now reads as the default instead of Never. The
picker offers a stored value that matches no choice, and the wake-lock
timeout is one shared constant.

Refs #17
2026-10-21 19:04:08 +02:00
makiolaj 207475ef37 fix(alarms): keep the alarm-set notice until a screen has shown it
The usual way to set an alarm is pick, OK, back, and back disposed the
editor's snackbar a moment after it appeared. Day taps left within the
one-second settle were never announced at all, and a rotation threw a
showing snackbar away for good.

The announcer is now an app-wide singleton holding the notice as state
until a snackbar has run to the end, so the list picks up what the
editor could not finish showing. An editor closed mid-settle announces
on the way out. Announcing is serialised, so a quick on-then-off cannot
land the "on" read last, and a notice nobody showed within ten seconds
is dropped rather than shown stale. The engine's one-alarm read now
shares upcoming()'s preview helper.

Refs #19
2026-10-21 19:00:56 +02:00
makiolaj a4cec23e5c fix(worldclock): keep the seconds option from costing reorder and catch-up
Rows that tick every second re-seeded the reorderable column mid-drag,
so it is handed ids now. A zone or clock change no longer waits for the
next minute: TIME_SET, TIMEZONE_CHANGED and TIME_TICK are merged into
the tick through a new ClockChanges seam. With seconds on, the row's
time drops a size so the city name keeps its room. The truncation unit
and the tick come from one place, and stale 200 ms comments are gone.
2026-10-21 18:57:10 +02:00
makiolaj 8910ab7fbf fix(timers): keep a ramp from restarting mid-ring and a migration from guessing
A timer ring that hands over from one expired timer to another now starts at full volume instead of ramping again from near silence.

The timer-vibrate migration only treats an install as fresh when the package was never updated and the store is empty. An updated install whose store was replaced after corruption, or an Auto Backup restore onto a new device, keeps the alarm default as before.

The Vibrate row now says whether it is on, and only when it is off adds that a silent timer still vibrates.

Refs #22
2026-10-21 18:56:07 +02:00
makiolaj 412e54db09 feat(settings): let alarms and timers choose when they silence themselves
A ringing alarm and a finished timer used to stop after a fixed ten
minutes. Settings > Alarms and Settings > Timers now each have a "Silence
after" row with Google Clock's choices: 1, 5, 10, 15, 20 or 25 minutes,
or Never. Ten minutes stays the default, so nothing changes on upgrade.
The value is app-wide; a per-alarm override would need a new column, a
migration and an editor row, and is left out.

"Never" rings for an hour, then stops. The backstop is also what keeps a
phone left at home from ringing its battery flat, what bounds the ring
services' wake locks, and what stops a reboot hours later from resuming a
ring nobody was there for. A truly unbounded ring would give all of that
up, so Never maps to a one-hour hard stop, longer than any finite choice,
and the settings hint says so. Stored values clamp to 0..60 on read, on
write and from a backup.

The window is read everywhere the fixed constant was: the resolver's
ringing and late-snooze checks (which now take it without a default), the
backstop onFire arms, how late a snooze fire may arrive, whether an
interrupted ring is resumed after a reboot, upcoming(), the timer ring
policy, the lapsed-session check and the timer ring screen. Both ring
services' wake-lock timeouts now derive from the hard stop instead of a
hard-coded fifteen minutes.

When the resolver closes a ring (the window was shortened mid-ring, the
clock jumped, the alarm was switched off), the engine now also stops the
ring service and cancels the backstop. Before, it only cleared
ringingSince and left the sound to a backstop armed for the old window.

Both values are backed up as default_alarm_silence_after_minutes and
default_timer_silence_after_minutes; a file without them imports as ten
minutes.

Fixes #17
2026-10-21 18:52:25 +02:00
makiolaj 58c9bfebb1 feat(alarms): say how long until an alarm rings when you set it
After a user action in the app sets an alarm, a snackbar says how long
until it rings: "Alarm set for 7 hours and 12 minutes from now". The row
already said "in 7h 12m", but nothing drew the eye to it, so an AM/PM or
wrong-day slip went unnoticed until the morning.

It is raised by switching an alarm on in the list, and in the editor by
picking a time (or keeping a new alarm's default one), arming or
disarming the skip, and changing the repeat days. Day taps wait a second
to settle, so Mon-Fri is one snackbar rather than five; a newer
countdown always replaces the one on screen. Switching the announced
alarm back off takes it back. A switched-off, ringing or snoozed alarm
says nothing, and the AlarmClock intents never reach it, since those
already get the status-bar icon.

AlarmSetCountdown decides what it says, purely, with the row's rounding;
the engine gains upcomingFor(), a read-only resolve of one alarm.

Fixes #19
2026-10-21 18:51:49 +02:00
makiolaj fbc3a192bf feat(timers): give timers their own vibration and gradual volume
Timers vibrated by the alarm vibration default and never ramped. Both are
now the timers' own app-wide settings, next to the default timer sound in
Settings > Timers: a Vibrate switch and the same Gradual volume picker
alarms use. Timer.resolveSettings reads them instead of the alarm default
and the old TimerRing.RAMP_SECONDS constant.

A fresh install starts with timer vibration off, as Google Clock does, and
no ramp. An existing install keeps what its timers did: a one-shot DataStore
migration writes the new key from the alarm vibration default before the
first read, so nobody's kitchen timer silently stops buzzing on update. A
fresh install is told apart by an empty store (anything that has run once
has written its boot id); the migration writes the default there too, so it
never runs twice.

Backups carry both new fields. A file without them restores the alarm
vibration default as the timers' (what that install's timers did) and a
flat ring.

Silent plus vibration off still signals: Silent gives the player no
sources, and its onSilenced callback starts the vibrator whatever the
setting says. The settings row says so.

Fixes #22
2026-10-21 18:49:45 +02:00
makiolaj 785adf715b feat(worldclock): add an option to display time with seconds
One switch in Settings > Clock adds seconds to the home readout and
every city row, in the same 12/24-hour and locale form as before, and
gives the analog face its second hand. Off by default.

The tab now ticks on the wall-clock boundary through a new
Ticker.alignedTicks: once a minute with seconds off, once a second only
with them on, replacing the free-running 200 ms tick. The setting is in
backups as world_clock_show_seconds; older files import with it off.

Fixes #23
2026-10-21 18:48:15 +02:00
makiolaj e870918a03 docs: plain-language pass over every doc
Removes AI-writing patterns across the docs, store listing and
templates: dash-heavy rhythm, not-X-but-Y contrasts, one-line kickers
and decorative bold. Facts, code, links and headings are unchanged.

Also fixes leftovers copied from the sibling apps: the crash report
template promised "no calendar content", and CONTRIBUTING described
Agendula's task-provider layout, test seams and commit style.
2026-10-06 19:14:11 +02:00
makiolaj 19ee678a1b feat(widgets): let each widget pick its theme and wallpaper colours
Every widget instance can now follow the system or be pinned to Light or
Dark, and on Android 12+ turn the wallpaper's colours off in favour of
the app's own palette. Both are set in the widget's config screen and
stored in its own Glance state; an instance without them keeps today's
look.

Glance content takes them through WidgetTheme. The plain-RemoteViews
parts (the analog and digital faces, readouts and chronometers) go
through a new WidgetPalette, which resolves the widget_* palette under a
forced day/night and sets concrete colours, or day/night pairs, on the
views. The app's own tones are now widget_app_*, so they stay reachable
on API 31+. The analog and digital clocks are re-coloured on API 31+
only: below that AnalogClock's hands and a shape's background tint can't
be set through RemoteViews, so their config screens don't offer it.

The next-alarm and timer widgets gain a config screen for this
(configuration_optional, so placing one doesn't stop there). The analog,
stopwatch and quick-timer config screens become settings screens with an
OK, like the digital clock's, since they now hold more than one choice.

Background opacity is left out: most faces are a shape with text in its
container colour rather than a card, so there is no background to fade
without losing contrast against the wallpaper.

Closes #6
2026-10-05 19:48:59 +02:00
makiolaj 462f9f8c9f feat(widgets): open Alarms from the clock widgets, or a tab of your choice
Tapping the digital or analog clock widget now opens Alarms instead of
World clock, and both config screens gain an "On tap" setting (Alarms,
Timers, Stopwatch, World clock) stored per instance next to the style.
Instances placed before the setting existed open Alarms. The analog
config becomes a settings screen like the digital one, with the style
behind a row, to make room for it.

A widget tap sometimes showed whatever the app was last on instead of
its tab, for two reasons. The shell replayed a deep link as a tab tap
with restoreState, so it restored the tab's saved back stack, nested
screen and all (fixed by the pager shell, which pops to the tab). And
the widget intents carried only NEW_TASK, so with a picker or settings
page left on top of MainActivity the task was merely brought forward and
onNewIntent never ran. Every widget that opens the app now builds its
intent in one place with CLEAR_TOP and SINGLE_TOP as well.

Closes #8
2026-10-05 19:42:14 +02:00
makiolaj be526711d7 feat(shell): swipe between tabs
The four tabs are now pages of one HorizontalPager on a single NavHost
route, so a sideways swipe moves between Alarms, Timers, Stopwatch and
World clock as well as tapping the bar. The pager is the source of truth
for the selected tab; editors, the timer setup page and Settings stay
NavHost destinations pushed over it. A tab tap from one of them pops
back to the tabs, and a deep link always lands on its tab rather than on
whatever nested screen was last open there.

Swiping is on only under a navigation bar: beside a rail the tabs are
stacked vertically, and a horizontal swipe has no counterpart there.

Closes #2
2026-10-05 19:42:14 +02:00
makiolaj 45e5a51c98 feat(worldclock): show home time as text under the analog face
The World clock tab now prints the home time under the hero face in the
app's 12/24-hour format, reversing the earlier call to leave it out.
Reading the exact minute off a dial is slow, and that minute is what
most visits to the tab are for.

The face no longer has a content description, so TalkBack does not read
the time twice. The readout, "Home", the city and the zone name are one
merged node instead.

Closes #4
2026-10-05 19:36:31 +02:00
makiolaj cac5b2dfca refactor(settings): remove the unused default timer duration
Settings > Timers offered a "Default duration", but nothing that creates
a timer ever read it: the keypad starts empty. The row, its preference,
ClockDefaults.timerDuration and the backup field are gone, and the
screen's hint and the hub's subtitle now only speak of the sound.

Backups no longer write default_timer_duration_millis. Older files that
carry it still import, since the codec ignores unknown keys.

Closes #5
2026-10-05 19:28:31 +02:00
makiolaj f37c5f2f84 feat(ringtones): let a music app play the alarm
"Choose from a music app…" in the sound picker browses any app that shares
its library through Android's MediaBrowserService and stores the choice as a
clockula://media URI in the existing ringtone field — no schema change, no
new permission, no internet. The music app streams with its own.

At ring time the app is asked to play through its media session. Every wait
is bounded and any failure falls back to the ordinary alarm sound: the app
won't connect or start, stalls or buffers too long, or the media stream
might not be heard (headphones or a classic Bluetooth device connected, Do
Not Disturb muting media). The ramp drives the media stream and the user's
media volume is restored when the last media ring ends.

RingAudioPlayer.start no longer suspends: vibration starts up front, the
timer collector is not held up, and a generation token keeps late work from
reviving a stopped ring.
2026-10-02 20:44:58 +02:00
makiolaj 5d03c654bf docs: mark M10 done
ARCHITECTURE.md's package table gets the new domain/backup,
domain/selfcheck, domain/widget, data/backup, backup/, widget/,
ui/settings/ and ui/widget/ entries, and §9/§10 are brought up to date
with what M10 actually built (the exported-component count, the
permission list unchanged, which 'not built yet' rows are now done).
CHANGELOG and ROADMAP's current-state line follow.
2026-10-01 22:34:07 +02:00
makiolaj 9ab3927efe docs: the system boundary, and what it refuses
`ARCHITECTURE.md` gains §17: the door, the split between the Android-free
validator and the reader that guards the unparcel, the range rule (clamp what
is happening now, drop what would define a future ring), the candidate table
for "which alarm did you mean", and the three deliberate narrowings — including
why a link we cannot read dismisses nothing rather than everything.

§4 records schema v3 and its migration; §13 records the reading of `PLAN.md` §6
this milestone builds, since a malformed intent lands the user in an editor
over a row created from the valid extras, there being no "new alarm" sentinel.
2026-09-23 11:02:20 +02:00
makiolaj 36f6b356d0 docs: the world clock, its seam and its caches
`ARCHITECTURE.md` gains §16 for the tab — where the zone list comes from, why
ICU sits behind a seam, what `ZoneDirectory` caches and on what key, and why
every comparison is read at the instant rather than against a stored offset.
The module, package and rule tables count the new arrivals.
2026-09-22 13:05:24 +02:00
makiolaj cb35cc9c95 docs: the stopwatch, and why its reading is floored and banked
`ARCHITECTURE.md` gains §15 for the stopwatch and records the one correctness
find this slice turned up: a lap taken inside a segment a reboot later
discarded would have dragged the readout, the lap totals and the shade
backwards. The reading is floored at the last lap's total — and the floor is
banked, not merely displayed, or the shade's free-running chronometer walks
away from a tab that is standing still.

The package, module, receiver, service and permission tables all count one
more; §10 loses the two rows M7 closed.
2026-09-22 10:24:08 +02:00
makiolaj 319d176ae8 docs: the timers, and the counts checked against the gate
ARCHITECTURE gains §14 for timers and the ring package's new shape. The test
counts were written before the review's fixes landed; they now match what the
gate actually runs.
2026-09-12 16:49:05 +02:00
makiolajandClaude Opus 5 176626b33a docs: the alarms screen, and the numbers put right
ARCHITECTURE gains §13 for the alarms screen. The test counts in §8 were
written before the review's fixes landed, and M4's instrumentation count was
one short; both now match what the gate actually runs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wmy1BpCKi8KeSjaWhYuCPV
2026-09-12 14:44:23 +02:00
makiolaj d4120d8e06 docs: the app shell, and what it earned
ARCHITECTURE gains §12 for the shell and loses the two rows M4 paid off; the
permissions row now says what M4 actually asks for and leaves the rest to
M10's self-check.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wmy1BpCKi8KeSjaWhYuCPV
2026-09-12 13:07:53 +02:00
makiolajandClaude Opus 5 a522d68098 docs: the alarm engine, and the blind spot it closes
ARCHITECTURE gains a section 11 on the engine itself — the state machine, the
two AlarmManager slots, the DST table, and the chain that keeps a denied
permission from turning into a silent morning. Section 5's known blind spot is
amended rather than deleted: the boot id exists now, and the stopwatch repair
moved from M7 to here.

Section 9's "no permissions are declared yet, deliberately" is finally untrue,
so it is replaced with the real set and why each one is there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 16:05:31 +02:00
makiolajandClaude Opus 5 971ee4f7a3 docs: point the reboot repair at the milestone that actually owns it
The roadmap puts the BOOT_COMPLETED and TIME_SET receivers, the ringing
service and the full-screen intent in M3; ARCHITECTURE.md credited them
to M6 and M7, which are Timers and Stopwatch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 13:53:11 +02:00
makiolajandClaude Opus 5 1e6443c907 docs: the first ARCHITECTURE.md
How Clockula is built today: the layers and the data seam, the package
layout, the four tables column by column, dependency injection, and the
JVM-first testing posture.

The section on the two clocks is the one a future contributor will need
most — which aggregate resolves against which clock and why, the timer's
three anchors, and the reboot window where a dead anchor can still read
as live, stated as it actually behaves rather than as it was first
assumed to. Closing that window needs a persisted boot id; M3's
BOOT_COMPLETED receiver is where it belongs.

Also says plainly what is not built yet, and points each at its milestone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 13:52:33 +02:00