Commit Graph
92 Commits
Author SHA1 Message Date
makiolaj df8be5e158 Merge branch 'feat/26-quick-settings-tiles' into release/v1.0.0
# Conflicts:
#	CHANGELOG.md
#	app/src/main/java/de/jeanlucmakiola/clockula/backup/BackupEngine.kt
#	app/src/main/java/de/jeanlucmakiola/clockula/data/backup/BackupCodec.kt
#	app/src/main/java/de/jeanlucmakiola/clockula/data/backup/BackupDto.kt
#	app/src/main/java/de/jeanlucmakiola/clockula/data/prefs/SettingsPrefs.kt
#	app/src/main/java/de/jeanlucmakiola/clockula/domain/backup/BackupContent.kt
#	app/src/main/java/de/jeanlucmakiola/clockula/ui/common/AlarmTimeFormatter.kt
#	app/src/main/java/de/jeanlucmakiola/clockula/ui/widget/NextAlarmWidgetContent.kt
#	app/src/main/res/values/strings.xml
#	app/src/test/java/de/jeanlucmakiola/clockula/ManifestRulesTest.kt
#	app/src/test/java/de/jeanlucmakiola/clockula/data/backup/BackupCodecTest.kt
#	app/src/test/java/de/jeanlucmakiola/clockula/data/prefs/SettingsPrefsTest.kt
#	docs/ARCHITECTURE.md
#	docs/BACKUP.md
#	docs/ROADMAP.md
2026-10-26 21:00:57 +01:00
makiolaj a0a1daa920 Merge branch 'feat/25-screen-saver' into release/v1.0.0
# Conflicts:
#	app/src/main/AndroidManifest.xml
#	app/src/main/java/de/jeanlucmakiola/clockula/backup/BackupEngine.kt
#	app/src/main/java/de/jeanlucmakiola/clockula/data/backup/BackupCodec.kt
#	app/src/main/java/de/jeanlucmakiola/clockula/data/backup/BackupDto.kt
#	app/src/main/java/de/jeanlucmakiola/clockula/data/di/TimeModule.kt
#	app/src/main/java/de/jeanlucmakiola/clockula/data/prefs/SettingsPrefs.kt
#	app/src/main/java/de/jeanlucmakiola/clockula/data/time/AndroidClockChanges.kt
#	app/src/main/java/de/jeanlucmakiola/clockula/domain/backup/BackupContent.kt
#	app/src/main/java/de/jeanlucmakiola/clockula/domain/widget/NextAlarmWidgetModel.kt
#	app/src/main/java/de/jeanlucmakiola/clockula/ui/alarms/AlarmsScreen.kt
#	app/src/main/java/de/jeanlucmakiola/clockula/ui/common/AlarmTimeFormatter.kt
#	app/src/main/java/de/jeanlucmakiola/clockula/ui/settings/ClockSettingsScreen.kt
#	app/src/main/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockFace.kt
#	app/src/main/res/values/strings.xml
#	app/src/test/java/de/jeanlucmakiola/clockula/data/backup/BackupCodecTest.kt
#	app/src/test/java/de/jeanlucmakiola/clockula/data/prefs/SettingsPrefsTest.kt
#	docs/ARCHITECTURE.md
#	docs/BACKUP.md
2026-10-26 20:59:33 +01:00
makiolaj 813bb52c03 Merge branch 'feat/24-automatic-home-clock' into release/v1.0.0
# Conflicts:
#	CHANGELOG.md
#	app/src/main/java/de/jeanlucmakiola/clockula/data/backup/BackupCodec.kt
#	app/src/main/java/de/jeanlucmakiola/clockula/data/backup/BackupDto.kt
#	app/src/main/java/de/jeanlucmakiola/clockula/data/prefs/ClockPrefs.kt
#	app/src/main/java/de/jeanlucmakiola/clockula/data/prefs/SettingsPrefs.kt
#	app/src/main/java/de/jeanlucmakiola/clockula/domain/ClockDefaults.kt
#	app/src/main/java/de/jeanlucmakiola/clockula/ui/settings/ClockSettingsScreen.kt
#	app/src/main/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockRow.kt
#	app/src/main/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockRowState.kt
#	app/src/main/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockSource.kt
#	app/src/main/res/values/strings.xml
#	app/src/test/java/de/jeanlucmakiola/clockula/data/backup/BackupCodecTest.kt
#	app/src/test/java/de/jeanlucmakiola/clockula/data/prefs/ClockPrefsTest.kt
#	app/src/test/java/de/jeanlucmakiola/clockula/data/prefs/SettingsPrefsTest.kt
#	docs/ARCHITECTURE.md
#	docs/BACKUP.md
2026-10-26 20:56:34 +01:00
makiolaj fd7f5e8dcd Merge branch 'feat/23-world-clock-seconds' into release/v1.0.0
# Conflicts:
#	app/src/main/java/de/jeanlucmakiola/clockula/backup/BackupEngine.kt
#	app/src/main/java/de/jeanlucmakiola/clockula/data/backup/BackupCodec.kt
#	app/src/main/java/de/jeanlucmakiola/clockula/data/backup/BackupDto.kt
#	app/src/main/java/de/jeanlucmakiola/clockula/data/prefs/SettingsPrefs.kt
#	app/src/main/java/de/jeanlucmakiola/clockula/domain/backup/BackupContent.kt
#	app/src/test/java/de/jeanlucmakiola/clockula/data/backup/BackupCodecTest.kt
#	app/src/test/java/de/jeanlucmakiola/clockula/ui/settings/SettingsViewModelTest.kt
#	docs/ARCHITECTURE.md
#	docs/BACKUP.md
2026-10-26 20:54:31 +01:00
makiolaj 48d09f7371 Merge branch 'feat/22-timer-vibrate-ramp' into release/v1.0.0
# Conflicts:
#	app/src/main/java/de/jeanlucmakiola/clockula/ClockulaApp.kt
#	app/src/main/java/de/jeanlucmakiola/clockula/data/prefs/SettingsPrefs.kt
#	app/src/main/java/de/jeanlucmakiola/clockula/domain/ClockDefaults.kt
#	app/src/main/java/de/jeanlucmakiola/clockula/domain/timer/TimerRing.kt
#	app/src/main/java/de/jeanlucmakiola/clockula/ui/settings/SettingsViewModel.kt
#	app/src/main/java/de/jeanlucmakiola/clockula/ui/settings/TimerDefaultsScreen.kt
#	app/src/test/java/de/jeanlucmakiola/clockula/data/prefs/SettingsPrefsTest.kt
#	docs/ARCHITECTURE.md
2026-10-26 20:54:01 +01:00
makiolaj 6f615be4ad Merge branch 'feat/21-schedule-date-pause' into release/v1.0.0
# Conflicts:
#	CHANGELOG.md
#	app/src/main/java/de/jeanlucmakiola/clockula/ui/alarms/AlarmEditorScreen.kt
#	app/src/main/java/de/jeanlucmakiola/clockula/ui/alarms/AlarmEditorViewModel.kt
#	app/src/test/java/de/jeanlucmakiola/clockula/ui/alarms/AlarmEditorViewModelTest.kt
#	app/src/test/java/de/jeanlucmakiola/clockula/ui/alarms/AlarmListRowsTest.kt
#	app/src/test/java/de/jeanlucmakiola/clockula/ui/alarms/AlarmsViewModelTest.kt
2026-10-26 20:52:26 +01:00
makiolaj a186e5afcd Merge branch 'feat/19-alarm-set-snackbar' into release/v1.0.0
# Conflicts:
#	app/src/main/java/de/jeanlucmakiola/clockula/ui/alarms/AlarmsScreen.kt
#	docs/ARCHITECTURE.md
2026-10-26 20:50:55 +01:00
makiolaj d81cd04b60 Merge branch 'feat/18-volume-buttons' into release/v1.0.0
# Conflicts:
#	app/src/main/java/de/jeanlucmakiola/clockula/alarm/AlarmEngine.kt
#	app/src/main/java/de/jeanlucmakiola/clockula/data/backup/BackupCodec.kt
#	app/src/main/java/de/jeanlucmakiola/clockula/data/prefs/ClockPrefs.kt
#	app/src/main/java/de/jeanlucmakiola/clockula/data/prefs/SettingsPrefs.kt
#	app/src/main/java/de/jeanlucmakiola/clockula/domain/ClockDefaults.kt
#	app/src/main/java/de/jeanlucmakiola/clockula/ui/settings/AlarmDefaultsScreen.kt
#	app/src/main/java/de/jeanlucmakiola/clockula/ui/settings/SettingsViewModel.kt
#	app/src/test/java/de/jeanlucmakiola/clockula/data/backup/BackupCodecTest.kt
#	app/src/test/java/de/jeanlucmakiola/clockula/data/prefs/ClockPrefsTest.kt
#	app/src/test/java/de/jeanlucmakiola/clockula/data/prefs/SettingsPrefsTest.kt
#	docs/ARCHITECTURE.md
#	docs/BACKUP.md
2026-10-26 20:49:43 +01:00
makiolaj 333a23d6be Merge branch 'feat/17-silence-after' into release/v1.0.0
# Conflicts:
#	app/src/main/java/de/jeanlucmakiola/clockula/alarm/AlarmEngine.kt
#	app/src/main/java/de/jeanlucmakiola/clockula/data/backup/BackupDto.kt
#	app/src/test/java/de/jeanlucmakiola/clockula/data/backup/BackupCodecTest.kt
#	app/src/test/java/de/jeanlucmakiola/clockula/data/prefs/SettingsPrefsTest.kt
#	docs/BACKUP.md
2026-10-26 20:48:54 +01:00
makiolaj 440d098c77 Merge branch 'feat/16-upcoming-alarm-notification' into release/v1.0.0
# Conflicts:
#	app/src/main/java/de/jeanlucmakiola/clockula/ClockulaApp.kt
2026-10-26 20:48:13 +01:00
makiolaj ea8e159b39 Merge branch 'feat/1-bedtime-mode' into release/v1.0.0
# Conflicts:
#	docs/ARCHITECTURE.md
2026-10-26 20:47:54 +01:00
makiolaj 2191166035 fix(alarms): show the alarm-set countdown as the family's SnackChip
The countdown used the stock Material snackbar, while Agendula and
Calendula confirm with floret-kit's SnackChip. It now opens as that chip:
bottom-start in the FAB's band on the list, ellipsising short of the FAB,
and in the editor's snackbar slot so it follows the predictive-back
preview. It fills the band's height, as an Undo chip does in the other
apps, and sits on the secondary container to stand apart from the cards.

The chip has no timer, so AlarmSetChipBand holds it for a short
snackbar's four seconds, stretched as the accessibility settings ask.
The hand-over is unchanged: a chip cut short stays pending for the next
resumed screen.

Beside the FAB there is room for about 308 dp, so the message is now
"Alarm in 7 hours and 12 minutes" rather than "Alarm set for … from now",
which would have lost its number to the ellipsis.

Refs #19
2026-10-26 20:21:56 +01:00
makiolaj 817fe77b17 fix(bedtime): record only accepted states, urgent wake-up, keep bedtime on old restores
A refused state change is now remembered as owed and forced by every pass
until it lands, instead of recording the opposite state, which could hide
the next real boundary. Any refusal, in the rule rewrite or the state
change, pulls the slot in to a retry, and the slot is armed in a finally
so a throwing step can't leave it unarmed. The wake-up that ends the mode
is armed allow-while-idle (one such fire a night), so Do Not Disturb no
longer lingers into the morning behind a doze deferral.

The manifest filter for the access-changed broadcast is gone: the
platform only delivers it to runtime-registered receivers, and the
screen's resume check covers it. A backup written before bedtime existed
now keeps the current bedtime settings, and a restore goes through the
engine's own field comparison, so a reminder-only difference leaves a
hand-switched mode alone.

Refs #1
2026-10-22 12:54:02 +02:00
makiolaj 74c0c278ef fix(bedtime): retry refused removals, state changes and rule rewrites
A removal the system refuses while turning bedtime off now keeps the slot
armed fifteen minutes out as its retry, instead of cancelling it and
leaving Do Not Disturb on until some unrelated launch. A refused state
change records the opposite state so the next pass retries it, and a
rule that was rewritten (which clears its condition on Android 10-14) is
switched regardless. A refused rewrite is remembered and retried by every
pass until it lands, rather than resting on the one edit that asked.

Refs #1
2026-10-22 12:41:15 +02:00
makiolaj 131a28c1be fix(bedtime): bounded fallback window, fresh-screen access check
Without exact-alarm access the slot falls back to a ten-minute window
instead of plain set(), whose slack could end an eight-hour night hours
late. A freshly opened bedtime screen reads whether bedtime is on from the
store, not from its still-loading state, so access granted while the
screen was closed creates the rule. A restore that rolled back no longer
forces the mode.

Refs #1
2026-10-22 12:30:26 +02:00
makiolaj 1a5552a221 fix(bedtime): keep the slot off the allow-while-idle ration
Android rations allow-while-idle wake-ups per app in doze, and the timer's
expiry slot and the alarm's fallback share that ration, so a 07:00
wake-up fire could hold back a timer due at 07:04. The bedtime slot is
now a plain exact alarm that doze may defer, which can move a mode
boundary but never a ring. A restore of unchanged bedtime settings no
longer forces the mode, and the screen skips its rule check while bedtime
is off.

Refs #1
2026-10-22 12:20:25 +02:00
makiolaj 8b182c999b fix(bedtime): small hours end at 06:00, retry daytime removals
Only a bedtime before 06:00 now counts as the small hours after the
chosen evening, so a day sleeper's 08:00 bedtime runs on the days picked.
A rule whose removal was refused while turning bedtime off in the daytime
is removed by the next pass, and a restored reminder length the picker
doesn't offer stays choosable.

Refs #1
2026-10-22 12:11:04 +02:00
makiolaj b701859156 fix(bedtime): name nights by their evening, retry a refused removal
A bedtime before noon now belongs to the evening before, so Sunday to
Thursday nights at 00:30 run early Monday to early Friday, not Saturday
night through Wednesday night. Turning bedtime off clears the "last
applied" state only once the rule is really gone, so a refused removal
is retried instead of leaving Do Not Disturb on. Reopening the screen
after granting access elsewhere creates the rule even if the system's
broadcast never arrived. The empty-mask rule lives in one place.

Refs #1
2026-10-22 12:02:39 +02:00
makiolaj 1a443c066f fix(bedtime): DST-safe windows, ordered edits, quieter passes
Whether the wake time is the same night or the next morning is now
decided on the local times, so a bedtime in a spring-forward gap no
longer turns a 45-minute window into a day-long one. Screen edits start
undispatched so they reach the engine's lock in the order they were made,
and an edit that rewrites the rule also re-asserts the mode, in case the
platform reset its state.

The launch-time bedtime pass runs even if an earlier engine pass failed,
a restore that never applied settings no longer forces the mode, and a
user who never turned bedtime on pays nothing for it at launch or boot.
The screen says when equal bedtime and wake-up times mean it never starts.

Refs #1
2026-10-22 11:53:39 +02:00
makiolaj 2dce78281a fix(bedtime): switch a recreated rule, keep manual overrides, finish edits
A rule recreated by a pass (deleted in Settings, access re-granted) now
gets its state set even when no boundary was crossed, and the applied
state is recorded only once the system accepted it. Edits that can't move
a boundary (reminder, effects) no longer re-assert the mode over a manual
override, and the screen's writes run on the application scope so leaving
right after a change can't cut it short. A late reminder counts the
minutes actually left, and one slot fire reads the zone once.

An equal bedtime and wake time is now no window instead of a day-long one,
which on every night meant Do Not Disturb never ended, and an empty days
mask in prefs reads as every night, as a backup's does. The rule-id extras
are translated to the bedtime action in MainActivity, keeping the routing
table keyed on the action alone.

Refs #1
2026-10-22 11:38:56 +02:00
makiolaj 119197106b fix(bedtime): follow boundaries, open from Settings, keep the user's policy
Settings opens a rule's configuration activity with no action, only the
component and the rule id, so MainActivity now routes on the rule-id
extras. Each engine pass reads the clock once, so the mode's state and the
next slot can't disagree around a boundary, and non-edit passes switch the
mode only when the schedule crossed a boundary since Clockula last set it
(persisted), which also catches up a wake-up missed during a force stop
while leaving a mode switched on by hand alone. A reminder fire that also
ends a window now turns the mode off, and a reminder the schedule moved
away from is withdrawn.

The rule is only rewritten when a field it carries changes, and a rewrite
keeps the existing policy if it still allows alarms, so Android 10-14 no
longer loses the user's Do Not Disturb edits. The bedtime receiver, screen
and fallback alarm call are guarded, the boot-path resync runs even if an
earlier engine failed, and a backup with no nights restores every night.

Refs #1
2026-10-22 11:29:33 +02:00
makiolaj 83a77c1bb0 fix(settings): name the bundled sound in the default-sound rows
Picking "Timer expired" as the default alarm or timer sound showed "Custom" in Settings, because the summary knew only the device default and silence. It now names the bundled sound, as the editors already do.

ARCHITECTURE.md now names the exact bundled sentinel instead of a prefix the code does not recognise, lists clockula://media values in the alarm column, gives the timer column the same values, and says RingtoneCatalog lists the bundled sounds too.

Refs #14
2026-10-22 11:20:39 +02:00
makiolaj 69b436cffd feat(bedtime): add an opt-in bedtime mode under Settings
Clockula can now turn on a Do Not Disturb mode of its own at bedtime and
off at the wake-up time, on the nights the user picks, with an optional
reminder before it starts. Digital Wellbeing's Bedtime mode can't be
driven by a third-party app, so this is a separate rule: a typed mode
with grayscale, dimmed wallpaper, dark theme and hidden always-on display
on Android 15+, and a plain DND schedule rule on Android 10-14.

The rule's policy always allows alarms (and media). BedtimeEngine owns one
AlarmManager slot of its own, on its own receiver and request code, and
every bedtime call on the boot, time-change and launch paths runs after
the alarm and timer passes, guarded. The rule id is never stored: the
platform lists an app's own rules, so restores and reinstalls find or
recreate it. Turning bedtime off removes the rule. The screen walks the
user through granting DND access and explains the Wellbeing overlap.
Settings are part of the JSON backup; older files restore bedtime as off.

Refs #1
2026-10-22 11:19:38 +02:00
makiolaj 1559e68b2f docs(ringtones): describe the bundled tone and test its no-sounds banner
ARCHITECTURE.md still said every non-silent ringtone_uri is a content URI and listed the picker's rows without the bundled "Timer expired" sound, so a reader could Uri.parse the clockula://tone/ sentinel directly. It now names the sentinel, BundledTones as its only resolver, and where the tone sits in the picker.

A view-model test now covers a device whose only listed tone is the bundled one, which must still show the "no alarm sounds" banner. The existing test only checked an empty list, so it passed even if the bundled tone were counted as a device sound.

Refs #14
2026-10-22 11:14:42 +02:00
makiolaj 5c6ec88bb6 docs(architecture): say the timer-vibrate copy runs alongside the boot pass
It runs in its own coroutine with no ordering against the engines, not ahead of them.

Refs #22
2026-10-22 11:05:48 +02:00
makiolaj 94f09e9e5a feat(alarms): choose what the volume buttons do while ringing
A new setting in Settings > Alarms: while the ring screen shows, the
volume buttons change the volume (the default, and what they did
before), snooze, dismiss, or do nothing. It is app-wide, stored next to
the clock defaults, and included in the backup; a backup without it
restores "change the volume".

A press is made without looking, so VolumeButtonPolicy keeps it from
ending an alarm more finally than the ring screen would. Dismiss with a
dismiss challenge set snoozes instead, so the challenge still stands.
With no snoozes left, Snooze (and Dismiss behind a challenge) falls
back to changing the volume, never to the engine's refused-snooze path,
which dismisses. The key's snooze also asks the engine to keep ringing
if the limit turns out to be reached after all (AlarmEngine.snooze's new
dismissIfRefused). The ring screen decides whether a press is its own
on key down and keeps that for the release, leaves the keys alone once
another alarm has taken over the ring, and ignores a second snooze or
dismiss while the first is still running.

Only the ring screen handles the keys. While the ring is a heads-up over
another app, a background app gets no key events, and the one way
around that does not hold up: a remote-playback MediaSession whose
VolumeProvider takes the keys. On Android 13 and later
MediaSessionRecord.canHandleVolumeKey refuses such a session from an app
without a MediaRouter2 routing session, which Clockula's would be, so on
current Pixels the keys never reach it. Where it does work, it loses to
whatever app is in front or played last, shows a cast-volume slider and
a phantom media player, and takes media buttons while the alarm rings.
AOSP DeskClock and BlackyHawky's Clock also handle the keys in their
alarm activity only. ARCHITECTURE.md records the limitation.

Fixes #18
2026-10-22 11:04:39 +02:00
makiolaj 4765b8e088 fix(screensaver): keep both screen saver activities out of the app's task
The settings screen Android's screen saver list opens gets a task of its
own and stays out of recents, so it never lands in or clears the app's
task, and its bars follow the app's theme. The on-demand screen saver is
noHistory, so leaving it by Home does not bring the app back to it later,
and it only touches the keep-screen-on flag when charging starts or stops.

The next-alarm line uses the same 12/24-hour rule as the big readout,
joined to the day by a translatable string; the minute's crossfade shows
the old minute fading out rather than two copies of the new one.
2026-10-22 08:39:07 +02:00
makiolaj 712b606779 fix(alarms): keep the upcoming notice off watches, and check past the grace
The notice is local-only, so its Dismiss is never bridged to a watch where the phone's unlock does not apply. The closing and skip-spending checks land a second past the fire grace, since onFire still accepts a fire at exactly the grace.

Fixes #16
2026-10-22 08:21:33 +02:00
makiolaj 9839520b7b refactor(alarms): one date formatter, and a picker year range sized to its date
The list and editor now format a date through the same helper as the
widget, and the date pickers keep their default years unless a stored
date lies beyond them, instead of always spanning to 9999.
2026-10-22 08:20:19 +02:00
makiolaj beeca29f74 fix(tiles): redraw on clock and zone changes, and tighten the tile surfaces
The tiles' refresh now rides WidgetSync's collectors instead of a second
copy of them, and SystemEventReceiver asks both tiles to redraw on a time
or zone change, where the times they show move without any write.

The quick-timer tile remembers its timer before starting it, so a failed
start can't leave a timer it cannot see, and shows the timer's stored end
rather than one re-derived from two clocks on every read.

The tile preferences screen closes on anything but QS_TILE_PREFERENCES,
runs in its own task, and asks for a redraw after saving a duration. The
tiles and the next-alarm widget share one 12/24-hour time formatter and
the widgets' launch intent.
2026-10-22 08:19:49 +02:00
makiolaj 5ad9bd02d6 fix(alarms): spend a pending skip only after its fire grace
The upcoming check that spends a skip dismissed in advance now wakes once the skipped occurrence's fire grace is over rather than at the occurrence itself, so it can never get ahead of a fire still in flight for it.

Fixes #16
2026-10-22 08:13:31 +02:00
makiolaj da9854abf8 feat(tiles): Quick Settings tiles for the next alarm and a quick timer
"Next alarm" reads the next-alarm widget's model and shows the weekday
and time of the next alarm, or "No alarms"; a tap opens the Alarms tab,
behind an unlock on a locked device, through the PendingIntent overload
of startActivityAndCollapse on API 34+.

"Quick timer" is the quick-timer widget's twin: at rest it shows its
duration and a tap starts a transient timer through the timer engine;
while that timer is active the tile is on, shows when it ends (or that
it is paused or done), and a tap resets it. It works on a locked device,
as the timer notification does. Its duration is a new backed-up setting,
chosen by long-pressing the tile, with the widget's bounds and picker.

Both are active tiles, redrawn by TileSync when the state they mirror
changes rather than bound on every panel open. No new permission.

Fixes #26
2026-10-22 08:09:14 +02:00
makiolaj fdb725e64c fix(alarms): spend a skip at its occurrence again
Keeping a skip armed through its fire grace let the editor's switch or another dismissal inside those two minutes ring the skipped alarm late, or be absorbed. The resolver goes back to spending the skip at its occurrence and recording it as handled, and the upcoming check wakes at an armed skip's occurrence so the list stops saying "skipping next" on time.

Fixes #16
2026-10-22 08:07:52 +02:00
makiolaj 1e29226364 feat(screensaver): add a screen saver with a dim night mode
A DreamService shows a full-screen clock, the date and the next alarm,
digital or as the world clock's analog face, on black. Night mode dims
every colour towards black and turns the display down; the clock moves
to a new spot each minute so nothing burns in. It wakes only on the
minute and on a time, zone or 12/24-hour change.

The tabs' settings gear becomes an overflow menu with "Screen saver" and
"Settings". An app cannot start the system's dream without a system
permission, so that item opens an activity with the same content, which
keeps the screen on only while charging. Style and night mode live in
Settings > Clock and in the dream's own settings screen, and are carried
in backups; older backups restore the defaults.

Fixes #25
2026-10-22 08:07:44 +02:00
makiolaj a88e696d52 fix(alarms): spend a skip only after its fire grace
The resolver now keeps a skip armed until its occurrence is out of the fire grace too, so a reschedule in those two minutes cannot spend it early and let a fire already in flight ring the dismissed alarm; onFire refuses that fire by the armed watermark. The notifier also cancels notices it posted itself, which the platform may not list as active yet.

Fixes #16
2026-10-22 08:00:38 +02:00
makiolaj 247dbcb751 fix(alarms): name a pause's last day, and switch on an alarm given a date
"Paused, back on" named the day after the pause, which for a weekday
alarm can be a Saturday it never rings on. A pause under way now reads
"Paused through Wed, Oct 28".

Picking a date on a switched-off alarm switches it on, the pause picker
cannot confirm a range that is already over, a spent pause stored with
its ends reversed is cleared too, and the widget formats its date in the
app's own locale.
2026-10-22 07:53:11 +02:00
makiolaj b4af164826 fix(alarms): spend a dismissed skip on time, and ring a re-armed handled alarm
The upcoming check now also wakes once a noticed occurrence's fire grace has run out, so a notice cannot outlive a late fire, and once an armed skip's has, so the list stops saying "skipping next" about an occurrence already gone. onFire is back to checking only the armed skip: the resolver deliberately re-arms a handled occurrence that a clock moved backwards put back in the future, and that one must ring.

Fixes #16
2026-10-22 07:51:57 +02:00
makiolaj b79b38bd2b fix(worldclock): fix Etc/GMT names at the fallback, not per caller
The previous rule named every non-place id by its current offset, which
renamed legacy ids such as Japan, called Etc/UTC "GMT" and made a CET
row's name change at DST. Only the Etc/GMT+-n ids have a misleading
last segment, so ZoneCatalog.cityFallbackFor now flips just those to
their real offset. That also reaches the zone picker's device row,
which takes its name from the same fallback.
2026-10-22 07:44:54 +02:00
makiolaj 1dcd743e5b fix(alarms): keep a skip on the next ring after any edit, and say when a pause ends
Moving the time or changing the days left a pending skip pointing at the
old sequence, so the skipped day could ring at its new time. Every editor
change that can move a fire time now re-arms the skip.

A pause under way now reads "Paused, back on Thu, Oct 29" instead of
naming its last day, which read as the day it ends before. Reopening a
begun pause keeps its start, the date pickers take their bounds from the
clock when they open, and the dismiss chooser shows the scheduled date and
pause like the list does.
2026-10-22 07:43:30 +02:00
makiolaj f570bdefb7 fix(alarms): keep the upcoming notice off a locked screen below Android 12
The Dismiss action's unlock requirement is honoured only from Android 12, so on 10 and 11 the notice is now secret on the secure lock screen rather than dismissible by whoever holds the phone.

Fixes #16
2026-10-22 07:41:05 +02:00
makiolaj 40b4c9e2e4 fix(worldclock): name offset-set zones by their real offset everywhere
The Etc/GMT-n naming fix only covered the local row. A home that
follows a device set by UTC offset still put "GMT-9" under a UTC+9
face. The rule now lives in the one place every row and the face take
their city from.
2026-10-22 07:38:49 +02:00
makiolaj 1253c0fc55 fix(alarms): no fire for an occurrence already accounted for
A natural fire whose instant equals the skip or the handled-occurrence watermark is ignored, so a dismissal racing a reschedule cannot ring. Dismissing a repeating alarm persists its resolution before arming the skip, so an unspent old skip cannot absorb it. The deferred notice sync runs even if starting the ring throws, the swipe record is one synchronised read-modify-write, and it stays out of backups.

Fixes #16
2026-10-21 20:26:50 +02:00
makiolaj 471cbdabd8 fix(alarms): re-arm a skip when the pause changes, and clear spent dates in place
A skip armed under one pause kept its watermark when the pause changed,
so removing the pause left the alarm silent until the old pause's end.
The engine now re-arms a pending skip after a pause edit.

Spent dates and pauses are cleared with targeted, conditional updates
instead of a whole-row rewrite. SET_ALARM ignores a date or pause that
has already gone by when looking for an alarm to re-use, the editor
re-reads the clock when a date picker opens, and the next-alarm widget
shows the year for a date in another year.
2026-10-21 20:25:28 +02:00
makiolaj f46b7e1e37 fix(worldclock): keep the local row's day label and legacy city names
The local row prepends "Local time" to the offset and day, so on a
phone a one-line summary cut off the day ("Tomorrow"), the part that
matters most on a long trip. It now wraps to two lines. Only Etc/
offsets and bare abbreviations are named by their offset; a legacy
place id such as US/Pacific keeps its place name. The picker's home
selection now derives from the same settings subscription the tab
uses.
2026-10-21 20:24:24 +02:00
makiolaj 1333001aa9 fix(worldclock): name an offset-set local row by its real offset
A device set by UTC offset rather than by city reports an id like
Etc/GMT-9, which is UTC+9; the local row took its last segment as the
city and read "GMT-9". It is now named by its actual offset. LocalClock
takes the already-resolved home zone rather than resolving it a second
time, the tab reads both of its settings from one subscription, and the
local row falls back to the unknown-zone sentence for TalkBack instead
of an empty time.
2026-10-21 20:18:10 +02:00
makiolaj dedcbd5dfd fix(alarms): a consumed skip stays handled, and only a swipe hides a notice
A skip spent on its occurrence now becomes the handled-occurrence watermark, so a second resolution inside the fire grace cannot ring the skipped alarm after all. The upcoming notice records a swipe through its delete intent rather than inferring one, so a reboot or a blocked channel no longer hides it for good; its Dismiss asks for an unlock, and a Dismiss for a switched-off alarm clears its notice.

Fixes #16
2026-10-21 20:18:05 +02:00
makiolaj b1a45db54a feat(alarms): schedule an alarm on a date, pause a repeating one
A one-shot alarm can now ring on a chosen date, and a repeating alarm
can be paused for a range of days. Both are local dates: the date is a
floor and the pause a hole in the occurrence sequence, so DST and zone
changes resolve as for any other alarm. A missed date behaves like a
missed one-shot, and each clears itself once it can no longer change a
candidate. Picking a repeat day clears the date, as in Google Clock;
"skip next" stays the quick version of a pause.

The editor uses the M3 date and date-range pickers, which offer no past
day. The list, the next-alarm widget and the self-check read the
resolver, so they follow. The schema goes to v4 with a migration, and
the backup format to v2, which still imports v1 files. SET_ALARM no
longer re-uses an alarm that carries a date or a pause.

Fixes #21
2026-10-21 20:16:34 +02:00
makiolaj 5b35ef65d4 fix(alarms): keep the upcoming Dismiss away from the fire
A fire broadcast already in flight when a skip was armed on its occurrence is now ignored, and a Dismiss that lands once the occurrence is due is stale. The notices are synced after the ring starts on the fire path, and a swiped-away notice stays gone across a process restart. A switched-off alarm answers NotFound.

Fixes #16
2026-10-21 20:09:47 +02:00
makiolaj f5388042c6 feat(worldclock): show local time automatically while travelling
Issue #24 asks for Google Clock's "automatic home clock": a temporary
Home row while the device is somewhere the time differs from home. In
Google Clock that works because the hero clock is local time. Clockula
is the other way round: the hero face is home (the stored home zone, or
the device's zone when none is stored), every row is read against home,
and with a stored home the face already shows home time on a trip. A
Home row would only repeat the face; what goes missing while travelling
is the local time.

So the feature is turned round. With a home zone stored and the device's
zone on a different UTC offset right now, the tab shows the device's
zone as a temporary "Local time" row between the face and the saved
cities. It is never stored, sits outside the reorderable column, has no
move or remove actions and does not count towards the 24-city cap. The
comparison is the current UTC offset, as Google's is, so Paris with
home in Berlin shows nothing. The row is hidden when a saved city is
already on the device's offset. It is rebuilt on every tick from the
same device-zone read the face uses, so it appears and disappears
within a tick of a time-zone change while the tab is open.

Home that follows the device can never differ from it. Settings > Clock
gets an "Automatic local clock" switch (on by default) under the home
zone row; while home follows the device the row is dimmed and its hint
says it needs a home time zone. The switch is stored as
automatic_local_clock and included in backups. Files written before this
change restore it as on.

Fixes #24
2026-10-21 20:06:13 +02:00
makiolaj e43339c386 feat(alarms): notify before an alarm rings, with Dismiss
Two hours before a natural occurrence (AOSP DeskClock's lead), a silent
"Upcoming alarm" notification offers Dismiss, and the alarm's row shows
the same Dismiss as a chip. Dismissing is dismissUpcoming's own branches:
a repeating alarm skips that occurrence, a one-shot is switched off.

The notices are synced at the end of every reschedule, after the real
registration, and a third AlarmManager slot on its own request code
wakes when the next window opens. The sync is wrapped so a notification
failure can never stop a ring. Dismiss names the occurrence and is
refused once the alarm is ringing or has moved, so it can't get round
the dismiss challenge. No challenge is asked before the ring, as in
Google Clock.

Fixes #16
2026-10-21 20:01:21 +02:00