feat(stopwatch): the foreground service, its silent notification and the receiver

The service is alive exactly while the stopwatch is not idle, and it is
`specialUse` with the subtype spelled out — reusing `systemExempted` would
have the app claim it is continuing alarm functionality, which it is not.
No wake lock, ever: the notification carries the platform chronometer
counting up from a base derived when it is posted and never stored, so the
system draws the ticking and the process can sleep through it.

Its channel is `IMPORTANCE_LOW` because nothing here ever alerts. The Lap,
Pause, Resume and Reset buttons are broadcasts carrying no extras — the
receiver reads the stored run rather than trusting an intent — and the body
opens the Stopwatch tab. Boot, time change and package replacement all reach
the third engine now, alongside the other two.
This commit is contained in:
2026-09-22 10:23:44 +02:00
parent 7f8b2e79fc
commit b667e46877
6 changed files with 435 additions and 0 deletions
+28
View File
@@ -34,6 +34,13 @@
<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" />
@@ -126,6 +133,27 @@
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