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
432 lines
21 KiB
XML
432 lines
21 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" />
|
||
|
||
<!-- "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>
|