docs(openspec): add SDD artifact trail for recent alarm and EQ changes
Persist the exploration, proposal, spec, design, tasks, and verify/archive reports produced during the multi-device EQ, alarm-countdown, and notification-visual-polish SDD cycles.
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
# Alarm Pre-Notice L10n Specification
|
||||
|
||||
## Purpose
|
||||
|
||||
Define the required behavior for the pre-notice notification text when an alarm is
|
||||
approaching. The notification MUST display computed remaining minutes in the device
|
||||
locale rather than hardcoded Spanish text.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Computed Remaining Minutes
|
||||
|
||||
The system MUST compute the number of minutes remaining until alarm fire time at the
|
||||
moment the pre-notice `BroadcastReceiver` fires and MUST use that value as the
|
||||
displayed countdown, NOT a hardcoded string.
|
||||
|
||||
Remaining minutes MUST be floored (integer division). If the computed value is less
|
||||
than 1 minute, the system MUST clamp to 1 and display "1 min".
|
||||
|
||||
#### Scenario: Normal pre-notice (alarm is ~30 min away)
|
||||
|
||||
- GIVEN an alarm is scheduled 30 minutes in the future
|
||||
- WHEN the pre-notice `BroadcastReceiver` fires at `triggerAtMillis - PRE_NOTICE_MILLIS`
|
||||
- THEN the notification body displays the remaining minutes derived from
|
||||
`(triggerAtMillis - currentTimeMillis) / 60_000`
|
||||
- AND the displayed value is a positive integer (e.g. "30 min")
|
||||
|
||||
#### Scenario: Alarm scheduled with less than 30 min remaining
|
||||
|
||||
- GIVEN an alarm is scheduled with fewer than 30 minutes from now
|
||||
- WHEN the pre-notice fires immediately (or is already past)
|
||||
- THEN the computed remaining minutes MAY be 0 or negative
|
||||
- AND the system clamps the displayed value to a minimum of 1 minute
|
||||
|
||||
#### Scenario: System clock drift
|
||||
|
||||
- GIVEN the device clock drifts so `currentTimeMillis` exceeds `triggerAtMillis`
|
||||
- WHEN the receiver fires and computes remaining minutes
|
||||
- THEN the system clamps to 1 and displays "1 min"
|
||||
- AND does NOT display a negative number or crash
|
||||
|
||||
---
|
||||
|
||||
### Requirement: L10n-Ready Notification Text
|
||||
|
||||
The system MUST NOT hardcode any natural-language string in the notification content.
|
||||
The pre-notice message MUST be produced via a localized string resource that accepts
|
||||
a `{minutes}` placeholder.
|
||||
|
||||
A new ARB key `preNoticeCountdown` MUST be added to all 13 locale ARB files. The
|
||||
string template MUST follow the same pattern as the existing `durationMinutesOnly`
|
||||
key.
|
||||
|
||||
#### Scenario: Device locale is Spanish
|
||||
|
||||
- GIVEN the device locale is `es`
|
||||
- WHEN the pre-notice notification is posted
|
||||
- THEN the notification body uses the Spanish translation of `preNoticeCountdown`
|
||||
with the computed minutes substituted
|
||||
|
||||
#### Scenario: Device locale is English
|
||||
|
||||
- GIVEN the device locale is `en`
|
||||
- WHEN the pre-notice notification is posted
|
||||
- THEN the notification body uses the English translation of `preNoticeCountdown`
|
||||
with the computed minutes substituted
|
||||
|
||||
#### Scenario: Missing locale translation (fallback)
|
||||
|
||||
- GIVEN the device locale has no translation for `preNoticeCountdown`
|
||||
- WHEN the pre-notice notification is posted
|
||||
- THEN the system falls back to the default locale translation (English)
|
||||
- AND does NOT display an untranslated key name or crash
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Notification ID Stability
|
||||
|
||||
The pre-notice notification MUST be posted with the same stable notification ID
|
||||
derived from `alarmId` that the system currently uses (`notificationIdForAlarm(alarmId)`).
|
||||
|
||||
#### Scenario: Pre-notice posted
|
||||
|
||||
- GIVEN an alarm with `alarmId = X`
|
||||
- WHEN the pre-notice fires
|
||||
- THEN `NotificationManagerCompat.notify()` is called with ID `notificationIdForAlarm(X)`
|
||||
- AND calling `notify()` again with the same ID updates the existing notification
|
||||
rather than creating a duplicate
|
||||
|
||||
---
|
||||
|
||||
### Requirement: No Additional Infrastructure
|
||||
|
||||
The pre-notice notification MUST be a single, static notification posted once at
|
||||
fire time. The system MUST NOT schedule per-minute update chains (AlarmManager
|
||||
repeat), WorkManager periodic tasks, or new foreground services to update the
|
||||
countdown text after posting.
|
||||
|
||||
#### Scenario: Notification posted
|
||||
|
||||
- GIVEN the pre-notice fires
|
||||
- WHEN the notification is posted
|
||||
- THEN exactly one `notify()` call is made
|
||||
- AND no new AlarmManager intents, WorkManager jobs, or foreground services are started
|
||||
@@ -0,0 +1,77 @@
|
||||
# Alarm Snooze Dismiss Specification
|
||||
|
||||
## Purpose
|
||||
|
||||
Define the required behavior for dismissing the alarm screen when the user taps
|
||||
Snooze. The dismissal MUST work reliably regardless of whether the app was already
|
||||
running or was launched cold (dead-app state) by the full-screen intent.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Reliable Screen Dismissal on Snooze
|
||||
|
||||
When the user taps Snooze, the alarm screen MUST be removed from view. The system
|
||||
MUST handle both a live Navigator stack (app was running) and an empty Navigator
|
||||
stack (app launched from dead state via full-screen intent).
|
||||
|
||||
The system MUST use `Navigator.canPop()` to determine the stack state before
|
||||
calling `Navigator.pop()`. If `canPop()` returns false, the system MUST call
|
||||
`SystemNavigator.pop()` as a fallback to close the activity.
|
||||
|
||||
#### Scenario: Snooze from a running app (Navigator stack non-empty)
|
||||
|
||||
- GIVEN the alarm screen is displayed and the app was already running before the alarm fired
|
||||
- WHEN the user taps Snooze
|
||||
- THEN `Navigator.canPop()` returns true
|
||||
- AND `Navigator.pop()` is called
|
||||
- AND the alarm screen is dismissed
|
||||
- AND the app returns to the previous screen
|
||||
|
||||
#### Scenario: Snooze from dead-app state (Navigator stack empty)
|
||||
|
||||
- GIVEN the app was not running when the alarm fired
|
||||
- AND the alarm screen was launched by the full-screen intent as the root activity
|
||||
- WHEN the user taps Snooze
|
||||
- THEN `Navigator.canPop()` returns false
|
||||
- AND `SystemNavigator.pop()` is called
|
||||
- AND the alarm screen activity is closed
|
||||
- AND the device returns to the home screen or the previous app
|
||||
|
||||
#### Scenario: Snooze side effects always complete regardless of dismiss path
|
||||
|
||||
- GIVEN either app state (running or dead)
|
||||
- WHEN the user taps Snooze
|
||||
- THEN `_liberarAudioLocal()` is called before any navigation action
|
||||
- AND `radio.audio.pausar()` is called before any navigation action
|
||||
- AND `alarmas.posponerAlarma()` is called before any navigation action
|
||||
- AND the navigation dismissal is the LAST action in `_posponer()`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Snooze Re-Trigger Is Unaffected
|
||||
|
||||
The snooze re-schedule logic MUST remain unchanged. The fix MUST NOT alter when or
|
||||
how `posponerAlarma()` reprograms the AlarmManager.
|
||||
|
||||
#### Scenario: Snooze re-trigger after dead-app dismissal
|
||||
|
||||
- GIVEN the alarm screen was dismissed via `SystemNavigator.pop()`
|
||||
- WHEN the snoozed time (`snoozeHasta`) is reached
|
||||
- THEN the AlarmManager fires the alarm again
|
||||
- AND the alarm screen is shown again via full-screen intent
|
||||
|
||||
---
|
||||
|
||||
### Requirement: No Unintended App Termination
|
||||
|
||||
`SystemNavigator.pop()` MUST only be called when `canPop()` is false (i.e., the
|
||||
screen was the root route launched from a dead-app full-screen intent). It MUST NOT
|
||||
be called when a live Navigator stack exists.
|
||||
|
||||
#### Scenario: Guard prevents accidental SystemNavigator.pop() in running-app state
|
||||
|
||||
- GIVEN the app has a non-empty Navigator stack
|
||||
- WHEN the user taps Snooze
|
||||
- THEN `canPop()` returns true
|
||||
- AND `SystemNavigator.pop()` is NOT called
|
||||
- AND only `Navigator.pop()` is used for dismissal
|
||||
Reference in New Issue
Block a user