Files
clockula/app/src/main/AndroidManifest.xml
T
makiolaj 19ee678a1b feat(widgets): let each widget pick its theme and wallpaper colours
Every widget instance can now follow the system or be pinned to Light or
Dark, and on Android 12+ turn the wallpaper's colours off in favour of
the app's own palette. Both are set in the widget's config screen and
stored in its own Glance state; an instance without them keeps today's
look.

Glance content takes them through WidgetTheme. The plain-RemoteViews
parts (the analog and digital faces, readouts and chronometers) go
through a new WidgetPalette, which resolves the widget_* palette under a
forced day/night and sets concrete colours, or day/night pairs, on the
views. The app's own tones are now widget_app_*, so they stay reachable
on API 31+. The analog and digital clocks are re-coloured on API 31+
only: below that AnalogClock's hands and a shape's background tint can't
be set through RemoteViews, so their config screens don't offer it.

The next-alarm and timer widgets gain a config screen for this
(configuration_optional, so placing one doesn't stop there). The analog,
stopwatch and quick-timer config screens become settings screens with an
OK, like the digital clock's, since they now hold more than one choice.

Background opacity is left out: most faces are a shape with text in its
container colour rather than a card, so there is no background to fade
without losing contrast against the wallpaper.

Closes #6
2026-10-05 19:48:59 +02:00

432 lines
21 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" />
<!-- "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 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 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/AnalogDateRefresh.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>