fix(alarm): localize pre-notice countdown and fix snooze dismiss
Build & Deploy PluriWave / Análisis de código (push) Successful in 39s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 2m33s

Replace hardcoded Spanish pre-notice text with computed remaining
minutes using l10n template passed via MethodChannel. Fix snooze
dismiss in dead-app state with canPop guard and SystemNavigator.pop
fallback.
This commit is contained in:
2026-06-28 11:55:15 +02:00
parent 58922de6fc
commit 4ffd73d136
8 changed files with 510 additions and 13 deletions
@@ -39,7 +39,8 @@ class AlarmScheduler(private val context: Context) {
snoozeMinutes: Int = 5,
fallbackStationName: String? = null,
fallbackStationUrl: String? = null,
fadeInSegundos: Int = 0
fadeInSegundos: Int = 0,
preNoticeTemplate: String? = null
): Boolean {
val existing = readSpec(id)
val preservedSnooze = preserveNativeSnooze(
@@ -70,7 +71,8 @@ class AlarmScheduler(private val context: Context) {
fallbackSound = fallbackSound,
volume = volume.coerceIn(0f, 1f),
fadeInSegundos = fadeInSegundos.coerceIn(0, 60),
timezoneId = TimeZone.getDefault().id
timezoneId = TimeZone.getDefault().id,
preNoticeTemplate = preNoticeTemplate
)
return scheduleSpec(spec, persistOnSuccess = true)
}
@@ -145,6 +147,7 @@ class AlarmScheduler(private val context: Context) {
PluriWaveAlarmReceiver.EXTRA_OCCURRENCE_AT,
spec.snoozeOriginMillis ?: spec.triggerAtMillis
)
putExtra(EXTRA_PRE_NOTICE_TEMPLATE, spec.preNoticeTemplate)
},
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
@@ -165,6 +168,7 @@ class AlarmScheduler(private val context: Context) {
PluriWaveAlarmReceiver.EXTRA_OCCURRENCE_AT,
spec.snoozeOriginMillis ?: spec.triggerAtMillis
)
putExtra(EXTRA_PRE_NOTICE_TEMPLATE, spec.preNoticeTemplate)
}
)
Log.d(tag, "alarm.schedule preNotice immediate id=${spec.id}")
@@ -652,7 +656,10 @@ class AlarmScheduler(private val context: Context) {
val fallbackSound: String?,
val volume: Float,
val fadeInSegundos: Int = 0,
val timezoneId: String
val timezoneId: String,
// Nullable for backward compat: old persisted alarms without this field
// fall back to the English default in the receiver. Schema stays v3.
val preNoticeTemplate: String? = null
) {
fun toJson(): JSONObject = JSONObject().apply {
put("schemaVersion", 3)
@@ -679,6 +686,7 @@ class AlarmScheduler(private val context: Context) {
put("volume", volume)
put("fadeInSegundos", fadeInSegundos)
put("timezoneId", timezoneId)
put("preNoticeTemplate", preNoticeTemplate)
}
companion object {
@@ -715,7 +723,8 @@ class AlarmScheduler(private val context: Context) {
fallbackSound = json.optString("fallbackSound").takeIf { it.isNotBlank() },
volume = json.optDouble("volume", 0.85).toFloat(),
fadeInSegundos = json.optInt("fadeInSegundos", 0).coerceIn(0, 60),
timezoneId = json.optString("timezoneId", TimeZone.getDefault().id)
timezoneId = json.optString("timezoneId", TimeZone.getDefault().id),
preNoticeTemplate = json.optString("preNoticeTemplate").takeIf { it.isNotBlank() }
)
}
}
@@ -730,6 +739,9 @@ class AlarmScheduler(private val context: Context) {
private const val PRE_NOTICE_MILLIS = 30 * 60 * 1000L
private const val SCHEDULE_UNICA = "unica"
private const val SCHEDULE_DIAS_SEMANA = "diasSemana"
// Intent extra key for the localized pre-notice template string.
// Declared once here; PluriWaveAlarmReceiver reads it via this constant.
const val EXTRA_PRE_NOTICE_TEMPLATE = "preNoticeTemplate"
}
}
@@ -115,7 +115,8 @@ class MainActivity : AudioServiceActivity() {
snoozeMinutes = call.argument<Int>("snoozeMinutes") ?: 5,
fallbackStationName = call.argument<String>("fallbackStationName"),
fallbackStationUrl = call.argument<String>("fallbackStationUrl"),
fadeInSegundos = call.argument<Int>("fadeInSegundos") ?: 0
fadeInSegundos = call.argument<Int>("fadeInSegundos") ?: 0,
preNoticeTemplate = call.argument<String>("preNoticeTemplate")
)
result.success(scheduled)
}
@@ -51,7 +51,8 @@ class PluriWaveAlarmReceiver : BroadcastReceiver() {
title,
snoozeMinutes,
intent.getLongExtra(EXTRA_TRIGGER_AT, 0L),
intent.getLongExtra(EXTRA_OCCURRENCE_AT, 0L)
intent.getLongExtra(EXTRA_OCCURRENCE_AT, 0L),
intent.getStringExtra(AlarmScheduler.EXTRA_PRE_NOTICE_TEMPLATE)
)
}
ACTION_POSTPONE_NEXT -> {
@@ -100,10 +101,14 @@ class PluriWaveAlarmReceiver : BroadcastReceiver() {
title: String,
snoozeMinutes: Int,
triggerAtMillis: Long,
occurrenceAtMillis: Long
occurrenceAtMillis: Long,
preNoticeTemplate: String? = null
) {
ensureChannel(context)
val remaining = computeRemainingMinutes(triggerAtMillis)
val contentText = formatPreNoticeText(preNoticeTemplate, remaining)
val openAppIntent = PendingIntent.getActivity(
context,
requestCode(alarmId, 1),
@@ -144,7 +149,7 @@ class PluriWaveAlarmReceiver : BroadcastReceiver() {
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(android.R.drawable.ic_dialog_info)
.setContentTitle(title)
.setContentText("Empieza en 30 minutos")
.setContentText(contentText)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setCategory(NotificationCompat.CATEGORY_REMINDER)
.setSilent(true)
@@ -156,12 +161,31 @@ class PluriWaveAlarmReceiver : BroadcastReceiver() {
try {
NotificationManagerCompat.from(context).notify(notificationIdForAlarm(alarmId), notification)
Log.d(TAG, "alarm.notification preNotice shown id=$alarmId")
Log.d(TAG, "alarm.notification preNotice shown id=$alarmId remaining=$remaining")
} catch (error: SecurityException) {
Log.e(TAG, "alarm.notification preNotice SecurityException id=$alarmId", error)
}
}
/**
* Computes the number of minutes remaining until [triggerAtMillis],
* clamped to a minimum of 1. Handles Doze-delayed wakeups and clock drift.
*/
private fun computeRemainingMinutes(triggerAtMillis: Long): Long =
maxOf(1L, (triggerAtMillis - System.currentTimeMillis()) / 60_000L)
/**
* Formats the pre-notice notification text by replacing the `{minutes}`
* placeholder in [template] with [remaining]. Falls back to an English
* default if [template] is null or blank.
*/
private fun formatPreNoticeText(template: String?, remaining: Long): String {
if (template.isNullOrBlank()) {
return "Starts in $remaining min"
}
return template.replace("{minutes}", remaining.toString())
}
private fun ensureChannel(context: Context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager