fix(alarm): make all date math wall-clock correct across DST and timezone changes
Full time-domain audit (three shipped date bugs prompted it) found one
root cause and two latent travel defects, all now fixed:
Day-stepping used add(Duration(days: 1)), which shifts the absolute
instant by exactly 86400s — documented Dart behavior (sdk#47666), so
crossing a DST transition drifted the wall hour by +-1h permanently
for the rest of the candidate scan (verified: 2026-03-28 07:30
Europe/Madrid + "1 day" = 08:30). The native Calendar engine preserves
wall time, and the single-authority fix made the drifted Dart verdict
win. Candidates now advance by calendar reconstruction (_siguienteDia:
DateTime(y, m, d+1, hora, minuto)), the same wall-clock-preserving
semantics as Calendar.add(DAY_OF_YEAR, 1) plus AOSP DeskClock's
defensive hour/minute re-assertion, keeping both engines in agreement
through any transition.
Instant-valued fields (snoozeHasta/snoozeOrigen/proximaEjecucion/
ultimaEjecucionGestionada/creadaEn/actualizadaEn) serialized as
offset-less local ISO, so re-parsing after a device timezone change
reinterpreted the same wall fields as a different instant. They now
serialize as UTC ("Z"); reads normalize to local, and legacy
offset-less payloads parse identically — no migration. fechaUnica
stays local on purpose: it is a wall-clock date.
One-shot alarms sent fechaUnica's midnight epoch to the native side,
whose boot/travel re-arm derives the calendar day back from it in the
CURRENT zone — a westward shift rolled the date to the previous day.
The channel now anchors the date at local noon, keeping it stable
across real-world zone shifts.
Property tests lock the no-drift guarantee (400 daily / 200 weekday
iterations must all land exactly at hora:minuto — on DST-observing
dev machines this crosses real transitions), plus UTC round-trip,
legacy-payload compatibility, and wall-date preservation tests.
This commit is contained in:
@@ -134,12 +134,19 @@ class AlarmaMusical {
|
||||
'volumen': volumen,
|
||||
'fadeInSegundos': fadeInSegundos,
|
||||
'sonidoInterno': sonidoInterno.name,
|
||||
'proximaEjecucion': proximaEjecucion?.toIso8601String(),
|
||||
'snoozeHasta': snoozeHasta?.toIso8601String(),
|
||||
'snoozeOrigen': snoozeOrigen?.toIso8601String(),
|
||||
'ultimaEjecucionGestionada': ultimaEjecucionGestionada?.toIso8601String(),
|
||||
'creadaEn': creadaEn?.toIso8601String(),
|
||||
'actualizadaEn': actualizadaEn?.toIso8601String(),
|
||||
// INSTANT fields serialize as UTC (offset-carrying "Z" ISO): a local
|
||||
// toIso8601String() has no offset, so re-parsing it after the device
|
||||
// changes timezone reinterprets the same wall fields as a DIFFERENT
|
||||
// instant (a snooze set in Madrid would shift hours after landing in
|
||||
// New York). fechaUnica stays local-ISO on purpose: it is a wall-clock
|
||||
// DATE (only y/m/d are ever read), which must follow the user.
|
||||
'proximaEjecucion': proximaEjecucion?.toUtc().toIso8601String(),
|
||||
'snoozeHasta': snoozeHasta?.toUtc().toIso8601String(),
|
||||
'snoozeOrigen': snoozeOrigen?.toUtc().toIso8601String(),
|
||||
'ultimaEjecucionGestionada':
|
||||
ultimaEjecucionGestionada?.toUtc().toIso8601String(),
|
||||
'creadaEn': creadaEn?.toUtc().toIso8601String(),
|
||||
'actualizadaEn': actualizadaEn?.toUtc().toIso8601String(),
|
||||
};
|
||||
|
||||
// persistence-resilience (D2): `id` stays a REQUIRED, un-defaulted cast
|
||||
@@ -196,8 +203,12 @@ class AlarmaMusical {
|
||||
return Emisora.fromMap(Map<String, dynamic>.from(raw));
|
||||
}
|
||||
|
||||
// Normalizes to LOCAL on read: new payloads carry "Z" (UTC instants,
|
||||
// toLocal converts), legacy offset-less payloads parse as local already
|
||||
// (toLocal is then the identity) — both shapes land as the same local
|
||||
// DateTime the scheduling math expects, so no data migration is needed.
|
||||
static DateTime? _dateFromJson(Object? raw) =>
|
||||
raw is String ? DateTime.tryParse(raw) : null;
|
||||
raw is String ? DateTime.tryParse(raw)?.toLocal() : null;
|
||||
|
||||
static T _enumFromName<T extends Enum>(
|
||||
List<T> values,
|
||||
|
||||
Reference in New Issue
Block a user