Commit Graph
182 Commits
Author SHA1 Message Date
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 6aa046515c Merge branch 'docs/27-wear-os-note' into release/v1.0.0 2026-10-26 20:47:36 +01:00
makiolaj 9c9b6171e0 Merge branch 'fix/14-review' into release/v1.0.0 2026-10-26 20:47:36 +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 bdcf780028 docs(roadmap): rewrap the Wear OS note
Refs #27
2026-10-22 11:15:52 +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 2927585608 docs(roadmap): name both data paths and F-Droid's real response
Refs #27
2026-10-22 11:13:55 +02:00
makiolaj b8968f5e19 docs(roadmap): settle the Wear OS note on what the docs say
Keep §5's own claim that only crash reports leave the phone, say the
dependency would ship in every channel, and keep the standalone app's
distribution caveat.

Refs #27
2026-10-22 11:12:18 +02:00
makiolaj dc23618c56 docs(roadmap): drop the AlarmClock route from the Wear OS note
AlarmClock intents only reach apps on the same device and can't read
state, so they are no route for a watch. Cite PRIVACY.md §4 and §5
together and say "no product flavours" precisely.

Refs #27
2026-10-22 11:10:38 +02:00
makiolaj da16ab23cc docs(roadmap): quote the policy and list the AlarmClock route for Wear OS
Cite PRIVACY.md's own wording, name F-Droid's non-free-dependency rule,
and note that third-party watch apps can already drive Clockula through
the AlarmClock intents.

Refs #27
2026-10-22 11:08:53 +02:00
makiolaj 99c052f94c docs(roadmap): state the Wear OS blocker against the policy as written
PRIVACY.md already documents Auto Backup and crash reports as ways data
can leave the phone, and there is a single build rather than an F-Droid
one. Say that, and call the entry blocked rather than unscoped.

Refs #27
2026-10-22 11:07:07 +02:00
makiolaj fe1c2a3d48 docs(roadmap): reduce the Wear OS note to the decision it needs
Any phone-to-watch channel sends data off the phone, which the privacy
policy rules out today, so the entry now states that decision and the
standalone alternative without speculating about notification bridging.

Refs #27
2026-10-22 11:05:53 +02:00
makiolaj cb5cd5123b docs(roadmap): keep the Wear OS note to what holds
Ringing notifications are foreground-service notifications, so making
them non-ongoing wouldn't bridge them; CompanionDeviceManager carries no
data; and watches pair through their makers' apps. Say only what holds
and note that a watch build has no F-Droid channel.

Refs #27
2026-10-22 11:03:44 +02:00
makiolaj 0213af1260 docs(roadmap): correct what a watch gets today
Ring and timer notifications are ongoing, which Wear OS doesn't bridge,
and bridging needs Google's Wear OS app anyway. Quote the privacy policy
exactly, drop the unchecked "only channel" claim, and list bridging the
ring as an option of its own.

Refs #27
2026-10-22 11:01:02 +02:00
makiolaj 95bb2d14ec docs(roadmap): record why a Wear OS companion is blocked on Play Services
A companion needs Google's Wearable Data Layer, which is a proprietary
service the privacy policy and the F-Droid build rule out. The roadmap
entry now records that constraint, what the bridged notifications
already give a watch, and the options if it is picked up.

Refs #27
2026-10-22 10:59:35 +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