feat(alarms): native reliability fixes and end-to-end snooze

- Use mediaPlayback|systemExempted FGS type with FOREGROUND_SERVICE_SYSTEM_EXEMPTED so alarms fire on Android 14+ (FOREGROUND_SERVICE_ALARM does not exist in the SDK)
- Deduplicate fire notifications: the foreground service FSI notification is the single owner; receiver path removed
- Notification channel v2 with alarm sound URI and USAGE_ALARM attributes, one-time guarded migration from legacy channels
- Pass fallback station through the MethodChannel (NativeAlarmSpec schemaVersion 3) with a three-stage audio chain: primary -> fallback station -> bundled WAV
- Native fade-in volume ramp honoring fadeInSegundos when the app is killed
- Request battery-optimization exemption once, tracked with a persisted asked-once flag
- Fix snooze end-to-end: native ACTION_SNOOZE now reports back to Flutter (snoozed event + cold-start sync), snooze anchor unified to occurrence+minutes on both sides, periodic recalc no longer erases an active snooze
- Add snooze buttons (3/5/10/custom) to the ringing screen with shared audio teardown
- Redesign ringing screen on PluriWaveScaffold with reduced-motion-aware entry animation (new PluriAnimate helper)
- Alarm editor: live next-trigger preview, searchable station pickers (primary and fallback), configurable snooze duration, volume floor down to 0
- New alarm strings localized across all 13 locales
- New unit/widget tests for the snooze flow, alarm bridge payloads, ringing screen and editor (77 tests green)
- SDD artifacts for the app-quality-and-native-alarms change (explore, proposal, spec, design, tasks, apply progress)
This commit is contained in:
2026-06-11 15:33:30 +02:00
parent b5acf97ba4
commit f3e9487215
57 changed files with 4902 additions and 509 deletions
+30
View File
@@ -1510,6 +1510,36 @@ abstract class AppLocalizations {
/// **'Sin emisora: usar sonido interno'**
String get noStationUseInternalSound;
/// No description provided for @alarmFallbackStationLabel.
///
/// In es, this message translates to:
/// **'Emisora de respaldo'**
String get alarmFallbackStationLabel;
/// No description provided for @alarmStationPickerSearchHint.
///
/// In es, this message translates to:
/// **'Buscá una emisora por nombre'**
String get alarmStationPickerSearchHint;
/// No description provided for @alarmSnoozeDurationTitle.
///
/// In es, this message translates to:
/// **'Duración de la posposición'**
String get alarmSnoozeDurationTitle;
/// No description provided for @snoozeAction.
///
/// In es, this message translates to:
/// **'Posponer'**
String get snoozeAction;
/// No description provided for @alarmSnoozeOptionLabel.
///
/// In es, this message translates to:
/// **'{minutes} min'**
String alarmSnoozeOptionLabel(int minutes);
/// No description provided for @saveFavoritesAlarmHint.
///
/// In es, this message translates to:
+17
View File
@@ -798,6 +798,23 @@ class AppLocalizationsAr extends AppLocalizations {
@override
String get noStationUseInternalSound => 'بدون محطة: استخدام الصوت الداخلي';
@override
String get alarmFallbackStationLabel => 'محطة احتياطية';
@override
String get alarmStationPickerSearchHint => 'ابحث عن محطة بالاسم';
@override
String get alarmSnoozeDurationTitle => 'مدة الغفوة';
@override
String get snoozeAction => 'غفوة';
@override
String alarmSnoozeOptionLabel(int minutes) {
return '$minutes د';
}
@override
String get saveFavoritesAlarmHint =>
'احفظ محطات في المفضلة لاستخدامها كمنبه موسيقي.';
+17
View File
@@ -804,6 +804,23 @@ class AppLocalizationsBn extends AppLocalizations {
String get noStationUseInternalSound =>
'স্টেশন নেই: অভ্যন্তরীণ শব্দ ব্যবহার করুন';
@override
String get alarmFallbackStationLabel => 'ব্যাকআপ স্টেশন';
@override
String get alarmStationPickerSearchHint => 'নাম দিয়ে স্টেশন খুঁজুন';
@override
String get alarmSnoozeDurationTitle => 'স্নুজ সময়কাল';
@override
String get snoozeAction => 'স্নুজ';
@override
String alarmSnoozeOptionLabel(int minutes) {
return '$minutes মিনিট';
}
@override
String get saveFavoritesAlarmHint =>
'সুরেলা অ্যালার্ম হিসেবে ব্যবহার করতে প্রিয়তে স্টেশন সংরক্ষণ করুন।';
+17
View File
@@ -806,6 +806,23 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get noStationUseInternalSound => 'Kein Sender: internen Ton verwenden';
@override
String get alarmFallbackStationLabel => 'Ersatzsender';
@override
String get alarmStationPickerSearchHint => 'Sender nach Name suchen';
@override
String get alarmSnoozeDurationTitle => 'Schlummerdauer';
@override
String get snoozeAction => 'Schlummern';
@override
String alarmSnoozeOptionLabel(int minutes) {
return '$minutes Min.';
}
@override
String get saveFavoritesAlarmHint =>
'Speichere Sender in Favoriten, um sie als musikalischen Alarm zu verwenden.';
+17
View File
@@ -801,6 +801,23 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get noStationUseInternalSound => 'No station: use internal sound';
@override
String get alarmFallbackStationLabel => 'Backup station';
@override
String get alarmStationPickerSearchHint => 'Search for a station by name';
@override
String get alarmSnoozeDurationTitle => 'Snooze duration';
@override
String get snoozeAction => 'Snooze';
@override
String alarmSnoozeOptionLabel(int minutes) {
return '$minutes min';
}
@override
String get saveFavoritesAlarmHint =>
'Save stations in Favorites to use them as a music alarm.';
+17
View File
@@ -804,6 +804,23 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get noStationUseInternalSound => 'Sin emisora: usar sonido interno';
@override
String get alarmFallbackStationLabel => 'Emisora de respaldo';
@override
String get alarmStationPickerSearchHint => 'Buscá una emisora por nombre';
@override
String get alarmSnoozeDurationTitle => 'Duración de la posposición';
@override
String get snoozeAction => 'Posponer';
@override
String alarmSnoozeOptionLabel(int minutes) {
return '$minutes min';
}
@override
String get saveFavoritesAlarmHint =>
'Guardá emisoras en Favoritos para usarlas como alarma musical.';
+17
View File
@@ -809,6 +809,23 @@ class AppLocalizationsFr extends AppLocalizations {
String get noStationUseInternalSound =>
'Aucune station : utiliser le son interne';
@override
String get alarmFallbackStationLabel => 'Station de secours';
@override
String get alarmStationPickerSearchHint => 'Rechercher une station par nom';
@override
String get alarmSnoozeDurationTitle => 'Durée de répétition';
@override
String get snoozeAction => 'Répéter';
@override
String alarmSnoozeOptionLabel(int minutes) {
return '$minutes min';
}
@override
String get saveFavoritesAlarmHint =>
'Enregistrez des stations dans les Favoris pour les utiliser comme alarme musicale.';
+17
View File
@@ -801,6 +801,23 @@ class AppLocalizationsHi extends AppLocalizations {
String get noStationUseInternalSound =>
'कोई स्टेशन नहीं: आंतरिक ध्वनि इस्तेमाल करें';
@override
String get alarmFallbackStationLabel => 'बैकअप स्टेशन';
@override
String get alarmStationPickerSearchHint => 'नाम से स्टेशन खोजें';
@override
String get alarmSnoozeDurationTitle => 'स्नूज़ अवधि';
@override
String get snoozeAction => 'स्नूज़';
@override
String alarmSnoozeOptionLabel(int minutes) {
return '$minutes मिनट';
}
@override
String get saveFavoritesAlarmHint =>
'उन्हें संगीतमय अलार्म के रूप में इस्तेमाल करने के लिए स्टेशन पसंदीदा में सहेजें।';
+17
View File
@@ -804,6 +804,23 @@ class AppLocalizationsId extends AppLocalizations {
String get noStationUseInternalSound =>
'Tanpa stasiun: gunakan suara internal';
@override
String get alarmFallbackStationLabel => 'Stasiun cadangan';
@override
String get alarmStationPickerSearchHint => 'Cari stasiun berdasarkan nama';
@override
String get alarmSnoozeDurationTitle => 'Durasi tunda';
@override
String get snoozeAction => 'Tunda';
@override
String alarmSnoozeOptionLabel(int minutes) {
return '$minutes mnt';
}
@override
String get saveFavoritesAlarmHint =>
'Simpan stasiun ke Favorit untuk digunakan sebagai alarm musik.';
+17
View File
@@ -805,6 +805,23 @@ class AppLocalizationsIt extends AppLocalizations {
String get noStationUseInternalSound =>
'Nessuna emittente: usa suono interno';
@override
String get alarmFallbackStationLabel => 'Emittente di riserva';
@override
String get alarmStationPickerSearchHint => 'Cerca un\'emittente per nome';
@override
String get alarmSnoozeDurationTitle => 'Durata posticipo';
@override
String get snoozeAction => 'Posticipa';
@override
String alarmSnoozeOptionLabel(int minutes) {
return '$minutes min';
}
@override
String get saveFavoritesAlarmHint =>
'Salva emittenti nei Preferiti per usarle come sveglia musicale.';
+17
View File
@@ -777,6 +777,23 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get noStationUseInternalSound => '局なし: 内部音を使用';
@override
String get alarmFallbackStationLabel => '予備の局';
@override
String get alarmStationPickerSearchHint => '局名で検索';
@override
String get alarmSnoozeDurationTitle => 'スヌーズ時間';
@override
String get snoozeAction => 'スヌーズ';
@override
String alarmSnoozeOptionLabel(int minutes) {
return '$minutes分';
}
@override
String get saveFavoritesAlarmHint => '音楽アラームとして使うには、局をお気に入りに保存してください。';
+17
View File
@@ -803,6 +803,23 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get noStationUseInternalSound => 'Sem estação: usar som interno';
@override
String get alarmFallbackStationLabel => 'Estação reserva';
@override
String get alarmStationPickerSearchHint => 'Buscar estação pelo nome';
@override
String get alarmSnoozeDurationTitle => 'Duração da soneca';
@override
String get snoozeAction => 'Soneca';
@override
String alarmSnoozeOptionLabel(int minutes) {
return '$minutes min';
}
@override
String get saveFavoritesAlarmHint =>
'Salve estações nos Favoritos para usá-las como alarme musical.';
+17
View File
@@ -805,6 +805,23 @@ class AppLocalizationsRu extends AppLocalizations {
String get noStationUseInternalSound =>
'Без станции: использовать внутренний звук';
@override
String get alarmFallbackStationLabel => 'Резервная станция';
@override
String get alarmStationPickerSearchHint => 'Поиск станции по названию';
@override
String get alarmSnoozeDurationTitle => 'Интервал повтора';
@override
String get snoozeAction => 'Отложить';
@override
String alarmSnoozeOptionLabel(int minutes) {
return '$minutes мин';
}
@override
String get saveFavoritesAlarmHint =>
'Сохраните станции в избранное, чтобы использовать их как музыкальный будильник.';
+17
View File
@@ -773,6 +773,23 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get noStationUseInternalSound => '无电台:使用内部声音';
@override
String get alarmFallbackStationLabel => '备用电台';
@override
String get alarmStationPickerSearchHint => '按名称搜索电台';
@override
String get alarmSnoozeDurationTitle => '贪睡时长';
@override
String get snoozeAction => '贪睡';
@override
String alarmSnoozeOptionLabel(int minutes) {
return '$minutes 分钟';
}
@override
String get saveFavoritesAlarmHint => '将电台保存到收藏,即可把它们用作音乐闹钟。';