feat(interop): one exported door for the whole AlarmClock contract

`AlarmClockActivity` is the only exported surface that takes a platform intent,
guarded by the caller-side `SET_ALARM` permission — which cannot go on
`MainActivity`, because the system checks it against the Launcher too. It is
invisible, does its work, and finishes.

Two intent-filters, not one: a filter carrying a `<data>` element never matches
an intent without data, so the deeplink actions need a filter of their own.
`IntentExtrasReader` turns the `Bundle` into a plain map and guards the
unparcel, because an exported door is reachable by anything on the device and a
hostile Parcelable must not crash the launch.
This commit is contained in:
2026-09-23 11:02:06 +02:00
parent 15dfaf300b
commit e55f3d19d4
10 changed files with 1258 additions and 0 deletions
+51
View File
@@ -71,6 +71,57 @@
</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