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
457 lines
22 KiB
XML
457 lines
22 KiB
XML
<?xml version="1.0" encoding="utf-8"?>
|
||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||
xmlns:tools="http://schemas.android.com/tools">
|
||
|
||
<!--
|
||
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" />
|
||
<!-- Enables the system's Live Update and status bar chip for a running timer. -->
|
||
<uses-permission android:name="android.permission.POST_PROMOTED_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" />
|
||
|
||
<!-- The stopwatch's foreground service. specialUse, not systemExempted:
|
||
that type's justification is an app with an exact-alarm permission
|
||
continuing alarm functionality in the background, and a stopwatch
|
||
schedules nothing, wakes nothing and rings nothing. Claiming it would
|
||
have the app tell the platform something untrue (M7 D3). -->
|
||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||
|
||
<!-- 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" />
|
||
|
||
<!-- The opt-in bedtime mode (issue #1): lists Clockula under Settings ›
|
||
Special app access › Do Not Disturb, where the user grants it by hand.
|
||
Without that grant nothing changes; with it, Clockula adds one Do Not
|
||
Disturb rule of its own, which always lets alarms through. -->
|
||
<uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" />
|
||
|
||
<!-- "Choose from a music app…": package visibility for the apps that share
|
||
a library through Android's standard MediaBrowserService, and for
|
||
nothing else. A visibility declaration, not a permission — and no
|
||
network: the music app streams with its own. -->
|
||
<queries>
|
||
<intent>
|
||
<action android:name="android.media.browse.MediaBrowserService" />
|
||
</intent>
|
||
</queries>
|
||
|
||
<application
|
||
android:name=".ClockulaApp"
|
||
android:allowBackup="true"
|
||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||
android:fullBackupContent="@xml/backup_rules"
|
||
android:icon="@mipmap/ic_launcher"
|
||
android:label="@string/app_name"
|
||
android:localeConfig="@xml/locales_config"
|
||
android:roundIcon="@mipmap/ic_launcher_round"
|
||
android:supportsRtl="true"
|
||
android:theme="@style/Theme.Clockula"
|
||
tools:targetApi="35">
|
||
<activity
|
||
android:name=".MainActivity"
|
||
android:exported="true"
|
||
android:launchMode="singleTop"
|
||
android:windowSoftInputMode="adjustResize">
|
||
<!-- The MAIN/LAUNCHER entry lives on the two <activity-alias>
|
||
below, so the launcher icon can be switched at runtime. -->
|
||
</activity>
|
||
|
||
<!-- The launcher entry, once per icon: exactly one alias is enabled at a
|
||
time, toggled from Settings → Appearance → App icon
|
||
(LauncherIconManager). The bloom is the manifest default. -->
|
||
<activity-alias
|
||
android:name=".BloomIconAlias"
|
||
android:enabled="true"
|
||
android:exported="true"
|
||
android:icon="@mipmap/ic_launcher"
|
||
android:label="@string/app_name"
|
||
android:roundIcon="@mipmap/ic_launcher_round"
|
||
android:targetActivity=".MainActivity">
|
||
<intent-filter>
|
||
<action android:name="android.intent.action.MAIN" />
|
||
<category android:name="android.intent.category.LAUNCHER" />
|
||
</intent-filter>
|
||
</activity-alias>
|
||
<activity-alias
|
||
android:name=".PlainIconAlias"
|
||
android:enabled="false"
|
||
android:exported="true"
|
||
android:icon="@mipmap/ic_launcher_plain"
|
||
android:label="@string/app_name"
|
||
android:roundIcon="@mipmap/ic_launcher_plain_round"
|
||
android:targetActivity=".MainActivity">
|
||
<intent-filter>
|
||
<action android:name="android.intent.action.MAIN" />
|
||
<category android:name="android.intent.category.LAUNCHER" />
|
||
</intent-filter>
|
||
</activity-alias>
|
||
|
||
<!-- The one exported door for android.provider.AlarmClock: every other
|
||
app on the device drives Clockula through it. Guarded by the
|
||
caller's own SET_ALARM permission, which AlarmClock's documentation
|
||
asks for and which is protection-level `normal`, so a legitimate
|
||
caller is granted it at install and a drive-by startActivity from an
|
||
app that never declared an interest in alarms is kept out. The
|
||
permission is on *this* activity and not on MainActivity, where it
|
||
would also be checked against the Launcher (M9 D2, D3).
|
||
|
||
Two intent-filters, deliberately: a filter with a <data> element
|
||
never matches an intent without data, and one without never matches
|
||
an intent with it — and DISMISS_ALARM/DISMISS_TIMER may arrive
|
||
either way. android.intent.category.VOICE is *not* declared: it
|
||
advertises the VoiceInteractor follow-on flows, which v1 does not
|
||
implement.
|
||
|
||
No window: translucent, out of recents, no history, no task of its
|
||
own. The children carry explicit closing tags rather than being
|
||
self-closed, so the whole declaration reads as one block — which is
|
||
how ManifestRulesTest reads it too.
|
||
|
||
M9 requests no permission of its own: the one named below is a
|
||
requirement on the *caller*, never something Clockula asks for. -->
|
||
<activity
|
||
android:name=".interop.AlarmClockActivity"
|
||
android:exported="true"
|
||
android:permission="com.android.alarm.permission.SET_ALARM"
|
||
android:excludeFromRecents="true"
|
||
android:noHistory="true"
|
||
android:taskAffinity=""
|
||
android:theme="@style/Theme.Clockula.Invisible">
|
||
<intent-filter>
|
||
<action android:name="android.intent.action.SET_ALARM"></action>
|
||
<action android:name="android.intent.action.SET_TIMER"></action>
|
||
<action android:name="android.intent.action.SHOW_ALARMS"></action>
|
||
<action android:name="android.intent.action.SHOW_TIMERS"></action>
|
||
<action android:name="android.intent.action.DISMISS_ALARM"></action>
|
||
<action android:name="android.intent.action.DISMISS_TIMER"></action>
|
||
<action android:name="android.intent.action.SNOOZE_ALARM"></action>
|
||
<category android:name="android.intent.category.DEFAULT"></category>
|
||
</intent-filter>
|
||
<!-- The deeplink form: clockula://alarm/{id} and clockula://timer/{id},
|
||
which the contract lets a caller carry as the intent's data. -->
|
||
<intent-filter>
|
||
<action android:name="android.intent.action.DISMISS_ALARM"></action>
|
||
<action android:name="android.intent.action.DISMISS_TIMER"></action>
|
||
<category android:name="android.intent.category.DEFAULT"></category>
|
||
<data android:scheme="clockula"></data>
|
||
</intent-filter>
|
||
</activity>
|
||
|
||
<!-- Standalone crash-report surface; MainActivity routes here on a
|
||
startup crash-loop. Not exported, kept out of recents. -->
|
||
<activity
|
||
android:name=".ui.crash.CrashReportActivity"
|
||
android:exported="false"
|
||
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 timer's ring screen: the alarm's window settings, opened by the
|
||
expired-timer notification's full-screen intent (issue #15). -->
|
||
<activity
|
||
android:name=".ui.ring.TimerRingActivity"
|
||
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" />
|
||
|
||
<!-- The timers: one foreground service covering both phases, the
|
||
countdown and the ring. systemExempted for the same reason the
|
||
alarm's ringing service is, and M6 adds **no** permission for it —
|
||
the foreground-service, wake-lock, vibrate and notification set
|
||
above already covers it (slice plan D6, D18). -->
|
||
<service
|
||
android:name=".timer.service.TimerService"
|
||
android:exported="false"
|
||
android:foregroundServiceType="systemExempted" />
|
||
|
||
<!-- The single expiry slot arriving. It carries no timer id: the fire is
|
||
an idempotent sweep (D5). Unexported, like the alarm's. -->
|
||
<receiver
|
||
android:name=".timer.receiver.TimerExpiryReceiver"
|
||
android:exported="false" />
|
||
|
||
<!-- The timer notification's buttons. Broadcasts rather than service
|
||
starts, so they work with the process dead (D16). -->
|
||
<receiver
|
||
android:name=".timer.receiver.TimerActionReceiver"
|
||
android:exported="false" />
|
||
|
||
<!-- The stopwatch: one foreground service, up while the run is not
|
||
idle. specialUse rather than systemExempted (M7 D3), and not
|
||
shortService, which caps at ~3 minutes against a stopwatch that may
|
||
run for hours. It holds no wake lock: there is no deadline to wake
|
||
for, and the elapsed value is derived from a persisted monotonic
|
||
anchor at read time. The <property> is API 31+ and ignored below. -->
|
||
<service
|
||
android:name=".stopwatch.service.StopwatchService"
|
||
android:exported="false"
|
||
android:foregroundServiceType="specialUse">
|
||
<property
|
||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||
android:value="stopwatch" />
|
||
</service>
|
||
|
||
<!-- The stopwatch notification's buttons. Broadcasts rather than service
|
||
starts, and they carry nothing: there is one stopwatch (M7 D16). -->
|
||
<receiver
|
||
android:name=".stopwatch.receiver.StopwatchActionReceiver"
|
||
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" />
|
||
<action android:name="android.app.action.SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED" />
|
||
</intent-filter>
|
||
</receiver>
|
||
|
||
<!-- The bedtime slot arriving (issue #1). Unexported, like the alarm's
|
||
and the timer's: only the PendingIntent the app made reaches it. -->
|
||
<receiver
|
||
android:name=".bedtime.receiver.BedtimeReceiver"
|
||
android:exported="false" />
|
||
|
||
<!-- 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
|
||
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
||
android:enabled="false"
|
||
android:exported="false">
|
||
<meta-data
|
||
android:name="autoStoreLocales"
|
||
android:value="true" />
|
||
</service>
|
||
|
||
<!-- The digital clock widget's per-instance style/date/world-clocks
|
||
picker (M11). Exported with only the APPWIDGET_CONFIGURE filter,
|
||
as the launcher requires to open it on placement and on "Edit
|
||
widget"; carries no extra data of its own beyond the app widget
|
||
id the launcher passes in. -->
|
||
<activity
|
||
android:name=".ui.widget.config.ClockWidgetConfigActivity"
|
||
android:exported="true"
|
||
android:theme="@style/Theme.Clockula">
|
||
<intent-filter>
|
||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||
</intent-filter>
|
||
</activity>
|
||
|
||
<!-- The analog clock widget's per-instance style picker (M11): the
|
||
same APPWIDGET_CONFIGURE contract as the digital widget's above. -->
|
||
<activity
|
||
android:name=".ui.widget.config.AnalogClockConfigActivity"
|
||
android:exported="true"
|
||
android:theme="@style/Theme.Clockula">
|
||
<intent-filter>
|
||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||
</intent-filter>
|
||
</activity>
|
||
|
||
<!-- The stopwatch widget's per-instance style picker: the same
|
||
APPWIDGET_CONFIGURE contract as the two above. -->
|
||
<activity
|
||
android:name=".ui.widget.config.StopwatchConfigActivity"
|
||
android:exported="true"
|
||
android:theme="@style/Theme.Clockula">
|
||
<intent-filter>
|
||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||
</intent-filter>
|
||
</activity>
|
||
|
||
<!-- The quick-timer widget's per-instance duration picker (M11): the
|
||
same APPWIDGET_CONFIGURE contract as the three above. -->
|
||
<activity
|
||
android:name=".ui.widget.config.QuickTimerConfigActivity"
|
||
android:exported="true"
|
||
android:theme="@style/Theme.Clockula">
|
||
<intent-filter>
|
||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||
</intent-filter>
|
||
</activity>
|
||
|
||
<!-- The next-alarm and timer widgets' per-instance colours: the same
|
||
APPWIDGET_CONFIGURE contract as the four above. -->
|
||
<activity
|
||
android:name=".ui.widget.config.NextAlarmConfigActivity"
|
||
android:exported="true"
|
||
android:theme="@style/Theme.Clockula">
|
||
<intent-filter>
|
||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||
</intent-filter>
|
||
</activity>
|
||
<activity
|
||
android:name=".ui.widget.config.TimerConfigActivity"
|
||
android:exported="true"
|
||
android:theme="@style/Theme.Clockula">
|
||
<intent-filter>
|
||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||
</intent-filter>
|
||
</activity>
|
||
|
||
<!-- The quick-timer widget's one tap: creates and starts a timer at
|
||
its configured duration. A broadcast, not a service start or an
|
||
activity, for the same reason as `TimerActionReceiver` (M11 D16). -->
|
||
<receiver
|
||
android:name=".timer.receiver.QuickTimerReceiver"
|
||
android:exported="false" />
|
||
|
||
<!-- The six home-screen widgets. Each is a GlanceAppWidgetReceiver,
|
||
and each must be exported with only the APPWIDGET_UPDATE filter —
|
||
that is how the launcher delivers a widget update, and the
|
||
platform requires exported="true" for a widget provider to be
|
||
pickable at all. No permission guards it, same as any other
|
||
app's widget: APPWIDGET_UPDATE is a platform-delivered broadcast,
|
||
never a door another app can open by naming the action itself.
|
||
updatePeriodMillis is 0 in every provider-info XML — WidgetSync
|
||
is the one refresh trigger for the data widgets, and the analog
|
||
widget's date is redrawn at midnight and on a clock or zone
|
||
change (widget/MidnightRefresh.kt). -->
|
||
<receiver
|
||
android:name=".widget.NextAlarmWidgetReceiver"
|
||
android:label="@string/widget_next_alarm_label"
|
||
android:exported="true">
|
||
<intent-filter>
|
||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||
</intent-filter>
|
||
<meta-data
|
||
android:name="android.appwidget.provider"
|
||
android:resource="@xml/widget_next_alarm_info" />
|
||
</receiver>
|
||
|
||
<receiver
|
||
android:name=".widget.TimerWidgetReceiver"
|
||
android:label="@string/widget_timer_label"
|
||
android:exported="true">
|
||
<intent-filter>
|
||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||
</intent-filter>
|
||
<meta-data
|
||
android:name="android.appwidget.provider"
|
||
android:resource="@xml/widget_timer_info" />
|
||
</receiver>
|
||
|
||
<receiver
|
||
android:name=".widget.ClockWidgetReceiver"
|
||
android:label="@string/widget_clock_label"
|
||
android:exported="true">
|
||
<intent-filter>
|
||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||
</intent-filter>
|
||
<meta-data
|
||
android:name="android.appwidget.provider"
|
||
android:resource="@xml/widget_clock_info" />
|
||
</receiver>
|
||
|
||
<receiver
|
||
android:name=".widget.AnalogClockWidgetReceiver"
|
||
android:label="@string/widget_analog_label"
|
||
android:exported="true">
|
||
<intent-filter>
|
||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||
</intent-filter>
|
||
<meta-data
|
||
android:name="android.appwidget.provider"
|
||
android:resource="@xml/widget_analog_clock_info" />
|
||
</receiver>
|
||
|
||
<receiver
|
||
android:name=".widget.StopwatchWidgetReceiver"
|
||
android:label="@string/widget_stopwatch_label"
|
||
android:exported="true">
|
||
<intent-filter>
|
||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||
</intent-filter>
|
||
<meta-data
|
||
android:name="android.appwidget.provider"
|
||
android:resource="@xml/widget_stopwatch_info" />
|
||
</receiver>
|
||
|
||
<receiver
|
||
android:name=".widget.QuickTimerWidgetReceiver"
|
||
android:label="@string/widget_quick_timer_label"
|
||
android:exported="true">
|
||
<intent-filter>
|
||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||
</intent-filter>
|
||
<meta-data
|
||
android:name="android.appwidget.provider"
|
||
android:resource="@xml/widget_quick_timer_info" />
|
||
</receiver>
|
||
</application>
|
||
|
||
</manifest>
|