fix(alarm): fail-safe alarm system overhaul (SDD alarm-system-overhaul, slice A)
Root-cause fix for the unstoppable-alarm incident (alarm rang 15 minutes, only uninstall silenced it) plus systematic hardening of every stop path. Native (Kotlin): - Verified stop: stopActiveAlarm now derives its result from the real post-teardown state (companion instance + synchronous stopEverything + activeRingingId check) instead of reporting unconditional success. - Atomic teardown: every stop path (stop action, notification button, snooze, missed, onDestroy, startForeground failure) funnels through one stopEverything() covering audio, wakelock, notification, foreground state and firing-record cleanup; player.release() guarded. - Bounded ringing: 10-minute auto-silence armed via AlarmManager fires a FIRED->MISSED transition with a localized missed-alarm notification; repeating alarms keep their native rearm, deleted alarms never produce ghost MISSED notifications. - Durable firing record with onStartCommand re-validation (resurrection guard) and boot-time stale cleanup; firing records cleared on every refuse/mismatch/cancel path. - New notification-only dismissal channel (dismissAlarmNotificationOnly) so UI-level dedup can never kill a live ring's audio. Flutter (Dart): - Stop/disable/edit/delete of a ringing alarm always attempt to silence it; on native-query failure the stop falls back toward silence via the id-scoped legacy stop. - Verified-stop results surface failures: the ringing screen keeps dismiss-by-design on success, but on a verified failure it stays up with a persistent force-stop banner (guarded against double-dismiss) and auto-dismisses if the ring ends externally (missed/notification). - Missed events sync alarm bookkeeping without opening the ringing UI. - 4 new l10n keys translated across all 13 locales (ARB guard green). 550 tests green, analyzer clean. Reviewed in 3 adversarial 4-lens rounds (2 deterministic + 1 refuter-corroborated critical fixed); formal gentle-ai receipt waived by maintainer authorization (correction scope legitimately exceeded the frozen genesis paths). On-device QA checklist in openspec/changes/alarm-system-overhaul/tasks.md pending before archive.
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
# Alarm Platform Contract — Android Alarm Reliability Reference (mid-2026)
|
||||
|
||||
Scope: authoritative rules for building a bulletproof Android alarm clock, targeting Android 14/15/16-era devices (targetSdk 35/36). Grounds the `alarm-system-overhaul` behavior spec and refactor. Repo context checked: `android/app/src/main/AndroidManifest.xml` declares `SCHEDULE_EXACT_ALARM`+`USE_EXACT_ALARM` (redundant pairing, see Rule 3), `USE_FULL_SCREEN_INTENT`, `POST_NOTIFICATIONS`, `FOREGROUND_SERVICE_MEDIA_PLAYBACK`+`FOREGROUND_SERVICE_SYSTEM_EXEMPTED`; has custom native `.PluriWaveAlarmService` (`foregroundServiceType=mediaPlayback|systemExempted`), `.PluriWaveAlarmReceiver`, `.PluriWaveBootReceiver` (handles LOCKED_BOOT_COMPLETED/BOOT_COMPLETED/USER_UNLOCKED/MY_PACKAGE_REPLACED/TIME_SET/TIMEZONE_CHANGED/SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED — good coverage of reschedule triggers) — AND a separate `com.ryanheise.audioservice.AudioService` (Flutter `audio_service` plugin, used for radio streaming). Two independent audio/media-session owners in one app is the most likely root cause of the app's documented incident (alarm rang 15 min, only uninstall stopped it).
|
||||
|
||||
## 1. Scheduling
|
||||
|
||||
1.1 Use `AlarmManager.setAlarmClock()` for user-facing alarm-clock semantics, not `setExactAndAllowWhileIdle()`. `setAlarmClock()` is the highest-priority alarm type: the system exits Doze/App Standby to deliver it and never defers it, and it shows the alarm-clock icon in the status bar. `setExactAndAllowWhileIdle()` is "nearly precise" and intended for non-user-visible exact work, not primary alarm firing. [Android AlarmManager docs](https://developer.android.com/develop/background-work/services/alarms/schedule) — all API levels, but the precision distinction matters most from Android 6 (Doze) onward.
|
||||
|
||||
1.2 On Android 12+ (API 31+), declare `SCHEDULE_EXACT_ALARM` and call `AlarmManager.canScheduleExactAlarms()` before scheduling; a `SecurityException` is thrown otherwise. On Android 14 (API 34), this permission is **no longer pre-granted on fresh installs** — apps must send the user to `ACTION_REQUEST_SCHEDULE_EXACT_ALARM` with an in-app rationale first. [Android 14 behavior change](https://developer.android.com/about/versions/14/changes/schedule-exact-alarms)
|
||||
|
||||
1.3 `USE_EXACT_ALARM` (Android 13+/API 33+) is an install-time-granted, non-revocable-by-user permission but is restricted by Play Store policy to apps whose **core function** is alarms or calendars. Declaring BOTH `SCHEDULE_EXACT_ALARM` and `USE_EXACT_ALARM` in the same manifest (as this app currently does) is redundant and risky: Play may reject `USE_EXACT_ALARM` if core-functionality review fails, silently falling back to the revocable permission — the app must not assume `USE_EXACT_ALARM` guarantees a grant. [Schedule alarms guide](https://developer.android.com/develop/background-work/services/alarms/schedule)
|
||||
|
||||
1.4 Register a receiver for `AlarmManager.ACTION_SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED` and, on receipt, re-check `canScheduleExactAlarms()` and reschedule/rebuild every pending alarm instance. Do not trust a cached permission flag — the system can revoke exact-alarm permission automatically (e.g. long-unused apps) and this broadcast is the only signal. Repo already listens for this action in `PluriWaveBootReceiver` — verify the handler actually reschedules rather than just logging.
|
||||
|
||||
1.5 Reschedule ALL alarms on: `BOOT_COMPLETED` (+ `LOCKED_BOOT_COMPLETED` for direct-boot-aware alarms), `MY_PACKAGE_REPLACED` (app update wipes AlarmManager state), `TIME_SET`, `TIMEZONE_CHANGED`. Missing any of these is a classic bug class ("alarm survived reboot but not timezone change"). Repo's `PluriWaveBootReceiver` already covers all four — confirm the Kotlin implementation actually recomputes trigger times rather than re-arming stale timestamps.
|
||||
|
||||
1.6 If exact-alarm permission is denied, fall back to `setAndAllowWhileIdle()`/`setWindow()`, never silently drop the alarm — and surface a persistent "exact alarms disabled, alarm may be late" warning in-app, mirroring AOSP DeskClock's user-facing permission nags.
|
||||
|
||||
## 2. Firing / Ringing (native-only responsibility)
|
||||
|
||||
2.1 Canonical pattern: `AlarmManager` fires a `PendingIntent` → **BroadcastReceiver.onReceive()** (short-lived, <10s budget) → immediately calls `Context.startForegroundService()` (or `startForeground()` from the service within 5s per Android 8+ FGS rules) → the **Foreground Service** owns the wake lock, the ringtone/media player, and vibration for the entire ringing lifetime. This exact chain is what AOSP DeskClock's `AlarmService` + `AlarmActivity` do, and what the `alarm` (gdelataillade) Flutter plugin's native Android layer does — audio ownership lives in native Kotlin/Java, never in a Dart isolate, because Dart isolates are not guaranteed to be alive when the alarm fires. [Alarm plugin Android install guide](https://github.com/gdelataillade/alarm/blob/main/help/INSTALL-ANDROID.md)
|
||||
|
||||
2.2 Foreground service type: use `mediaPlayback` (Android 10+ requirement) — AOSP DeskClock and the `alarm` plugin both use a media-playback-typed FGS for the ringing service. `FOREGROUND_SERVICE_SYSTEM_EXEMPTED` alone is not a substitute; this app's `PluriWaveAlarmService` already declares `mediaPlayback|systemExempted`, which is correct, but the app must ensure exact-alarm-triggered FGS starts are exempt from the Android 12+ background-start restrictions (they are, by design, per platform docs: exact alarms are excluded from FGS-from-background limits).
|
||||
|
||||
2.3 Do NOT rely on the audio system's implicit wake lock alone for anything beyond audio — the FGS itself must hold `PARTIAL_WAKE_LOCK` (`WAKE_LOCK` permission, already declared) for any non-audio work (vibration loop, timeout logic, UI signaling) and must release it deterministically in every stop path (`onDestroy`, explicit stop, timeout). When only `MediaPlayer`/`ExoPlayer` plays audio with `AudioAttributes.USAGE_ALARM`, the audio framework manages its own wake lock for the playback itself — but application-level state transitions still need an explicit lock if the CPU could otherwise sleep between them. [Background work: wake locks](https://developer.android.com/develop/background-work/background-tasks/awake/wakelock/identify-wls)
|
||||
|
||||
2.4 Play ringtone via `MediaPlayer`/`ExoPlayer`/`Ringtone` configured with `AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_ALARM).setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)`. This ties playback to the **alarm** audio stream (`STREAM_ALARM`), which (a) bypasses Do Not Disturb by default — Android treats alarm-stream audio as a high-priority interruption that sounds regardless of DND unless the user explicitly disabled "Alarms" under DND exceptions — and (b) uses the alarm volume slider, not media/ring volume. **This is the single highest-value correctness rule for a hybrid Flutter+native app**: if the alarm ever plays through the `audio_service`/`just_audio` media-session pipeline (music/media stream) instead of a dedicated `STREAM_ALARM`/`USAGE_ALARM` player, it will (a) respect DND media-silencing, (b) follow media volume (can be 0), and (c) fight the radio-streaming engine for the same media session — a plausible root cause of "two engines playing / can't stop" incidents.
|
||||
|
||||
2.5 Auto-silence: industry-standard timeout is a bounded window (AOSP DeskClock uses a configurable timeout, default historically ~15 minutes, now user-configurable "Silence after" 1–30 min or "Never"); after timeout, stop audio/vibration, transition to a "missed alarm" notification, and — for repeating alarms — compute and arm the next occurrence. Never let the ringing service loop indefinitely with no upper bound; an unbounded loop is exactly the "rang 15 minutes, uninstall required" failure mode. [AOSP DeskClock `AlarmStateManager`](https://github.com/LineageOS/android_packages_apps_DeskClock/blob/89aae7601d0bc17bf5f6e89f5a0b919a184256ae/src/com/android/deskclock/alarms/AlarmStateManager.java)
|
||||
|
||||
## 3. UI over lock screen
|
||||
|
||||
3.1 Android 14+ (API 34): `USE_FULL_SCREEN_INTENT` becomes a **special app access** permission auto-granted by Play only to apps whose core function is calls or alarms; otherwise it must be requested via `ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT` and checked with `NotificationManager.canUseFullScreenIntent()`. An alarm-clock app qualifies for default grant, but must still runtime-check the flag (Play/OEM can still revoke) and have a non-FSI fallback (heads-up high-priority notification) if denied. [source.android.com FSI limits](https://source.android.com/docs/core/permissions/fsi-limits) — effective policy date May 31 2024 / enforced from Jan 22 2025.
|
||||
|
||||
3.2 Notification channel for ringing must be `IMPORTANCE_HIGH` (or `IMPORTANCE_MAX` where the platform still exposes it) with `setSound(null, ...)` — the CHANNEL must not play its own sound because the foreground service is already playing the alarm tone; a channel sound + service audio together is a double-audio bug. Set `category = CATEGORY_ALARM`. Attach `setFullScreenIntent(pendingIntent, true)` pointing at a dedicated full-screen Activity, and `setOngoing(true)`.
|
||||
|
||||
3.3 The full-screen ringing Activity must call `setShowWhenLocked(true)` + `setTurnScreenOn(true)` (API 27+) instead of the deprecated window flags, matching this app's `MainActivity` manifest attributes — but the ringing UI should be its OWN activity (not `MainActivity`) with `android:excludeFromRecents`, `singleInstance`/`singleTask`, so it can be shown/dismissed independently of app navigation state.
|
||||
|
||||
3.4 Android 12+ (API 31+) notification trampoline restriction: Stop/Snooze notification action buttons must be `PendingIntent.getForegroundService()` or `PendingIntent.getBroadcast()` directly — **never** a broadcast/service that itself calls `startActivity()`. Any notification action that needs to show UI after tapping must build a `PendingIntent` pointing straight at the destination Activity; do not chain through a receiver-that-launches-activity ("trampoline"), which is blocked and produces silent no-ops or logcat-only failures on Android 12+. [Notification trampoline restrictions](https://developer.android.com/about/versions/12/behavior-changes-12)
|
||||
|
||||
## 4. Stop/Snooze correctness (single source of truth)
|
||||
|
||||
4.1 There must be exactly ONE process/component that owns "is the alarm currently ringing" state and exactly ONE component that owns the audio player instance. In a Flutter app, this MUST be native Kotlin (the foreground service), not Dart — Dart/Flutter engine lifecycle (background isolates, `audio_service`'s Flutter-side handlers) is not guaranteed to be running or reachable when the user taps Stop from a lock-screen notification while the main Flutter engine is not attached. All Stop/Snooze `PendingIntent`s must route to the SAME native receiver/service that started the ringing, using a stable, unique request-code/alarm-instance-id so the intent unambiguously identifies the alarm to stop — never a broadcast that "maybe" reaches Dart via a plugin channel.
|
||||
|
||||
4.2 Foreground service must NOT use `START_STICKY` for the ringing service if that causes the OS to resurrect it after being killed with no user-visible way to stop it. If restart-on-kill is desired for reliability, the restarted service must re-check persisted "is this alarm still supposed to be ringing" state (e.g., a stop-timestamp or dismissed flag in SharedPreferences/DB) on `onStartCommand` and self-terminate immediately if the alarm was already dismissed/stopped — otherwise a killed-and-restarted service can resume playing after the user already stopped it, exactly matching "un-dismissable alarm" reports. [START_STICKY restart discussion](https://issuetracker.google.com/issues/36986292)
|
||||
|
||||
4.3 Notification cancellation and service lifecycle must be coupled atomically: cancelling/removing the notification does NOT stop a foreground service, and stopping the service without cancelling the notification leaves a stuck notification. Every stop path (user tap, timeout, snooze) must call BOTH `stopForeground(STOP_FOREGROUND_REMOVE)` (or equivalent) AND `stopSelf()`, plus release the wake lock, in one atomic method — never rely on the notification's own dismissal to imply the service stopped.
|
||||
|
||||
4.4 If audio is ever routed through a second engine (e.g., `audio_service`'s `AudioService`/media session, used here for radio streaming) instead of the alarm-ringing native player, the Stop action must explicitly stop/abandon audio focus and release that engine too — a stop path that only signals the native alarm service while a second, independently-running audio engine continues playing is a documented Flutter/plugin-interaction failure mode (`audio_service` + background alarm plugin conflicts are reported upstream). [audio_service background-alarm issue](https://github.com/ryanheise/audio_service/issues/704)
|
||||
|
||||
4.5 Handle process death mid-ring: persist "alarm X is currently firing since T" to durable storage (not just in-memory) BEFORE starting playback, and clear it only on a confirmed stop. On any service restart/reboot, if a "firing" record has no matching stop record and its age exceeds the auto-silence timeout, treat it as missed and clean up rather than resuming indefinitely.
|
||||
|
||||
## 5. Reference state machine (AOSP DeskClock `AlarmStateManager`)
|
||||
|
||||
States: `SILENT_STATE` → `LOW_NOTIFICATION_STATE` → `HIGH_NOTIFICATION_STATE` → `FIRED_STATE` → (`SNOOZE_STATE` | `MISSED_STATE`) → `DISMISSED_STATE`, plus a transient `HIDE_NOTIFICATION_STATE`.
|
||||
- SILENT → LOW_NOTIFICATION: scheduled ahead-of-time via `setSilentState()`/`scheduleInstanceStateChange()`.
|
||||
- LOW → HIGH_NOTIFICATION: automatic at `getHighNotificationTime()`; HIGH cannot be user-hidden (only dismiss/snooze).
|
||||
- HIGH → FIRED: at the actual alarm trigger time; `setFiredState()` arms a timeout via `scheduleInstanceStateChange(context, timeout, instance, MISSED_STATE)`.
|
||||
- FIRED → SNOOZE: user action, increments instance trigger time and re-arms.
|
||||
- FIRED → MISSED: automatic on timeout expiry (this IS the auto-silence mechanism).
|
||||
- MISSED → (delete | disable | reschedule next occurrence): `updateParentAlarm()` — one-shot alarms with "delete after use" are deleted, others disabled; repeating alarms create the next instance via `createInstanceAfter()`.
|
||||
- Any state → DISMISSED: final, deletes the instance and checks the parent alarm for rescheduling.
|
||||
[AOSP DeskClock AlarmStateManager, LineageOS mirror](https://github.com/LineageOS/android_packages_apps_DeskClock/blob/89aae7601d0bc17bf5f6e89f5a0b919a184256ae/src/com/android/deskclock/alarms/AlarmStateManager.java)
|
||||
|
||||
Division of responsibility to replicate: the STATE MACHINE (scheduling transitions, timeout arming, missed/dismiss bookkeeping) can be modeled in Dart/domain layer as long as it only computes *when* to transition — but the ACTUAL transition execution at fire time (starting the FGS, playing audio, holding the wake lock, posting the FSI notification, handling Stop/Snooze taps) MUST be native Kotlin, invoked directly from the BroadcastReceiver, independent of whether the Flutter engine is attached.
|
||||
|
||||
Contrast with Fossify/Simple-Clock's simpler model: their `AlarmReceiver` does NOT use a foreground service at all when screen is off — it posts a high-importance full-screen notification and launches a `ReminderActivity` directly, which owns audio playback itself while visible. This is simpler but riskier (no dedicated FGS-held wake lock across the whole ring duration); AOSP DeskClock's FGS-centric model is the more robust reference for a "wakes reliably from any state" alarm. [Simple-Clock AlarmReceiver](https://github.com/SimpleMobileTools/Simple-Clock/blob/master/app/src/main/kotlin/com/simplemobiletools/clock/receivers/AlarmReceiver.kt)
|
||||
|
||||
## 6. Flutter-specific plugin architecture notes
|
||||
|
||||
6.1 `android_alarm_manager_plus`: creates its own background `FlutterEngine` to run a Dart callback on alarm fire. Documented limitations: does not manage `SCHEDULE_EXACT_ALARM` permission at all (app must handle it); can crash when combined with other plugins that also spin up background engines/services (conflicts with `audio_service`-style plugins); callback reliability degrades when the app process was fully killed. **Verdict: not suitable as the sole mechanism for ringing UI/audio — at most usable for lightweight rescheduling logic, never for owning playback.** [plus_plugins issue #266](https://github.com/fluttercommunity/plus_plugins/issues/266)
|
||||
|
||||
6.2 `alarm` (gdelataillade): purpose-built alarm plugin that keeps ALL ringing responsibility (foreground service, `NotificationOnKillService` safety net, audio, vibration, volume) in native Kotlin, exposing only schedule/cancel/stream-of-events to Dart. This is the correct architectural split for a Flutter alarm app and is the closer analog to what this app's custom `PluriWaveAlarmService`/`PluriWaveAlarmReceiver` should converge toward — provided the alarm audio path is fully decoupled from `audio_service`'s media session. [alarm plugin Android install docs](https://github.com/gdelataillade/alarm/blob/main/help/INSTALL-ANDROID.md)
|
||||
|
||||
## 7. Notifications
|
||||
|
||||
7.1 `POST_NOTIFICATIONS` runtime permission (Android 13+/API 33) must be requested before posting any notification, including the ringing FSI notification — if denied, the FSI/heads-up path degrades to nothing being shown, so the app needs an in-app fallback warning.
|
||||
7.2 Alarm channel: `IMPORTANCE_HIGH`, `sound = null` (service owns audio), `CATEGORY_ALARM`, `setOngoing(true)` while ringing, action buttons wired to direct broadcast/service `PendingIntent`s (never trampolines — see 3.4).
|
||||
7.3 Android 16 "Notification Cooldown" (gradual volume reduction for bursty same-app notifications) explicitly **does not apply to calls, alarms, priority conversations, or emergency alerts** — but this exemption is presumably keyed off `CATEGORY_ALARM`/correct channel classification, so mis-categorized alarm notifications could incorrectly get cooled down. Classify correctly. [Android 16 notification cooldown](https://www.computerworld.com/article/3609666/android-16-notification-cooldown.html)
|
||||
7.4 Update (not re-post) the ringing notification via a stable notification ID per alarm instance; re-posting can retrigger heads-up/FSI unexpectedly and complicates the "notification removed but service alive" desync bug (Rule 4.3).
|
||||
|
||||
## 8. Failure-mode checklist (must survive)
|
||||
|
||||
- [ ] Process death mid-ring (service killed by LMK/OEM) — state persisted, restart re-validates before resuming (4.2, 4.5).
|
||||
- [ ] Device reboot — `BOOT_COMPLETED`/`LOCKED_BOOT_COMPLETED` reschedules every pending alarm from durable storage (1.5).
|
||||
- [ ] Doze / App Standby — `setAlarmClock()` used, not a type that Doze can defer (1.1); FGS start from exact alarm is exempt from BG-start restrictions (2.2).
|
||||
- [ ] SCHEDULE_EXACT_ALARM revoked by user/system mid-session — `ACTION_SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED` handled, fallback scheduling used, user warned (1.4, 1.6).
|
||||
- [ ] Locale/timezone change — `TIME_SET`/`TIMEZONE_CHANGED` recompute all trigger times (1.5).
|
||||
- [ ] App update — `MY_PACKAGE_REPLACED` reschedules (1.5).
|
||||
- [ ] Media/ring volume at 0 — irrelevant if alarm audio correctly uses `STREAM_ALARM`/`USAGE_ALARM` (2.4); verify alarm volume specifically, not device master volume.
|
||||
- [ ] DND enabled — alarm-stream audio bypasses DND by default; verify the app is not accidentally routing through a stream DND does silence (2.4).
|
||||
- [ ] Battery optimization / not in Doze-exemption whitelist — regular (non-`setAlarmClock`) alarms are deferred under Doze; either use `setAlarmClock()` (exempt) or guide the user through `ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` (1.1, repo already declares `REQUEST_IGNORE_BATTERY_OPTIMIZATIONS`).
|
||||
- [ ] Two audio engines racing (native alarm player vs. `audio_service` media session) — single source of truth for "is ringing" and for the player instance; Stop must tear down both if both can ever be active (4.1, 4.4) — **highest-priority item given this app's dual audio-engine manifest**.
|
||||
- [ ] Notification dismissed/swiped but service still alive, or vice versa — coupled stop path (4.3).
|
||||
- [ ] Full-screen intent permission denied/revoked (Android 14+ policy or user revocation) — `canUseFullScreenIntent()` checked, heads-up fallback exists (3.1).
|
||||
- [ ] Notification trampoline blocked (Android 12+) — all action `PendingIntent`s go straight to service/broadcast or directly to an Activity, never chained (3.4).
|
||||
- [ ] Auto-silence/missed-alarm path — bounded ring duration, transition to MISSED, repeating alarms rearm next occurrence (2.5, state machine section 5).
|
||||
|
||||
## Top 10 rules this Flutter app is most likely violating (given dual native-service + audio_service architecture)
|
||||
|
||||
1. Alarm ringing audio possibly not exclusively on `STREAM_ALARM`/`USAGE_ALARM` if it shares code paths with `audio_service`'s media-session player used for radio (Rule 2.4) — needs verification.
|
||||
2. Two independent audio/media engines (`PluriWaveAlarmService` native + `com.ryanheise.audioservice.AudioService`) with no single documented "who owns ringing audio" contract (Rule 4.1, 4.4) — matches the reported incident signature.
|
||||
3. Redundant `SCHEDULE_EXACT_ALARM` + `USE_EXACT_ALARM` declaration without a documented fallback if Play strips `USE_EXACT_ALARM` core-functionality grant (Rule 1.3).
|
||||
4. Unclear whether Stop/Snooze notification actions route directly to service/broadcast `PendingIntent`s vs. any trampoline-like indirection (Rule 3.4) — needs code check.
|
||||
5. Unclear whether the ringing foreground service re-validates "already stopped" state on every `onStartCommand` (protection against `START_STICKY` resurrection) (Rule 4.2).
|
||||
6. Unclear whether notification cancellation and `stopForeground()`/`stopSelf()` are coupled atomically in every stop path (Rule 4.3).
|
||||
7. Unclear whether a durable "currently firing" record exists to recover deterministically from process death mid-ring (Rule 4.5).
|
||||
8. No confirmed bounded auto-silence timeout / missed-alarm transition (Rule 2.5) — an unbounded ring loop is the literal shape of the known incident.
|
||||
9. `ACTION_SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED` receiver exists in the manifest but its Kotlin handler's actual behavior (reschedule vs. log-only) is unverified (Rule 1.4).
|
||||
10. Full-screen intent fallback path (heads-up notification when FSI is denied/revoked) is unverified — app relies on `USE_FULL_SCREEN_INTENT` but Android 14+ can still revoke it via Play/OEM policy (Rule 3.1).
|
||||
|
||||
Note: items 2–9 require the codebase-focused sdd-explore pass (Kotlin source read) to confirm/refute; this document is the external-research half only.
|
||||
Reference in New Issue
Block a user