feat(alarm): the ringing service, and never degrading to silence
A systemExempted foreground service holds the ring: audio focus, USAGE_ALARM, the volume ramp, vibration, and the ringtone URI. It goes foreground synchronously before it reads anything from the database, because the platform kills a service that reaches startForeground late — including on the path where there turns out to be no session to ring. The chain that matters is the one that never ends in silence. A full-screen intent when canUseFullScreenIntent() allows it; a high-priority heads-up notification that still rings when it does not; the device's default alarm ringtone when the configured URI will not open; and forced vibration when no audio source will play at all. Preparing a MediaPlayer is done off the main thread — a synchronous prepare at the moment the alarm fires is an ANR — and a player is released rather than orphaned on every failure path, so a dismissal cannot be outlived by the ring it cancelled. The ring screen here is plumbing and a placeholder: window flags to show over the lockscreen and turn the screen on. Its UI is M4. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,14 +3,44 @@
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<!--
|
||||
No permissions yet, deliberately. Clockula's permission set is the alarm
|
||||
engine's (USE_EXACT_ALARM, POST_NOTIFICATIONS, RECEIVE_BOOT_COMPLETED,
|
||||
USE_FULL_SCREEN_INTENT, FOREGROUND_SERVICE*) and it is declared in M3
|
||||
alongside the receivers and the ringing service that need it — so the
|
||||
manifest never claims a capability the code cannot yet honour. See
|
||||
docs/PLAN.md §4.
|
||||
Clockula's permission set is the alarm engine's, and nothing more. Each
|
||||
one is here because a specific path would otherwise fail silently — see
|
||||
docs/ARCHITECTURE.md §9 and docs/PLAN.md §4.
|
||||
-->
|
||||
|
||||
<!-- The exact-alarm ladder. USE_EXACT_ALARM is install-granted on 33+ for an
|
||||
app whose core function is alarms; SCHEDULE_EXACT_ALARM covers 31–32,
|
||||
where it is pre-granted, and is bounded there so the two never overlap.
|
||||
Below 31 an exact alarm needs no permission at all. -->
|
||||
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
|
||||
<uses-permission
|
||||
android:name="android.permission.SCHEDULE_EXACT_ALARM"
|
||||
android:maxSdkVersion="32" />
|
||||
|
||||
<!-- Re-register the next alarm after a reboot: AlarmManager keeps nothing. -->
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
|
||||
<!-- The ring screen over the lock screen. Denied, the ring degrades to a
|
||||
heads-up notification that still sounds — never to silence. -->
|
||||
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
|
||||
|
||||
<!-- The ring notification. Denied, the alarm still sounds: the audio belongs
|
||||
to the foreground service, not to the notification. -->
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<!-- The ringing service. systemExempted is Android's own answer for an app
|
||||
holding an exact-alarm permission that continues an alarm in the
|
||||
background, including a haptics-only one. -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" />
|
||||
|
||||
<!-- Keep the CPU awake for the length of a ring; the screen is the window's
|
||||
business, not this one's. -->
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
||||
<!-- The alarm vibrates, and vibrates on its own when no audio source opens. -->
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
|
||||
<application
|
||||
android:name=".ClockulaApp"
|
||||
android:allowBackup="true"
|
||||
@@ -42,6 +72,53 @@
|
||||
android:excludeFromRecents="true"
|
||||
android:launchMode="singleTask" />
|
||||
|
||||
<!-- The ring screen. Shown over the lock screen with the display turned
|
||||
on; excluded from recents so a dismissed alarm leaves nothing
|
||||
behind. M4 replaces its contents, not its declaration. -->
|
||||
<activity
|
||||
android:name=".ui.ring.AlarmRingActivity"
|
||||
android:exported="false"
|
||||
android:excludeFromRecents="true"
|
||||
android:launchMode="singleTask"
|
||||
android:showOnLockScreen="true"
|
||||
android:showWhenLocked="true"
|
||||
android:taskAffinity=""
|
||||
android:turnScreenOn="true" />
|
||||
|
||||
<!-- The ring itself. systemExempted per D17: not mediaPlayback (an alarm
|
||||
is not the user's media session) and emphatically not shortService,
|
||||
which caps at ~3 minutes against a 10-minute ring window. -->
|
||||
<service
|
||||
android:name=".alarm.ring.AlarmRingService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="systemExempted" />
|
||||
|
||||
<!-- The two AlarmManager slots arriving. Unexported: a PendingIntent the
|
||||
app created is delivered regardless, and nothing else may fake a fire. -->
|
||||
<receiver
|
||||
android:name=".alarm.receiver.AlarmFireReceiver"
|
||||
android:exported="false" />
|
||||
|
||||
<!-- The notification's Snooze and Dismiss buttons. -->
|
||||
<receiver
|
||||
android:name=".alarm.receiver.AlarmActionReceiver"
|
||||
android:exported="false" />
|
||||
|
||||
<!-- Protected system broadcasts are delivered to unexported receivers —
|
||||
this is how androidx.work declares its own RescheduleReceiver.
|
||||
LOCKED_BOOT_COMPLETED is deliberately absent: the database and the
|
||||
preferences live in credential-encrypted storage. -->
|
||||
<receiver
|
||||
android:name=".alarm.receiver.SystemEventReceiver"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<action android:name="android.intent.action.TIME_SET" />
|
||||
<action android:name="android.intent.action.TIMEZONE_CHANGED" />
|
||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<!-- Persists the per-app language on API < 33, where the platform
|
||||
per-app-languages API is unavailable. On 33+ this is a no-op. -->
|
||||
<service
|
||||
|
||||
Reference in New Issue
Block a user