Commit Graph
13 Commits
Author SHA1 Message Date
makiolaj fb81bfbf2e feat(widgets): next alarm, timer and clock home-screen widgets
Three Glance widgets, mirroring state the app already computes rather
than owning any of their own: the next alarm (AlarmEngine.upcoming()),
the live pill's subject timer (same selection logic, no ticker of its
own — a running countdown is the platform chronometer, exactly like
the ring notification), and the local time growing to world clocks at
larger sizes. WidgetSync collects the same read-only flows the shell
already reads and redraws on change, event-driven (updatePeriodMillis
= 0) — it touches no engine, only re-renders, and covers every write
path including SystemEventReceiver's since the process is already
alive whenever those run.

Tapping a widget reuses the same internal navigation actions the
AlarmClock contract's door already produces; the timer buttons send
the same unexported broadcasts its notification does. A widget is
just another caller of machinery that already exists.

ManifestRulesTest is updated deliberately: three GlanceAppWidgetReceivers
join the exported set, each pinned to exactly the platform-required
APPWIDGET_UPDATE filter and no permission — no new permission is
requested anywhere in this change.
2026-10-01 22:32:07 +02:00
makiolaj a11396b058 feat(selfcheck): why might my alarm not ring?
A pure rule table (domain/selfcheck/SelfCheck.kt) over real device
state: exact-alarm and full-screen-intent permission, notification and
alarm-channel state, battery-optimisation exemption, background
restriction, DND (read-only — Clockula never requests
ACCESS_NOTIFICATION_POLICY or touches DND itself), alarm stream
volume, a known aggressive-OEM allowlist, and the system's own next
alarm compared against what Clockula itself computed. Each row maps to
a deep link into the exact settings page responsible.

FSI denial and a missing battery exemption warn rather than fail —
both degrade (to heads-up, and to a doze-exempt setAlarmClock
registration) rather than silence the alarm. The settings hub's
'Why might my alarm not ring?' row now shows the live problem count.
2026-10-01 22:15:42 +02:00
makiolaj 616bf7f67f feat(backup): JSON export/import through SAF, whole-state replace
clockula-backup-v1.json, schema documented in docs/BACKUP.md per
PLAN.md §7: alarms, timers and world clocks as configuration only —
ring state and a timer's running anchors never leave the device, since
neither means anything on another one. Unknown fields are ignored at
any level; a malformed known field rejects the whole file rather than
importing it half-way.

Import is whole-state replacement, not a merge, and the backup screen
says so before the user confirms. Before replacing: a ringing alarm is
dismissed and every running timer is deleted, so the import can never
strand a live ring session; after it, AlarmEngine and TimerEngine are
told to re-resolve so the imported state is live immediately.

BackupDao adds delete-all/insert-many queries under one transaction —
no schema or version change, queries only.
2026-10-01 22:06:44 +02:00
makiolaj 7253445c4e feat(settings): a settings hub — appearance, alarm/timer/clock defaults, data, help
Reachable from a gear icon on every tab, composed from kit components
(GroupedRow/OptionPicker/AboutCard/LanguagePickerRow) following
Calendula's hub shape: About card first, then headed groups. Every
control writes immediately — no Save button, same as the alarm editor.

Appearance: theme, dynamic colour (API 31+), language. Alarm and timer
defaults reuse the editor's own ringtone and override pickers
(OverridePickers gets an includeAppDefault flag so the settings rows
don't offer an 'inherit' option that has nothing above it to inherit
from) and the world clock's home-zone picker. Reliability and Data
rows are placeholders for the self-check and backup screens landing
next. Help surfaces the issue tracker, a pending crash report (the
same dialog MainActivity already shows on launch), and the app
version.

Settings has no owning tab — ShellNavigation.selectedDestinationOf
deliberately leaves it unmapped, so the shell falls back to Alarms
while it's open, same as any other unrecognised route.
2026-10-01 17:36:06 +02:00
makiolajandClaude Sonnet 5 d47220947b feat(timers): redo the add-timer flow as a full-screen calculator keypad
The Timers tab gets a full rewrite of how a timer is created, after Google
Clock as the interaction reference (PLAN.md §11):

- A dedicated TimerSetupScreen replaces the bottom-sheet/inline setup panel:
  a per-unit "00h 00m 00s" readout, a "00" key, and a big centred play
  button that never shifts position when Clear fades in beside it.
- The keypad grows into the thumb zone on its own screen (92dp keys, pushed
  toward the bottom), and both it and the readout read bold.
- Presets are dropped from the flow; TimerDurationEntry gains the matching
  domain support (hoursSegment/minutesSegment/secondsSegment,
  plusDoubleZero()).
- An empty timer list shows the same readout/keypad/Start inline, in place
  of a "tap + to add" card — the keypad is still the empty state, just the
  redesigned one.
- Timer cards are disconnected like the Alarms tab's own list, each with an
  instant, unconfirmed delete via a small corner button (re-creating a
  timer is faster than reading a confirmation dialog would be).
- Hero readouts go bold app-wide to match: stopwatch, alarm rows, world
  clock, the ring screen, and both editors' length/time displays
  (ClockulaReadoutDefaults.Hero and matching call sites).

Alongside: Android 16 Live Update support for the running-timer notification
(POST_PROMOTED_NOTIFICATIONS, ProgressStyle), the live pill restyled to a
plain tonal circle, the alarm dismiss-chooser dialog moved onto Floret's
OptionCard, and small fixes to addTime's zero/negative-result handling,
repeat-day selection and alarm routes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-01 17:06:54 +02:00
makiolaj 46a049f988 feat(alarms): ask which alarm, when the request names more than one
"Dismiss my alarm" with two alarms set is a question, not an instruction. When
the search matches more than one the app asks, in a dialog listing the
candidates; when it matches exactly one it acts without asking.

The Timers tab's setup effect no longer re-keys on a list that changes with
every readout tick.
2026-09-23 11:02:20 +02:00
makiolaj 26f1effb4f feat(worldclock): the tab, the picker, and the face that answers at a glance
A hero face for home over a list of cities, each carrying its time, its offset
and how many days apart it is from you. The dial morphs between a circle and a
sun with the hour *there*: the one showpiece M0 reserved, spent on information
rather than decoration — a glance says "it is the middle of the night for them"
with no sentence needed.

The picker searches the device's own zones by word prefix. The list reorders by
drag **and** by move-up/move-down actions, because a drag-only reorder is
unreachable by a screen reader; each row reads as one sentence rather than
three texts in child order. Home is set by hand or left following the device.
At the twenty-fourth city the refusal is written on the screen, not only in the
content description.
2026-09-22 13:05:18 +02:00
makiolaj 7b727e0d40 feat(stopwatch): the tab — the readout, the laps, and the fastest and slowest
A fixed readout over a lap list, newest first, the lap in progress counting
as its own row once there is a lap to compare it against. The fastest and
the slowest are marked in `primary` and `tertiary` — never `error`, and never
by colour alone: each carries a spoken marker so the emphasis survives being
read aloud.

The state rebuilds only what actually moves: the recorded rows and their
emphasis come off the lap table once, and the tick recomputes the hero
figure and the row in progress. Laps stop at 999 and the readout ticks only
while the stopwatch is running.
2026-09-22 10:23:54 +02:00
makiolaj b67142a727 feat(timers): the timers tab, and the pill that finally has something to show
Multiple concurrent timers with labels, presets, add, pause, reset and +1 min.
The setup panel is inline on an empty tab, so the first timer costs no
navigation.

The live pill has been waiting since M4 for something that could start a
timer. It now goes through the engine rather than the repository, because a
scheduler registration and a foreground service have to move with the state —
its own contract is unchanged.

Three new architecture rules keep it that way: no screen may name the
scheduler, the service or AlarmManager, and a second MediaPlayer anywhere
outside the ring path fails the build.
2026-09-12 16:48:36 +02:00
makiolajandClaude Opus 5 97ca617e6a feat(alarms): the list and the editor
The tab the app has been missing. Rows carry the time, the repeat summary and
the next-fire countdown, ordered by time of day rather than by when they fire
next — a row must not move under the thumb reaching for it, so the next alarm
is marked by colour instead. The editor covers time, repeat days, label,
ringtone, vibration and snooze, with the five per-alarm overrides as
three-state pickers: a switch cannot say "I have not chosen".

Two things that look like details and are not. A ringing alarm is dismissed
through the engine before it is disabled, edited or deleted — otherwise its
state is cleared while the service keeps sounding and the auto-silence backstop
returns early on its own guard, leaving the alarm ringing until the wake-lock
timeout. And clearing the last repeat day disarms a pending skip, because
otherwise the resolver disables the alarm and the control that would have
explained it is already hidden.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wmy1BpCKi8KeSjaWhYuCPV
2026-09-12 14:43:46 +02:00
makiolaj 83c5e9fe3f feat(shell): the navigation host, four tabs and the live pill
`NavigationSuiteScaffold` gives the bar on a phone and the rail on a wide
layout from one declaration. Each tab keeps its own back stack; re-selecting
a tab returns to its root, and predictive back is wired only where back
actually goes somewhere.

The pill is the running-state surface from PLAN §9: it floats above the bar,
speaks for whichever subject is closest to needing attention, and pauses or
stops it from any tab. Its callbacks are gated on a live subject, so a second
tap during its exit animation cannot restart a timer the user just reset.

The notification permission is asked once, and the flag is written from the
request's result rather than before it — a process death mid-dialog leaves
the ask due again rather than recorded and never made. Three tabs are empty
shells until M5 through M8 fill them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wmy1BpCKi8KeSjaWhYuCPV
2026-09-12 13:07:18 +02:00
makiolajandClaude Opus 5 bf511b47d7 feat(alarm): the ringing service, and never degrading to silence
A systemExempted foreground service holds the ring: audio focus, USAGE_ALARM,
the volume ramp, vibration, and the ringtone URI. It goes foreground
synchronously before it reads anything from the database, because the platform
kills a service that reaches startForeground late — including on the path where
there turns out to be no session to ring.

The chain that matters is the one that never ends in silence. A full-screen
intent when canUseFullScreenIntent() allows it; a high-priority heads-up
notification that still rings when it does not; the device's default alarm
ringtone when the configured URI will not open; and forced vibration when no
audio source will play at all.

Preparing a MediaPlayer is done off the main thread — a synchronous prepare at
the moment the alarm fires is an ANR — and a player is released rather than
orphaned on every failure path, so a dismissal cannot be outlived by the ring
it cancelled.

The ring screen here is plumbing and a placeholder: window flags to show over
the lockscreen and turn the screen on. Its UI is M4.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 16:05:25 +02:00
Jean-Luc MakiolaandClaude Opus 5 5ddec0f248 feat: theme, app entry points, and the sage identity
Clockula's seed is #6B7A5C, the third step of the family's colour cycle.

Calendula's slate (#5C6B7A) and Agendula's mauve (#7A5C6B) are the same three
bytes rotated — and that byte rotation is exactly a 120° hue rotation, so the
three seeds sit at 210°, 330° and 90° with identical saturation and lightness.
The fallback schemes are therefore not eyeballed: they are Agendula's
hand-picked scheme with its hue rotated the same 120°, which keeps the three
apps structurally one palette turned rather than three colours picked. The
cycle closes at three.

The launcher icon keeps the family's Calendula bloom badge path-for-path and
swaps the task card for a clock dial, drawn as a 308° arc so the badge
overhangs into a gap rather than colliding with the stroke — the same device
Agendula uses to open its card's corner.

MainActivity is M0 scaffolding: theme plus the crash surface, with a
placeholder where the four-tab shell and the live pill land in M4. The theme
follows stored preferences from M2, once there are any.

The manifest declares no permissions yet, deliberately. Clockula's permission
set belongs to the alarm engine and is declared in M3 alongside the receivers
and the ringing service that need it, so the manifest never claims a
capability the code cannot honour.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L94fydiJC37LtxVusNQBDy
2026-09-11 11:46:54 +02:00