Completes the bridge the native side already exposed. AlarmScheduler and PluriWaveAlarmService record a pre-notice that could not be armed, a refused foreground-service start, and a per-alarm reschedule that failed after a reboot -- but nothing read them, so all three still ended at logcat. EstadoAlarmas now drains them at startup and turns each into a per-alarm exception, which the card already knows how to mark. An alarm that never reached the OS stops looking identical to one that did. The read is deliberately tolerant: a failure to read is logged and swallowed, never surfaced as an alarm error, so a diagnostics gap cannot masquerade as a scheduling problem.
599 lines
22 KiB
Dart
599 lines
22 KiB
Dart
import 'dart:async';
|
|
import 'dart:ui' show Locale;
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/services.dart';
|
|
|
|
import '../l10n/display_names.dart';
|
|
import '../l10n/gen/app_localizations.dart';
|
|
import '../modelos/alarma_musical.dart';
|
|
|
|
class EventoAlarmaAndroid {
|
|
const EventoAlarmaAndroid({
|
|
required this.alarmaId,
|
|
required this.titulo,
|
|
required this.accion,
|
|
this.triggerAtMillis = 0,
|
|
this.occurrenceAtMillis = 0,
|
|
this.snoozeMinutes = 5,
|
|
this.snoozeUntilMillis = 0,
|
|
});
|
|
|
|
/// Action reported when the native service snoozed an alarm by itself
|
|
/// (notification "Posponer" while the app may be backgrounded/killed).
|
|
static const accionSnoozed = 'snoozed';
|
|
|
|
/// Action reported when a pending snooze was cancelled natively from the
|
|
/// countdown notification ("Detener" while the app may be killed).
|
|
static const accionSnoozeCancelled = 'snoozeCancelled';
|
|
|
|
/// Action reported when a fired alarm auto-silenced unattended after the
|
|
/// 10-minute bound (Decision 3), never a user-initiated stop.
|
|
static const accionMissed = 'missed';
|
|
|
|
final String alarmaId;
|
|
final String titulo;
|
|
final String accion;
|
|
final int triggerAtMillis;
|
|
final int occurrenceAtMillis;
|
|
final int snoozeMinutes;
|
|
final int snoozeUntilMillis;
|
|
|
|
factory EventoAlarmaAndroid.fromMap(Map<Object?, Object?> map) {
|
|
return EventoAlarmaAndroid(
|
|
alarmaId: map['alarmId'] as String? ?? '',
|
|
titulo: map['alarmTitle'] as String? ?? 'PluriWave',
|
|
accion: map['alarmAction'] as String? ?? '',
|
|
triggerAtMillis: (map['triggerAtMillis'] as num?)?.toInt() ?? 0,
|
|
occurrenceAtMillis: (map['occurrenceAtMillis'] as num?)?.toInt() ?? 0,
|
|
snoozeMinutes: (map['snoozeMinutes'] as num?)?.toInt() ?? 5,
|
|
snoozeUntilMillis: (map['snoozeUntilMillis'] as num?)?.toInt() ?? 0,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Active native snooze persisted by `AlarmScheduler` (Kotlin). Used on cold
|
|
/// start so Flutter (single source of truth) can import snoozes performed
|
|
/// while the engine was dead.
|
|
class EstadoSnoozeNativo {
|
|
const EstadoSnoozeNativo({
|
|
required this.alarmaId,
|
|
required this.snoozeHasta,
|
|
required this.snoozeOrigen,
|
|
});
|
|
|
|
final String alarmaId;
|
|
final DateTime snoozeHasta;
|
|
final DateTime snoozeOrigen;
|
|
|
|
factory EstadoSnoozeNativo.fromMap(Map<Object?, Object?> map) {
|
|
return EstadoSnoozeNativo(
|
|
alarmaId: map['alarmId'] as String? ?? '',
|
|
snoozeHasta: DateTime.fromMillisecondsSinceEpoch(
|
|
(map['snoozeUntilMillis'] as num?)?.toInt() ?? 0,
|
|
),
|
|
snoozeOrigen: DateTime.fromMillisecondsSinceEpoch(
|
|
(map['snoozeOriginMillis'] as num?)?.toInt() ?? 0,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class DiagnosticoAlarmasAndroid {
|
|
const DiagnosticoAlarmasAndroid({
|
|
required this.puedeProgramarExactas,
|
|
required this.notificacionesPermitidas,
|
|
required this.puedeUsarPantallaCompleta,
|
|
required this.ignoraOptimizacionBateria,
|
|
required this.alarmasNativasPendientes,
|
|
required this.fabricante,
|
|
required this.versionSdk,
|
|
});
|
|
|
|
final bool puedeProgramarExactas;
|
|
final bool notificacionesPermitidas;
|
|
final bool puedeUsarPantallaCompleta;
|
|
final bool ignoraOptimizacionBateria;
|
|
final int alarmasNativasPendientes;
|
|
final String fabricante;
|
|
final int versionSdk;
|
|
|
|
factory DiagnosticoAlarmasAndroid.fromMap(Map<Object?, Object?> map) {
|
|
return DiagnosticoAlarmasAndroid(
|
|
puedeProgramarExactas: map['canScheduleExactAlarms'] as bool? ?? true,
|
|
notificacionesPermitidas: map['notificationsEnabled'] as bool? ?? true,
|
|
puedeUsarPantallaCompleta: map['canUseFullScreenIntent'] as bool? ?? true,
|
|
ignoraOptimizacionBateria:
|
|
map['isIgnoringBatteryOptimizations'] as bool? ?? true,
|
|
alarmasNativasPendientes: map['nativePendingAlarmsCount'] as int? ?? 0,
|
|
fabricante: map['manufacturer'] as String? ?? 'Android',
|
|
versionSdk: map['sdkInt'] as int? ?? 0,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Fail-safe stop result (Decision 1). `detenido` reports whether a
|
|
/// non-no-op teardown was dispatched (never silently swallowed); `alarmaId`
|
|
/// is the id that was actually ringing, for Dart-side reconciliation.
|
|
class ResultadoDetencion {
|
|
const ResultadoDetencion({
|
|
required this.detenido,
|
|
required this.estabaSonando,
|
|
this.alarmaId,
|
|
});
|
|
|
|
final bool detenido;
|
|
final bool estabaSonando;
|
|
final String? alarmaId;
|
|
|
|
/// A thrown channel error or a missing native response is treated as a
|
|
/// failure, never as an implicit success.
|
|
static const fallo = ResultadoDetencion(
|
|
detenido: false,
|
|
estabaSonando: false,
|
|
);
|
|
|
|
factory ResultadoDetencion.fromMap(Map<Object?, Object?> map) {
|
|
return ResultadoDetencion(
|
|
detenido: map['stopped'] as bool? ?? false,
|
|
estabaSonando: map['wasRinging'] as bool? ?? false,
|
|
alarmaId: map['activeAlarmId'] as String?,
|
|
);
|
|
}
|
|
}
|
|
|
|
class EjecucionAlarmaNativa {
|
|
const EjecucionAlarmaNativa({
|
|
required this.alarmaId,
|
|
required this.gestionadaEn,
|
|
});
|
|
|
|
final String alarmaId;
|
|
final DateTime gestionadaEn;
|
|
|
|
factory EjecucionAlarmaNativa.fromMap(Map<Object?, Object?> map) {
|
|
return EjecucionAlarmaNativa(
|
|
alarmaId: map['alarmId'] as String? ?? '',
|
|
gestionadaEn: DateTime.fromMillisecondsSinceEpoch(
|
|
(map['handledAtMillis'] as num?)?.toInt() ?? 0,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// A scheduling-reliability failure the NATIVE side recorded on its own
|
|
/// (fix/alarmas-fallos-silenciosos, item 2): the pre-notice reminder, the
|
|
/// ringing foreground service, and a post-boot/unlock reschedule can each
|
|
/// fail without ever going through a Dart method-channel call that could
|
|
/// throw -- the native scheduler persists these instead (mirroring how
|
|
/// handled occurrences and snooze state already survive a killed engine),
|
|
/// and this is the cold-start sync so the Dart side finds out at all.
|
|
class FalloProgramacionNativo {
|
|
const FalloProgramacionNativo({
|
|
required this.alarmaId,
|
|
required this.tipo,
|
|
required this.ocurridoEn,
|
|
});
|
|
|
|
final String alarmaId;
|
|
final String tipo;
|
|
final DateTime ocurridoEn;
|
|
|
|
factory FalloProgramacionNativo.fromMap(Map<Object?, Object?> map) {
|
|
return FalloProgramacionNativo(
|
|
alarmaId: map['alarmId'] as String? ?? '',
|
|
tipo: map['type'] as String? ?? '',
|
|
ocurridoEn: DateTime.fromMillisecondsSinceEpoch(
|
|
(map['atMillis'] as num?)?.toInt() ?? 0,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
abstract class PuertoAlarmasAndroid {
|
|
Stream<EventoAlarmaAndroid> get eventosAlarma;
|
|
|
|
/// Provides the UI localizations used to localize the alarm/station names
|
|
/// sent to the native scheduler.
|
|
void configurarLocalizaciones(AppLocalizations l10n);
|
|
|
|
Future<void> programar(AlarmaMusical alarma);
|
|
Future<void> cancelar(String alarmaId);
|
|
|
|
/// Failures the NATIVE side recorded on its own, outside any Dart call:
|
|
/// a pre-notice that could not be armed, a refused foreground-service
|
|
/// start when the alarm should have rung, and a per-alarm reschedule that
|
|
/// failed after a reboot. Each entry carries the alarm id and one of
|
|
/// [ExcepcionAlarma]'s `tipoFallo*` constants.
|
|
///
|
|
/// Before this existed every one of those paths logged to logcat and
|
|
/// stopped there, so an alarm could sit switched on in the list having
|
|
/// never reached the OS at all — the user's "as if there were no alarm".
|
|
Future<List<Map<String, Object?>>> fallosNativosProgramacion();
|
|
Future<void> ocultarNotificacionAlarma(String alarmaId);
|
|
|
|
/// Notification-only dismissal (RES-1): hides the fire notification for
|
|
/// [alarmaId] WITHOUT stopping native ring audio for any alarm. Used when a
|
|
/// genuinely different alarm rings while another one is still active.
|
|
Future<void> ocultarSoloNotificacion(String alarmaId);
|
|
Future<void> detenerSonidoNativo(String alarmaId);
|
|
|
|
/// Synchronous companion snapshot (Decision 1): the id of the alarm
|
|
/// currently ringing natively, or null if none is.
|
|
Future<String?> alarmaSonandoId();
|
|
|
|
/// Id-agnostic fail-safe stop: silences whatever is ringing regardless of
|
|
/// which alarm the caller thinks is active, and reports a verified result.
|
|
Future<ResultadoDetencion> detenerSonidoActivo();
|
|
Future<bool> solicitarPermisoAlarmasExactas();
|
|
Future<bool> solicitarPermisoNotificaciones();
|
|
Future<bool> solicitarPermisoPantallaCompleta();
|
|
Future<bool> solicitarExencionBateria();
|
|
|
|
/// Opens the system's per-app notification settings screen directly
|
|
/// (`ACTION_APP_NOTIFICATION_SETTINGS`), as opposed to
|
|
/// [solicitarPermisoNotificaciones]'s runtime permission popup. Used from
|
|
/// the reliability diagnostics screen: once a user is troubleshooting an
|
|
/// alarm that already failed, sending them straight to Settings is more
|
|
/// robust than a runtime dialog the OS may refuse to show again after a
|
|
/// prior denial.
|
|
Future<bool> abrirConfiguracionNotificaciones();
|
|
|
|
Future<DiagnosticoAlarmasAndroid> diagnostico();
|
|
Future<EventoAlarmaAndroid?> obtenerEventoInicial();
|
|
Future<List<EjecucionAlarmaNativa>> obtenerEjecucionesNativasGestionadas();
|
|
Future<List<EstadoSnoozeNativo>> obtenerEstadoSnoozeNativo();
|
|
|
|
/// Scheduling-reliability failures the native side recorded on its own
|
|
/// (pre-notice, foreground-service start, or post-boot reschedule) since
|
|
/// the last sync.
|
|
Future<List<FalloProgramacionNativo>> obtenerFallosProgramacionNativos();
|
|
}
|
|
|
|
class ServicioAlarmasAndroid implements PuertoAlarmasAndroid {
|
|
ServicioAlarmasAndroid({
|
|
MethodChannel channel = const MethodChannel('pluriwave/alarm_scheduler'),
|
|
}) : _channel = channel {
|
|
_instalarHandler();
|
|
}
|
|
|
|
final MethodChannel _channel;
|
|
|
|
// Instance state (S3-R2): each bridge owns its controller and l10n so
|
|
// independent instances never share events through globals.
|
|
final _eventosController = StreamController<EventoAlarmaAndroid>.broadcast();
|
|
AppLocalizations? _l10n;
|
|
|
|
AppLocalizations get _textos {
|
|
final actual = _l10n;
|
|
if (actual != null) return actual;
|
|
return lookupAppLocalizations(const Locale('es'));
|
|
}
|
|
|
|
@override
|
|
void configurarLocalizaciones(AppLocalizations l10n) {
|
|
_l10n = l10n;
|
|
// Push every localized notification/channel/chooser string to the native
|
|
// side so it can build localized notifications even when the Flutter engine
|
|
// is dead (alarm fired from a killed app). Fire-and-forget; runs once per
|
|
// locale change (callers guard against per-rebuild churn).
|
|
unawaited(_enviarTextosNotificacion(l10n));
|
|
}
|
|
|
|
Future<void> _enviarTextosNotificacion(AppLocalizations l10n) async {
|
|
try {
|
|
await _channel.invokeMethod<void>('setNotificationStrings', {
|
|
'ringTitle': l10n.alarmRingingNotificationTitle,
|
|
'snoozeLabel': l10n.snoozeAction,
|
|
'stopLabel': l10n.stopAlarmAction,
|
|
'skipLabel': l10n.skipNextAction,
|
|
'snoozeAgainLabel': l10n.snoozeAgainAction,
|
|
'fireChannelName': l10n.alarmFireChannelName,
|
|
'fireChannelDescription': l10n.alarmFireChannelDescription,
|
|
'preNoticeChannelName': l10n.alarmPreNoticeChannelName,
|
|
'preNoticeChannelDescription': l10n.alarmPreNoticeChannelDescription,
|
|
'preNoticeTemplate': _plantillaMinutos(l10n.preNoticeCountdown),
|
|
'snoozeCountdownTemplate': _plantillaMinutos(l10n.snoozeCountdown),
|
|
'openFolderTitle': l10n.openFolderChooserTitle,
|
|
'openRecordingTitle': l10n.openRecordingChooserTitle,
|
|
// Title of the storage root that RecordingsDocumentsProvider publishes
|
|
// to the system file manager, so the recordings folder is browsable
|
|
// without leaving app-private storage.
|
|
'recordingsRootTitle': l10n.recordingsFolderTitle,
|
|
'missedTitle': l10n.alarmMissedNotificationTitle,
|
|
'missedTemplate': _plantillaNombre(l10n.alarmMissedNotificationText),
|
|
});
|
|
} catch (e) {
|
|
debugPrint('[PluriWave][alarmas] setNotificationStrings ERROR $e');
|
|
}
|
|
}
|
|
|
|
/// Turns a localized `{int} -> String` message into a template with a literal
|
|
/// `{minutes}` placeholder for Kotlin to fill at fire time: it calls the
|
|
/// message with a unique sentinel and swaps the sentinel back for `{minutes}`.
|
|
static String _plantillaMinutos(String Function(int) traducir) {
|
|
const sentinel = 42424242;
|
|
return traducir(sentinel).replaceFirst(sentinel.toString(), '{minutes}');
|
|
}
|
|
|
|
/// Same sentinel-swap approach as [_plantillaMinutos], but for a
|
|
/// `{String} -> String` message: swaps a unique sentinel token back for the
|
|
/// literal `{name}` placeholder Kotlin fills in at fire time.
|
|
static String _plantillaNombre(String Function(Object) traducir) {
|
|
const sentinel = 'PLURIWAVE_NAME_SENTINEL';
|
|
return traducir(sentinel).replaceFirst(sentinel, '{name}');
|
|
}
|
|
|
|
@override
|
|
Stream<EventoAlarmaAndroid> get eventosAlarma => _eventosController.stream;
|
|
|
|
@override
|
|
Future<void> programar(AlarmaMusical alarma) async {
|
|
final proxima = alarma.proximaProgramable;
|
|
if (proxima == null || !alarma.activa) {
|
|
debugPrint(
|
|
'[PluriWave][alarmas] cancelar por inactiva/sin proxima id=${alarma.id} activa=${alarma.activa} proxima=$proxima',
|
|
);
|
|
await cancelar(alarma.id);
|
|
return;
|
|
}
|
|
debugPrint(
|
|
'[PluriWave][alarmas] programar id=${alarma.id} nombre=${alarma.nombre} proxima=${proxima.toIso8601String()} preaviso=${proxima.subtract(const Duration(minutes: 30)).toIso8601String()}',
|
|
);
|
|
final programada = await _channel.invokeMethod<bool>('scheduleAlarm', {
|
|
'id': alarma.id,
|
|
'title': localizedAlarmName(_textos, alarma.nombre),
|
|
'triggerAtMillis': proxima.millisecondsSinceEpoch,
|
|
'preNoticeAtMillis':
|
|
alarma.snoozeHasta == null
|
|
? proxima
|
|
.subtract(const Duration(minutes: 30))
|
|
.millisecondsSinceEpoch
|
|
: 0,
|
|
'hour': alarma.hora,
|
|
'minute': alarma.minuto,
|
|
'scheduleType': alarma.tipoProgramacion.name,
|
|
'weekdays': alarma.diasSemana,
|
|
// Anchored at LOCAL NOON, not midnight: the native side derives the
|
|
// calendar DAY back from this epoch in whatever timezone the device is
|
|
// in when it re-arms (boot/travel). A midnight epoch reinterpreted in
|
|
// a westward zone rolls to the previous day; noon keeps the intended
|
|
// date stable across any real-world zone shift (+-11h).
|
|
'oneShotDateMillis':
|
|
alarma.fechaUnica == null
|
|
? null
|
|
: DateTime(
|
|
alarma.fechaUnica!.year,
|
|
alarma.fechaUnica!.month,
|
|
alarma.fechaUnica!.day,
|
|
12,
|
|
).millisecondsSinceEpoch,
|
|
'snoozeUntilMillis': alarma.snoozeHasta?.millisecondsSinceEpoch,
|
|
'snoozeOriginMillis': alarma.snoozeOrigen?.millisecondsSinceEpoch,
|
|
'snoozeMinutes': alarma.snoozeMinutos,
|
|
'lastHandledAtMillis':
|
|
alarma.ultimaEjecucionGestionada?.millisecondsSinceEpoch,
|
|
'soundOnVacation': alarma.sonarEnVacaciones,
|
|
'stationName':
|
|
alarma.emisora == null
|
|
? null
|
|
: localizedStationName(_textos, alarma.emisora!.nombre),
|
|
'stationUrl': alarma.emisora?.url,
|
|
'fallbackStationName':
|
|
alarma.emisoraFallback == null
|
|
? null
|
|
: localizedStationName(_textos, alarma.emisoraFallback!.nombre),
|
|
'fallbackStationUrl': alarma.emisoraFallback?.url,
|
|
'fallbackSound': alarma.sonidoInterno.name,
|
|
'volume': alarma.volumen,
|
|
'fadeInSegundos': alarma.fadeInSegundos,
|
|
});
|
|
if (programada != true) {
|
|
throw StateError(_textos.androidExactAlarmScheduleError);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<void> cancelar(String alarmaId) =>
|
|
_logAndInvokeVoid('cancelAlarm', {'id': alarmaId});
|
|
|
|
@override
|
|
Future<void> ocultarNotificacionAlarma(String alarmaId) =>
|
|
_logAndInvokeVoid('dismissAlarmNotification', {'id': alarmaId});
|
|
|
|
@override
|
|
Future<void> ocultarSoloNotificacion(String alarmaId) =>
|
|
_logAndInvokeVoid('dismissAlarmNotificationOnly', {'id': alarmaId});
|
|
|
|
@override
|
|
Future<void> detenerSonidoNativo(String alarmaId) =>
|
|
_logAndInvokeVoid('stopNativeAlarmSound', {'id': alarmaId});
|
|
|
|
@override
|
|
Future<String?> alarmaSonandoId() async {
|
|
try {
|
|
return await _channel.invokeMethod<String>('getActiveRingingAlarmId');
|
|
} catch (e) {
|
|
// Fail-toward-silence (Finding 2): a query failure must NOT be
|
|
// mistaken for "nothing is ringing" by callers like
|
|
// EstadoAlarmas._detenerSiEstaSonando, which would otherwise skip the
|
|
// stop entirely on a genuinely ringing alarm. Rethrow so the caller can
|
|
// fall back to the id-scoped legacy stop instead.
|
|
debugPrint('[PluriWave][alarmas] alarmaSonandoId ERROR $e');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<ResultadoDetencion> detenerSonidoActivo() async {
|
|
try {
|
|
final raw = await _channel.invokeMethod<Map<Object?, Object?>>(
|
|
'stopActiveAlarm',
|
|
);
|
|
if (raw == null) return ResultadoDetencion.fallo;
|
|
return ResultadoDetencion.fromMap(raw);
|
|
} catch (e) {
|
|
debugPrint('[PluriWave][alarmas] detenerSonidoActivo ERROR $e');
|
|
return ResultadoDetencion.fallo;
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<List<Map<String, Object?>>> fallosNativosProgramacion() async {
|
|
try {
|
|
final raw = await _channel.invokeMethod<List<Object?>>(
|
|
'getNativeSchedulingFailures',
|
|
);
|
|
if (raw == null) return const [];
|
|
return raw
|
|
.whereType<Map<Object?, Object?>>()
|
|
.map((m) => m.map((k, v) => MapEntry(k.toString(), v)))
|
|
.toList();
|
|
} catch (e) {
|
|
// Never let a diagnostics read break alarm handling: an older build
|
|
// of the native side simply has no such channel method.
|
|
debugPrint('[PluriWave][alarmas] fallosNativosProgramacion ERROR $e');
|
|
return const [];
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<bool> solicitarPermisoAlarmasExactas() async {
|
|
final abierto = await _channel.invokeMethod<bool>(
|
|
'requestExactAlarmPermission',
|
|
);
|
|
return abierto ?? false;
|
|
}
|
|
|
|
@override
|
|
Future<bool> solicitarPermisoNotificaciones() async {
|
|
final abierto = await _channel.invokeMethod<bool>(
|
|
'requestPostNotificationsPermission',
|
|
);
|
|
return abierto ?? false;
|
|
}
|
|
|
|
@override
|
|
Future<bool> solicitarPermisoPantallaCompleta() async {
|
|
final abierto = await _channel.invokeMethod<bool>(
|
|
'requestFullScreenIntentPermission',
|
|
);
|
|
return abierto ?? false;
|
|
}
|
|
|
|
@override
|
|
Future<bool> solicitarExencionBateria() async {
|
|
final abierto = await _channel.invokeMethod<bool>(
|
|
'requestIgnoreBatteryOptimizations',
|
|
);
|
|
return abierto ?? false;
|
|
}
|
|
|
|
@override
|
|
Future<bool> abrirConfiguracionNotificaciones() async {
|
|
final abierto = await _channel.invokeMethod<bool>(
|
|
'openNotificationSettings',
|
|
);
|
|
return abierto ?? false;
|
|
}
|
|
|
|
@override
|
|
Future<DiagnosticoAlarmasAndroid> diagnostico() async {
|
|
debugPrint('[PluriWave][alarmas] diagnostico android');
|
|
final raw = await _channel.invokeMethod<Map<Object?, Object?>>(
|
|
'diagnostics',
|
|
);
|
|
final diag = DiagnosticoAlarmasAndroid.fromMap(raw ?? const {});
|
|
debugPrint(
|
|
'[PluriWave][alarmas] diagnostico exactas=${diag.puedeProgramarExactas} notificaciones=${diag.notificacionesPermitidas} sdk=${diag.versionSdk} fabricante=${diag.fabricante}',
|
|
);
|
|
return diag;
|
|
}
|
|
|
|
@override
|
|
Future<EventoAlarmaAndroid?> obtenerEventoInicial() async {
|
|
final raw = await _channel.invokeMethod<Map<Object?, Object?>>(
|
|
'getInitialAlarmIntent',
|
|
);
|
|
if (raw == null || raw.isEmpty) return null;
|
|
final evento = EventoAlarmaAndroid.fromMap(raw);
|
|
debugPrint(
|
|
'[PluriWave][alarmas] evento inicial id=${evento.alarmaId} accion=${evento.accion}',
|
|
);
|
|
return evento.alarmaId.isEmpty ? null : evento;
|
|
}
|
|
|
|
@override
|
|
Future<List<EjecucionAlarmaNativa>>
|
|
obtenerEjecucionesNativasGestionadas() async {
|
|
final raw = await _channel.invokeMethod<List<Object?>>(
|
|
'getHandledAlarmOccurrences',
|
|
);
|
|
if (raw == null || raw.isEmpty) return const [];
|
|
return raw
|
|
.whereType<Map<Object?, Object?>>()
|
|
.map(EjecucionAlarmaNativa.fromMap)
|
|
.where(
|
|
(evento) =>
|
|
evento.alarmaId.isNotEmpty &&
|
|
evento.gestionadaEn.millisecondsSinceEpoch > 0,
|
|
)
|
|
.toList();
|
|
}
|
|
|
|
@override
|
|
Future<List<EstadoSnoozeNativo>> obtenerEstadoSnoozeNativo() async {
|
|
final raw = await _channel.invokeMethod<List<Object?>>(
|
|
'getNativeSnoozeState',
|
|
);
|
|
if (raw == null || raw.isEmpty) return const [];
|
|
return raw
|
|
.whereType<Map<Object?, Object?>>()
|
|
.map(EstadoSnoozeNativo.fromMap)
|
|
.where(
|
|
(estado) =>
|
|
estado.alarmaId.isNotEmpty &&
|
|
estado.snoozeHasta.millisecondsSinceEpoch > 0,
|
|
)
|
|
.toList();
|
|
}
|
|
|
|
@override
|
|
Future<List<FalloProgramacionNativo>>
|
|
obtenerFallosProgramacionNativos() async {
|
|
final raw = await _channel.invokeMethod<List<Object?>>(
|
|
'getNativeSchedulingFailures',
|
|
);
|
|
if (raw == null || raw.isEmpty) return const [];
|
|
return raw
|
|
.whereType<Map<Object?, Object?>>()
|
|
.map(FalloProgramacionNativo.fromMap)
|
|
.where((fallo) => fallo.alarmaId.isNotEmpty && fallo.tipo.isNotEmpty)
|
|
.toList();
|
|
}
|
|
|
|
Future<void> _logAndInvokeVoid(String method, Map<String, Object?> args) {
|
|
debugPrint('[PluriWave][alarmas] $method $args');
|
|
return _channel.invokeMethod<void>(method, args);
|
|
}
|
|
|
|
// Installed once per instance from the constructor. Creating a second
|
|
// instance over the SAME channel re-binds the platform handler to the
|
|
// newest instance (production has exactly one instance per channel).
|
|
void _instalarHandler() {
|
|
_channel.setMethodCallHandler((call) async {
|
|
if (call.method != 'alarmFired') return;
|
|
final args = call.arguments;
|
|
if (args is Map) {
|
|
final evento = EventoAlarmaAndroid.fromMap(args);
|
|
if (evento.alarmaId.isNotEmpty) {
|
|
debugPrint(
|
|
'[PluriWave][alarmas] evento nativo id=${evento.alarmaId} accion=${evento.accion}',
|
|
);
|
|
_eventosController.add(evento);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|