Three Glance widgets, mirroring state the app already computes rather than owning any of their own: the next alarm (AlarmEngine.upcoming()), the live pill's subject timer (same selection logic, no ticker of its own — a running countdown is the platform chronometer, exactly like the ring notification), and the local time growing to world clocks at larger sizes. WidgetSync collects the same read-only flows the shell already reads and redraws on change, event-driven (updatePeriodMillis = 0) — it touches no engine, only re-renders, and covers every write path including SystemEventReceiver's since the process is already alive whenever those run. Tapping a widget reuses the same internal navigation actions the AlarmClock contract's door already produces; the timer buttons send the same unexported broadcasts its notification does. A widget is just another caller of machinery that already exists. ManifestRulesTest is updated deliberately: three GlanceAppWidgetReceivers join the exported set, each pinned to exactly the platform-required APPWIDGET_UPDATE filter and no permission — no new permission is requested anywhere in this change.
280 lines
14 KiB
XML
280 lines
14 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" />
|
||
|
||
<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">
|
||
<intent-filter>
|
||
<action android:name="android.intent.action.MAIN" />
|
||
<category android:name="android.intent.category.LAUNCHER" />
|
||
</intent-filter>
|
||
</activity>
|
||
|
||
<!-- 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 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" />
|
||
</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
|
||
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
||
android:enabled="false"
|
||
android:exported="false">
|
||
<meta-data
|
||
android:name="autoStoreLocales"
|
||
android:value="true" />
|
||
</service>
|
||
|
||
<!-- The three M10 home-screen widgets (docs/PLAN.md §9). 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. -->
|
||
<receiver
|
||
android:name=".widget.NextAlarmWidgetReceiver"
|
||
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: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: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>
|
||
</application>
|
||
|
||
</manifest>
|