Commit Graph
166 Commits
Author SHA1 Message Date
makiolaj 40c71a49f7 fix(timers): delete on the application scope, and wait for storage in the delete tests
The timer list's delete now outlives a back press the same way the alarm list's does. The instrumented delete and Undo tests wait for the row to change in storage, because that work runs outside Compose's idling.
2026-10-22 08:35:14 +02:00
makiolaj ddec2d8ed1 fix(alarms): open the ring screen when the editor's Delete is refused
Deleting a ringing alarm from the editor is refused, and a refused tap did nothing visible. It now opens the ring screen, the same way out the list's card offers.
2026-10-22 08:25:12 +02:00
makiolaj 7adda73a79 fix(alarms): keep the editor's Delete off a ringing alarm too
With its confirmation gone, one tap on the editor's Delete would have
silenced a ringing alarm round its dismiss challenge, which the list's
swipe is already kept from doing; the ring screen is the way out. The
list's delete now runs on the application scope like the editor's, a
thrown-away card ignores a new drag, and the timers snackbar clears the
live pill on an empty tab.
2026-10-22 08:15:52 +02:00
makiolaj 5f3b97b32a fix(alarms): refuse a list delete of a ringing alarm in the engine
The list's delete re-checks for a ring under the engine lock, so a
swipe released as the alarm fires cannot silence it round its dismiss
challenge. An Undo whose snackbar is already up no longer goes stale
on a rotation after a long accessibility timeout, and a deleted card is
thrown clear of the screen edge.
2026-10-22 08:05:25 +02:00
makiolaj 697678b226 fix(alarms): drop an Undo offered while no list could show it
A delete can finish while no list is on screen, such as the app sent
home the instant the editor's Delete was tapped. Its Undo is now
dropped unshown once it is 30 seconds old, instead of surfacing hours
later. Alarms and timers share one UndoSlot for this. A card whose
swipe is re-enabled mid-settle still slides home.
2026-10-22 07:54:43 +02:00
makiolaj 62da327771 fix(alarms): bring a card home when its swipe is disabled mid-gesture
A card whose alarm starts ringing during a drag or the throw-away
animation slides back instead of staying displaced, and the delete is
re-checked after the animation, so it cannot land on a ringing alarm.
2026-10-22 07:44:28 +02:00
makiolaj af12b7bb34 fix(alarms): keep a ringing card out of the swipe, and Undo off the ViewModel scope
A ringing alarm's card no longer swipes or offers Delete: it leads to
the ring screen, and a swipe must not get round the dismiss challenge.
A drag cancelled before release (a ring screen opening on top) never
deletes. Undo restores on the application scope, since a claimed Undo
has no second chance. Closing a cycle and deleting share one teardown.
2026-10-21 20:22:25 +02:00
makiolaj ef26d433e1 fix(alarms): let an unanswered Undo lapse, and make delete and restore atomic
A snackbar the screen leaves unanswered (another tab, Settings, the app
closing) now counts as expired, except across a rotation. Held back, it
could restore a snapshot from hours ago, ring a long-gone timer, or
land on top of a backup restored in the meantime. One shared
UndoSnackbarEffect does this for alarms and timers.

The engines' delete and restore run non-cancellable, so a caller
cancelled half-way cannot leave a restored alarm unscheduled. The
editor writes the label still in flight before it deletes, so Undo
brings back what was typed.

The swipe counts the touch-slop overshoot, keeps a card flung back
towards the start, slides a card home if its row is still there a
second after the delete, and no longer recomposes on every drag frame.
2026-10-21 20:13:09 +02:00
makiolaj 2feed2ee35 feat(alarms): swipe to delete, with Undo
An alarm card swipes away towards the end edge, or deletes from its
"Delete" accessibility action, and the list's snackbar offers Undo.
The editor's Delete drops its confirmation for the same Undo, routed
through a singleton so the list can answer it after the editor pops.
Timer cards get the same Undo for their corner delete.

AlarmEngine.delete stops a ringing alarm and clears a snoozed one's
notification under the engine lock; restore puts the row back under
its own id with its ring state and reschedules. A ringing alarm comes
back dismissed, never ringing; a finished timer comes back reset.

The swipe is hand-rolled so the end-to-start drag stays with the tab
pager.

Fixes #20
2026-10-21 20:02:00 +02:00
makiolaj 3f431a7f32 fix: keep pending crash reports out of Auto Backup
The backup rules were empty, despite their comments, so a pending crash
report followed a restore to a new phone and prompted there.
2026-10-21 17:40:29 +02:00
makiolaj cac58a98db fix: say why a restore failed in words that can be translated
The snackbar appended the codec's or the exception's own English text,
class names and all. Failures now carry a category (not a backup, from a
newer version, invalid data, too large, unreadable) that the screen maps
to a string resource; the detail stays in the result for tests.
2026-10-21 17:39:38 +02:00
makiolaj 3003f75333 fix: keep the built-in timer presets when a backup has none
A backup without the timer_presets field decoded it as an empty list,
which means "the user removed them all", so restoring it wiped the
built-in presets. A missing field now keeps them. BACKUP.md describes
this and the per-alarm clamps.
2026-10-21 17:37:11 +02:00
makiolaj 805da39074 fix: hold a timer's heads-up while an alarm is ringing
A timer whose ring was held back by a ringing alarm still popped a
heads-up over the alarm's screen, then alerted again once the alarm
ended. The heads-up now waits with the ring.
2026-10-21 17:36:35 +02:00
makiolaj 85c12811b0 fix: redraw the next-alarm widget at midnight
The widget worked out "Today" when it was drawn, and nothing redrew it
when the date changed, so a widget drawn on Monday night still named
Tuesday on Tuesday morning. It now shares the analog faces' midnight
redraw, renamed to MidnightRefresh, and redraws on a clock or zone change.
2026-10-21 17:35:39 +02:00
makiolaj 9af3d2b515 fix: label the ringtone preview button as a preview
TalkBack read the play button as the tone's name alone, right after the
row that already said it.
2026-10-21 17:34:13 +02:00
makiolaj 1823e885a1 chore: move floret-kit to the selected-row semantics fix 2026-10-21 17:34:13 +02:00
makiolaj 3a9b7e16ea fix: refresh the Settings problem count on return
The hub read the self-check once, so after fixing something in the
self-check screen it kept showing the old number of problems. It now
re-reads on every resume, as the self-check screen does.
2026-10-21 17:32:37 +02:00
makiolaj f9f2bdc9a8 fix: survive a refused background service start
A sticky restart of the timer or stopwatch service has no user action
behind it, so Android 12+ can refuse its foreground start, and the crash
repeated with every restart. The same could happen to the alarm ring
after an inexact fire, after its state was written. Refused starts now
stop the service, or post the ring screen's notification, instead of
crashing. The ring service is stopped with stopService, which cannot
throw from the background.
2026-10-21 17:31:26 +02:00
makiolaj 7ca8d54b4a fix: reschedule when exact alarms are allowed again
Granting "Alarms & reminders" again left the next alarm on the inexact
fallback until something else re-registered it. The system's permission
broadcast now reschedules alarms and timers.
2026-10-21 17:31:26 +02:00
makiolaj d7d958971f fix: fall back when a ring's player fails mid-ring
A MediaPlayer that errored after it started left the alarm or timer
silent, with no fallback and no forced vibration. The ring now moves on
to the next sound in the chain, and vibrates when none is left.
2026-10-21 17:28:34 +02:00
makiolaj c99106321a fix: bring back a swiped-away ring notification
From Android 14 a foreground service's notification can be swiped away on
an unlocked phone, which left a sounding alarm with no controls. The ring
notification now reposts itself when it is deleted, and tapping a ringing
alarm in the list opens its ring screen.
2026-10-21 17:28:08 +02:00
makiolaj 9cd40d6813 fix: don't flag another app's earlier alarm in the self-check
The system reports only the earliest alarm clock of any app, so an older
alarm left in the stock clock showed up as a problem the offered fix
couldn't solve. Another app's alarm at or before Clockula's next fire is
now fine; a missing registration, or anything later than ours, still is
a problem.
2026-10-21 17:26:50 +02:00
makiolaj 99b357f4fb fix: clamp an alarm's own snooze and ramp values when they are read
Backup import clamped the app-wide defaults but passed an alarm's own
snooze length, snooze limit and volume ramp through unchanged, so a
snooze of 0 re-rang at once and a negative limit turned snoozing off.
Both import and the database read now clamp them to the ranges the
pickers offer, and imported timer lengths to the setup range.
2026-10-21 17:25:55 +02:00
makiolaj 96fc9c19cf fix: keep the device awake until an alarm or timer starts ringing
The fire broadcast's wake lock ended with the receiver, and the ring
services only took their own after a database read, so a dozing device
could suspend in between and ring late. Both receivers now hold a short
timed wake lock across the hand-off, and the alarm service takes its own
before the read.
2026-10-21 17:25:39 +02:00
makiolaj b883554dc4 fix: keep the dismiss challenge behind the notification's Dismiss
The ringing notification's Dismiss ended the alarm directly, so a heads-up
or the lock screen got round a sum or hold challenge. With a challenge set,
Dismiss now opens the ring screen. Snooze is shown only while snoozes are
left, since a refused snooze dismisses.
2026-10-21 17:23:17 +02:00
makiolaj 4d29669222 fix: ring a snooze that is delivered late
A snooze stays pending for the ring window after it comes due, but the
fire was rejected after the two-minute grace and re-registered at the
same past instant, so the alarm looped on wakeups and never rang after a
reboot or a late delivery.
2026-10-21 17:22:33 +02:00
makiolaj d48c2e68fa chore: 1.0.0-beta.3
Beta — Codeberg pre-release / detect (push) Successful in 4s
Beta — Codeberg pre-release / beta (push) Successful in 11m36s
Adds the screen-on and timer ring screen fixes, the "Timer expired" sound, the hardened backup restore and the faster fallback when a music app refuses to play an alarm.
v1.0.0-beta.3
2026-10-21 15:31:38 +02:00
makiolaj ec391e8675 docs: tidy the changelog and provenance wording
Drops CODE_QUALITY_REVIEW.md as well: 11ea9be fixed all eight of its
findings, and git history keeps the review.
2026-10-21 15:31:38 +02:00
makiolaj d5016183ad fix: fall back at once when a music app refuses the alarm
The start waited for PLAYING and nothing else, so an app that answered
the play request with an error (YouTube without Premium) left the alarm
vibrating in silence for the full 15 s START_TIMEOUT before the default
sound took over. An error stamped at or after the request, or a lost
connection, now ends the wait straight away. An error left over from
the user's own listening still doesn't count.
2026-10-21 15:31:38 +02:00
makiolaj 6637554c9d ci: fail the Codeberg publish when an upload fails
The asset uploads and the release PATCH only printed their HTTP status,
so a failed upload still ended in "Published" and left the release
Obtainium follows without its APK or checksum. Each now retries with
backoff and fails the job if it never succeeds.
2026-10-21 14:16:59 +02:00
makiolaj 67010a632c refactor: restore settings through their typed prefs
applyBackup re-derived every key name, type and clamp by hand because
Pref.write was internal to floret-kit. floret-kit now has a typed
MutablePreferences[pref] = value, so a restored value is stored exactly
as its setter would, and a changed range or encoding reaches restore
too.
2026-10-21 14:16:58 +02:00
makiolaj 405374de7e fix: close the alarm screen when a restore replaces the ringing alarm
A backup restore stops the ring while its row still exists and deletes
the row afterwards, so the screen's one check came too early and left a
silent ring screen up for good. It now closes once the coordinator and
storage both say nothing rings, which still keeps it up if the import
fails and the ring resumes.
2026-10-21 14:16:58 +02:00
makiolaj c83c12cf64 feat: offer the "Timer expired" sound
The sound Google Clock and BlackyHawky's Clock use for timers is
timer_expire.ogg from AOSP DeskClock, bundled with those apps rather
than installed as a system ringtone, so RingtoneManager never lists it.
It is now bundled here too and listed first in every sound picker.

The choice is stored as clockula://tone/timer_expired and resolved to
the raw resource when played, so it survives a restore into another
build variant. The file is Apache-2.0, recorded in docs/PROVENANCE.md
with the licence text in licenses/.

Fixes #14
2026-10-21 14:16:58 +02:00
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