fix(alarm): fail-safe alarm system overhaul (SDD alarm-system-overhaul, slice A)
Build & Deploy PluriWave / Análisis de código (push) Successful in 25s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 2m25s

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:
Javier Bautista Fernández
2026-07-22 23:52:36 +02:00
parent 0f9a6a1719
commit 29f7d54e85
50 changed files with 2461 additions and 24 deletions
+24
View File
@@ -1694,6 +1694,30 @@ abstract class AppLocalizations {
/// **'Detener alarma'**
String get stopAlarmAction;
/// No description provided for @alarmStopFailedMessage.
///
/// In es, this message translates to:
/// **'No pudimos confirmar que la alarma se detuvo. Inténtalo de nuevo.'**
String get alarmStopFailedMessage;
/// No description provided for @alarmForceStopAction.
///
/// In es, this message translates to:
/// **'Forzar detención'**
String get alarmForceStopAction;
/// No description provided for @alarmMissedNotificationTitle.
///
/// In es, this message translates to:
/// **'Alarma perdida'**
String get alarmMissedNotificationTitle;
/// No description provided for @alarmMissedNotificationText.
///
/// In es, this message translates to:
/// **'{name} se silenció automáticamente después de 10 minutos.'**
String alarmMissedNotificationText(Object name);
/// No description provided for @pauseAction.
///
/// In es, this message translates to:
+15
View File
@@ -899,6 +899,21 @@ class AppLocalizationsAr extends AppLocalizations {
@override
String get stopAlarmAction => 'إيقاف المنبه';
@override
String get alarmStopFailedMessage =>
'تعذّر التأكد من إيقاف المنبه. حاول مرة أخرى.';
@override
String get alarmForceStopAction => 'إيقاف قسري';
@override
String get alarmMissedNotificationTitle => 'منبه فائت';
@override
String alarmMissedNotificationText(Object name) {
return 'تم كتم $name تلقائيًا بعد 10 دقائق.';
}
@override
String get pauseAction => 'إيقاف مؤقت';
+15
View File
@@ -908,6 +908,21 @@ class AppLocalizationsBn extends AppLocalizations {
@override
String get stopAlarmAction => 'অ্যালার্ম বন্ধ করুন';
@override
String get alarmStopFailedMessage =>
'অ্যালার্ম বন্ধ হয়েছে তা নিশ্চিত করা যায়নি। আবার চেষ্টা করুন।';
@override
String get alarmForceStopAction => 'জোর করে বন্ধ করুন';
@override
String get alarmMissedNotificationTitle => 'মিস হওয়া অ্যালার্ম';
@override
String alarmMissedNotificationText(Object name) {
return '১০ মিনিট পর $name স্বয়ংক্রিয়ভাবে নিঃশব্দ করা হয়েছে।';
}
@override
String get pauseAction => 'বিরতি দিন';
+15
View File
@@ -910,6 +910,21 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get stopAlarmAction => 'Alarm stoppen';
@override
String get alarmStopFailedMessage =>
'Wir konnten nicht bestätigen, dass der Alarm gestoppt wurde. Versuche es erneut.';
@override
String get alarmForceStopAction => 'Erzwungen stoppen';
@override
String get alarmMissedNotificationTitle => 'Verpasster Alarm';
@override
String alarmMissedNotificationText(Object name) {
return '$name wurde nach 10 Minuten automatisch stummgeschaltet.';
}
@override
String get pauseAction => 'Pausieren';
+15
View File
@@ -903,6 +903,21 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get stopAlarmAction => 'Stop alarm';
@override
String get alarmStopFailedMessage =>
'We couldn\'t confirm the alarm stopped. Try again.';
@override
String get alarmForceStopAction => 'Force stop';
@override
String get alarmMissedNotificationTitle => 'Missed alarm';
@override
String alarmMissedNotificationText(Object name) {
return '$name was silenced automatically after 10 minutes.';
}
@override
String get pauseAction => 'Pause';
+15
View File
@@ -907,6 +907,21 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get stopAlarmAction => 'Detener alarma';
@override
String get alarmStopFailedMessage =>
'No pudimos confirmar que la alarma se detuvo. Inténtalo de nuevo.';
@override
String get alarmForceStopAction => 'Forzar detención';
@override
String get alarmMissedNotificationTitle => 'Alarma perdida';
@override
String alarmMissedNotificationText(Object name) {
return '$name se silenció automáticamente después de 10 minutos.';
}
@override
String get pauseAction => 'Pausar';
+15
View File
@@ -913,6 +913,21 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get stopAlarmAction => 'Arrêter lalarme';
@override
String get alarmStopFailedMessage =>
'Nous navons pas pu confirmer larrêt de lalarme. Réessayez.';
@override
String get alarmForceStopAction => 'Forcer larrêt';
@override
String get alarmMissedNotificationTitle => 'Alarme manquée';
@override
String alarmMissedNotificationText(Object name) {
return '$name a été mise en sourdine automatiquement après 10 minutes.';
}
@override
String get pauseAction => 'Pause';
+15
View File
@@ -904,6 +904,21 @@ class AppLocalizationsHi extends AppLocalizations {
@override
String get stopAlarmAction => 'अलार्म रोकें';
@override
String get alarmStopFailedMessage =>
'हम पुष्टि नहीं कर सके कि अलार्म बंद हुआ। फिर से कोशिश करें।';
@override
String get alarmForceStopAction => 'जबरन बंद करें';
@override
String get alarmMissedNotificationTitle => 'छूटा हुआ अलार्म';
@override
String alarmMissedNotificationText(Object name) {
return '10 मिनट बाद $name अपने आप म्यूट कर दिया गया।';
}
@override
String get pauseAction => 'विराम दें';
+15
View File
@@ -908,6 +908,21 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get stopAlarmAction => 'Hentikan alarm';
@override
String get alarmStopFailedMessage =>
'Kami tidak dapat memastikan alarm berhenti. Coba lagi.';
@override
String get alarmForceStopAction => 'Paksa berhenti';
@override
String get alarmMissedNotificationTitle => 'Alarm terlewat';
@override
String alarmMissedNotificationText(Object name) {
return '$name dibisukan secara otomatis setelah 10 menit.';
}
@override
String get pauseAction => 'Jeda';
+15
View File
@@ -910,6 +910,21 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get stopAlarmAction => 'Ferma sveglia';
@override
String get alarmStopFailedMessage =>
'Non siamo riusciti a confermare larresto della sveglia. Riprova.';
@override
String get alarmForceStopAction => 'Forza arresto';
@override
String get alarmMissedNotificationTitle => 'Sveglia mancata';
@override
String alarmMissedNotificationText(Object name) {
return '$name è stata disattivata automaticamente dopo 10 minuti.';
}
@override
String get pauseAction => 'Pausa';
+14
View File
@@ -875,6 +875,20 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get stopAlarmAction => 'アラームを停止';
@override
String get alarmStopFailedMessage => 'アラームが停止したことを確認できませんでした。もう一度お試しください。';
@override
String get alarmForceStopAction => '強制停止';
@override
String get alarmMissedNotificationTitle => 'アラームの聞き逃し';
@override
String alarmMissedNotificationText(Object name) {
return '$nameは10分後に自動的に消音されました';
}
@override
String get pauseAction => '一時停止';
+15
View File
@@ -905,6 +905,21 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get stopAlarmAction => 'Parar alarme';
@override
String get alarmStopFailedMessage =>
'Não conseguimos confirmar que o alarme parou. Tente novamente.';
@override
String get alarmForceStopAction => 'Forçar parada';
@override
String get alarmMissedNotificationTitle => 'Alarme perdido';
@override
String alarmMissedNotificationText(Object name) {
return '$name foi silenciado automaticamente após 10 minutos.';
}
@override
String get pauseAction => 'Pausar';
+15
View File
@@ -909,6 +909,21 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get stopAlarmAction => 'Остановить будильник';
@override
String get alarmStopFailedMessage =>
'Не удалось подтвердить, что будильник остановлен. Попробуйте снова.';
@override
String get alarmForceStopAction => 'Принудительно остановить';
@override
String get alarmMissedNotificationTitle => 'Пропущенный будильник';
@override
String alarmMissedNotificationText(Object name) {
return '$name был автоматически отключён через 10 минут.';
}
@override
String get pauseAction => 'Пауза';
+14
View File
@@ -871,6 +871,20 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get stopAlarmAction => '停止闹钟';
@override
String get alarmStopFailedMessage => '无法确认闹钟已停止,请重试。';
@override
String get alarmForceStopAction => '强制停止';
@override
String get alarmMissedNotificationTitle => '错过的闹钟';
@override
String alarmMissedNotificationText(Object name) {
return '$name已在10分钟后自动静音';
}
@override
String get pauseAction => '暂停';