Commit Graph
133 Commits
Author SHA1 Message Date
makiolaj badd7aaa54 fix: turn the screen on when an alarm or timer goes off
SystemUI launches a full-screen intent only when a notification is first
posted, never on an update. The alarm service went foreground with a
placeholder that had none and added it in the update, so a locked phone
could ring behind a black screen. The placeholder now carries it.

Timers had no full-screen screen at all. A finished timer now opens
TimerRingActivity over the lock screen with Stop and +1 min, posted
under a second notification id so the expiry is a fresh post. It only
does so while the timer is sounding, so it never covers a ringing
alarm's screen or wakes the display for an already auto-silenced timer.

Fixes #15
2026-10-21 14:16:47 +02:00
makiolaj 2f39b00398 feat: link translators to Weblate from the README and settings
Beta — Codeberg pre-release / detect (push) Successful in 4s
Beta — Codeberg pre-release / beta (push) Skipped
2026-10-10 14:30:03 +02:00
makiolaj 27df918da0 ci: move release and beta jobs to the light and gradle runners 2026-10-10 14:30:03 +02:00
makiolaj 11ea9be468 fix: harden backup restore and alarm playback
Beta — Codeberg pre-release / detect (push) Successful in 5s
Beta — Codeberg pre-release / beta (push) Skipped
2026-10-08 16:48:44 +02:00
makiolaj 61eff32f5f chore: 1.0.0-beta.2
Beta — Codeberg pre-release / detect (push) Successful in 26s
Beta — Codeberg pre-release / beta (push) Successful in 13m35s
Brings in main's move to floret-kit 0.7.0 and the fix for the crash it
caused on every tab (#10).
v1.0.0-beta.2
2026-10-08 09:25:43 +02:00
makiolaj 276c88942e Merge branch 'main' into release/v1.0.0
# Conflicts:
#	README.md
#	gradle/libs.versions.toml
2026-10-08 09:24:01 +02:00
makiolaj e49afb182c fix: compile against the material3 that ships, ending the tab crash (#10)
floret-kit 0.7.0 pulls in material3 1.5.0-alpha29 on the runtime
classpath while the app still compiled against alpha21. alpha29 removed
the composable-content ButtonGroup overload, so the stopwatch and timer
controls threw NoSuchMethodError as soon as a tab opened.

Align the Compose BOM, material3, core-ktx and appcompat with
floret-kit 0.7.0, and make each variant's compile classpath resolve
consistently with its runtime classpath, so a future skew fails the
build instead of a user's app. The two ButtonGroups never used
animateWidth, so they become a Row with the group's spacing; the
removed ToggleButtonDefaults.shapes() was the default anyway.
2026-10-08 09:23:46 +02:00
Jean-Luc Makiola c0e0d6269d Merge pull request 'Move to floret-kit 0.7.0' (#9) from chore/floret-kit-0.7.0 into main
Release — F-Droid repo + Gitea/Codeberg release + Play / detect (push) Successful in 6s
Release — F-Droid repo + Gitea/Codeberg release + Play / release (push) Successful in 19m22s
Release — F-Droid repo + Gitea/Codeberg release + Play / play (push) Successful in 7s
Reviewed-on: https://codeberg.org/jlmakiola/clockula/pulls/9
v0.1.0
2026-10-07 10:17:01 +02:00
makiolaj 4290892cb2 chore: move to floret-kit 0.7.0 and its toolchain
floret-kit main now needs Gradle >= 9.6, so bump to Gradle 9.8.0,
AGP 9.4.1, Kotlin 2.4.20, and Hilt 2.60.1 / KSP 2.3.11 to read
Kotlin 2.4 metadata.
2026-10-07 09:54:01 +02:00
makiolaj 24098e8845 docs(readme): restructure along Calendula and Agendula's READMEs 2026-10-06 22:53:28 +02:00
makiolaj f5052f3fa9 chore: 1.0.0-beta.1
Beta — Codeberg pre-release / detect (push) Successful in 5s
Beta — Codeberg pre-release / beta (push) Successful in 11m15s
The first public build. CHANGELOG's Unreleased section is rewritten as
user-facing notes, since it becomes the beta's release notes. The README
gains an install section, and the store short description fits Play's
80 characters.
v1.0.0-beta.1
2026-10-06 19:14:11 +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 881f6b1286 ci(release): port Agendula's 1.1.0 release pipeline, with betas
Brings the workflows, scripts and fastlane setup up to Agendula's
v1.1.0-beta.1 state:

- beta.yaml: a release/** branch carrying X.Y.Z-beta.N publishes a
  Gitea + Codeberg pre-release with the signed APK. Never F-Droid or Play.
- scripts/version_info.sh derives versionCode from versionName for every
  workflow and CI step. Clockula has no shipped 1.0.x codes, so it uses one
  scheme: X*1000000 + Y*10000 + Z*100 + (beta N, or 99 for stable).
- CI checks the committed versionCode, blocks a beta from reaching main,
  enforces the 500-char What's New and validates the store listing.
- Shared publish_gitea_release.sh / publish_codeberg_release.sh /
  release_notes.sh replace the inline steps in release.yaml.
- Renovate also runs on every push to main to rebase conflicted PRs.
- jniLibs keepDebugSymbols, which the reproducibility guard now requires.

Fixes a bug present in Agendula too: the Codeberg tag lookups used
/git/refs/tags/{tag}, which matches by prefix, so v1.0.0 would have
"existed" as soon as v1.0.0-beta.1 did. That would have refused every
later beta and skipped the stable release. Both now use /tags/{tag}.
2026-10-06 19:14:11 +02:00
makiolaj b5680bdc9e docs(changelog): note the fixes for #2 through #8 2026-10-05 19:49:14 +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 8cbb8b311c feat(widgets): let the quick timer start a custom duration
The quick-timer config screen now ends its list of presets with a
"Custom…" row that opens the timer keypad. Anything from 1 second to
59:59 is accepted; past that the keypad shows a range hint and OK stays
disabled, since the widget's readout is sized for MM:SS. A stored value
that isn't a preset marks the Custom row, with the value as its summary.

The keypad dialog is lifted out of the timer editor into a shared
TimerDurationDialog, so both use the same one; the editor's now also
shows the range hint the setup screen shows.

Closes #7
2026-10-05 19:45:31 +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 eb63f02d38 fix(timers): fit the add-timer keypad on one screen
The keypad had fixed 92 dp keys, about 410 dp for its four rows. On a
short screen, in landscape, in split screen or at a large display or
font size, the readout, keypad and Start row no longer fit and Start was
cut off.

The keypad now takes whatever height the readout and Start row leave and
sizes its keys to fit, between the 48 dp touch-target minimum and the
old 92 dp, with the gap scaled along. A landscape screen puts the keypad
beside the readout and Start. Normal phones keep the current look. The
Timers tab's empty-state input shares the same panel and now sits
outside the lazy list, so it has a real height to fit into.

Closes #3
2026-10-05 19:33:49 +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 face2edb8d fix(ringtones): let a sound preview be stopped
Release — F-Droid repo + Gitea/Codeberg release + Play / detect (push) Failing after 10s
Release — F-Droid repo + Gitea/Codeberg release + Play / release (push) Skipped
Release — F-Droid repo + Gitea/Codeberg release + Play / play (push) Skipped
The picker's play button only ever started a preview; nothing but taking
a row, playing another one or leaving the picker stopped it. The
previewer now publishes what it is sounding, cleared on stop and when the
tone ends, and the playing row's button becomes a stop button.
2026-10-02 22:33:20 +02:00
makiolaj fe64521707 fix(widgets): keep GlanceAppWidget classes distinct under R8
R8 merged the near-identical widget classes (timer, quick timer,
stopwatch, digital clock) into the analog clock's class. Glance finds a
widget's placed instances by its GlanceAppWidget's class name, so every
updateAll hit every widget: starting a quick timer redrew the timer
widgets' content into the analog and digital clocks until their config
screen re-rendered them. Debug builds were unaffected.
2026-10-02 22:33:20 +02:00
makiolaj a1337e7d9a fix(widgets): keep WorkManager's InputMerger constructor under R8
R8 under AGP 9 pruned the no-arg constructor of OverwritingInputMerger,
which WorkManager instantiates reflectively before running any worker.
Every Glance session worker then failed with "Could not create Input
Merger", no RemoteViews reached the host, and every widget showed
"Can't load widget" in minified builds. Debug builds were unaffected.
2026-10-02 22:15:05 +02:00
makiolaj a8bc652b22 feat(settings): call the app "Clock" and add an app icon picker
The launcher and system label is now a plain, translatable "Clock"; the
brand name Clockula stays on the About card and in crash reports via an
untranslated app_brand_name string.

Settings → Appearance → App icon chooses between the bloom mark and a new
plain clock icon (the same dial and hands without the badge). The launcher
entry moves off MainActivity onto two <activity-alias> components, exactly
one enabled at a time, as Calendula does for its launcher name. The
component-enabled state is the single source of truth, and switching
enables the target before disabling the other so the launcher never sees
zero entries.
2026-10-02 20:55:09 +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 767b9ed72d chore(fastlane): add store listing icon 2026-10-02 20:18:24 +02:00
makiolaj acd99964ae fix(widgets): keep android:tint in picker previews, suppress UseAppTint
The previews are inflated as RemoteViews by the launcher, where AppCompat's
app:tint never applies, so lint's suggested fix would drop the tint.
2026-10-02 19:18:14 +02:00
makiolaj bad996d946 feat(widgets): stopwatch style picker and widget-picker previews
The stopwatch widget gets a per-instance style, chosen in a new
StopwatchConfigActivity on the APPWIDGET_CONFIGURE contract. Every widget
receiver now carries its own label, with a separate description, and the
widget picker shows sample-value preview layouts instead of a bare name.
2026-10-02 19:16:36 +02:00
makiolaj 7f2b047182 feat(settings): category hub with sub-screens, matching Agendula and Calendula
The flat settings list becomes a hub of icon-chip category rows that slide
into their own screens: Appearance, Alarms, Timers and Clock. The About card
carries the app logo and a support row; source, licence and privacy links
move to an About group at the foot, above a plain version line.

Report a problem now surfaces a pending crash report first, replacing the
separate "Send last crash report" row.
2026-10-02 19:16:32 +02:00
makiolaj 520bf379b4 docs: add privacy policy 2026-10-02 19:11:18 +02:00
makiolaj 0f5cf4f1eb fix(widgets): config saves, live world clocks, quick-timer guards
- Config screens share WidgetConfigActivity: a picker that dismisses right
  after selecting no longer finishes with RESULT_CANCELED (which removed a
  newly placed widget on API 29/30), and the save runs to completion.
- Digital config shows only once stored settings are read, so an early OK
  or a late load can't write defaults over them.
- Digital widget collects world clocks inside the composition, so edits
  reach a live session.
- Quick timer: a second tap while its timer is active starts nothing;
  durations are capped at 59:59 (1 h preset dropped) to fit the readout.
- Analog date hand is redrawn at local midnight and on time/zone changes
  (AnalogDateRefresh); updatePeriodMillis back to 0.
- WidgetSync routes each flow only to the widgets that read it.
- Stopwatch widget uses the ElapsedRealtimeClock seam and has a two-row
  minimum height.
- Remove dead quick-timer helpers; share showWorldClockIntent.
2026-10-02 18:18:04 +02:00
makiolaj 82fff19d3d feat(widgets): M3 Expressive widget lineup — analog, digital, stopwatch, quick timer
Six widgets, each with minimal settings, styled with M3 Expressive shapes and
Material You colours, and sized proportionally to the placed widget
(SizeMode.Exact + LocalSize):

- Analog clock (new): nine faces on MaterialShapes dials (Round, Numeral,
  Day, Cookie, Sunny, Scallop, Clover, Square, Petal) with per-face hands,
  a seconds dot and Google-Clock-style orbiting date on some faces, and a
  day/date pill on others. Style picker previews inflate the real views.
- Digital clock: rebuilt as RemoteViews faces (Classic, Thin, Bold, Pill,
  Stacked) shared by the widget and its config previews; world clocks are
  opt-in and only on the card faces.
- Next alarm and Timers: shape badges, icon buttons, proportional readouts;
  a lone timer on a tall widget gets a stacked layout.
- Stopwatch (new): large readout with play/pause pill and lap/reset.
- Quick timer (new): a Scallop that is a timer of its own — tap to start a
  transient timer of its configured duration, tap again to reset/stop.

Colours resolve in the launcher (widget_* palette resources, GlanceTheme's
dynamic colours) so widgets follow day/night and the wallpaper. Data widgets
collect their flows inside the composition so a live Glance session never
shows stale state, chronometer bases use elapsedRealtime, and config
activities close cleanly on an invalid widget id.
2026-10-02 18:01:11 +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 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
makiolaj 0664791ac5 test(manifest): fix ManifestRulesTest permission-list drift
POST_PROMOTED_NOTIFICATIONS was added to AndroidManifest.xml for the
timer's Live Update chip but never added to this test's expected
permission list — pre-existing drift, unrelated to M10, caught while
touching this file for settings work.
2026-10-01 17:35:55 +02:00
makiolaj 3266807ffe build: add kotlinx.serialization and Glance for M10
A pure-Kotlin JSON codec (so the backup round-trip test runs on the
JVM) and Jetpack Glance (the three M10 widgets). Both resolve from
google()/mavenCentral() only, no verification-metadata file exists to
update, and the repro guard checks vcsInfo/dependenciesInfo/foojay —
none of which this touches.
2026-10-01 17:23:24 +02:00
makiolaj c8cc156691 docs: bring home-screen widgets into M10, out of post-v1
Widgets share M10's machinery end to end: the next-alarm and timer
state a widget mirrors is exactly what the settings/backup/self-check
work already touches, and a widget tap is just another caller of the
MainActivity door the AlarmClock contract already opens. Keeping them
parked in post-v1 would mean building the same read paths twice.

QS tile, screensaver and bedtime stay deferred — none of them shares
M10's surface the way widgets do.

Also tightens PLAN.md §4's DND wording (interruption-filter read, no
ACCESS_NOTIFICATION_POLICY) and §7's backup semantics (whole-state
replace, ring state excluded, Room column vocabulary) ahead of M10's
implementation.
2026-10-01 17:22:07 +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 3ff74dc1d5 docs: mark M9 done
Release — F-Droid repo + Gitea/Codeberg release + Play / detect (push) Successful in 8s
Release — F-Droid repo + Gitea/Codeberg release + Play / release (push) Failing after 6m50s
Release — F-Droid repo + Gitea/Codeberg release + Play / play (push) Skipped
2026-09-23 11:02:24 +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 46f625b6b2 test(arch): the platform's intents stay at the door, and the show intent stays pointed
Three new rules. Nothing under `domain/` may name `android.content.Intent`, so
the validator stays testable without a device. The `AlarmClock` action strings
may be named only under `domain/interop/` and `interop/`. And
`AndroidAlarmScheduler` may not name `AlarmRingActivity` — the defect this
milestone fixed would otherwise come back the next time someone reaches for a
"show the alarm" intent.
2026-09-23 11:02:20 +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 02f3794ee8 feat(shell): navigation the system can ask for, read once per launch
An incoming request is more than a tab now — it can open an editor, compose a
timer or ask which alarm to dismiss. So it is read once, when the activity is
created rather than on every configuration change: re-deriving it on a rotation
would drag the user back into an editor they had just left.

The extras are unparcelled behind a guard here too. `MainActivity` is exported,
and before this it only ever read the action, which never unparcels.
2026-09-23 11:02:06 +02:00
makiolaj e55f3d19d4 feat(interop): one exported door for the whole AlarmClock contract
`AlarmClockActivity` is the only exported surface that takes a platform intent,
guarded by the caller-side `SET_ALARM` permission — which cannot go on
`MainActivity`, because the system checks it against the Launcher too. It is
invisible, does its work, and finishes.

Two intent-filters, not one: a filter carrying a `<data>` element never matches
an intent without data, so the deeplink actions need a filter of their own.
`IntentExtrasReader` turns the `Bundle` into a plain map and guards the
unparcel, because an exported door is reachable by anything on the device and a
hostile Parcelable must not crash the launch.
2026-09-23 11:02:06 +02:00
makiolaj 15dfaf300b fix(alarm): point the next-alarm show intent at the app, not the ring screen
The `AlarmClockInfo` show intent — what the status-bar alarm icon and the
lockscreen's next-alarm line open — targeted `AlarmRingActivity` with
`CLEAR_TASK`. Tapping it opened a ring screen for an alarm that was not
ringing, which resolved to "finished" and closed itself again.

It now opens the app on its Alarms tab. That show intent is what "next-alarm
publishing" means, so the rule pinning it belongs to this milestone.
2026-09-23 11:01:54 +02:00
makiolaj 72b788b589 feat(engines): dismiss and snooze from outside, under the same lock
The verbs an assistant can ask for go on the engines, not on a handler that
orchestrates repositories behind their backs. `dismissUpcoming` and
`snooze(id, minutesOverride)` move the ring slot and the backstop;
`dismissExpired` and `dismissAllExpired` move the timer's expiry registration.
Each of those is why they belong under the engine's mutex.

A snooze duration a caller sends is clamped to one hour, because that one is
the user's own request happening now. An alarm or timer marked to delete
itself is deleted as its cycle closes rather than left disabled.
2026-09-23 11:01:54 +02:00