Files
clockula/app/src/main/AndroidManifest.xml
T
makiolaj fb81bfbf2e feat(widgets): next alarm, timer and clock home-screen widgets
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.
2026-10-01 22:32:07 +02:00

280 lines
14 KiB
XML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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>