fix(alarms): harden native playback and pre-notice actions

This commit is contained in:
Javier Bautista Fernández
2026-05-28 12:03:58 +02:00
parent 41bbd0ea17
commit 659e6da189
16 changed files with 1370 additions and 180 deletions
+79 -3
View File
@@ -10,17 +10,26 @@ class EventoAlarmaAndroid {
required this.alarmaId,
required this.titulo,
required this.accion,
this.triggerAtMillis = 0,
this.occurrenceAtMillis = 0,
this.snoozeMinutes = 5,
});
final String alarmaId;
final String titulo;
final String accion;
final int triggerAtMillis;
final int occurrenceAtMillis;
final int snoozeMinutes;
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,
);
}
}
@@ -29,12 +38,18 @@ 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;
@@ -42,13 +57,33 @@ class DiagnosticoAlarmasAndroid {
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,
);
}
}
class ServicioAlarmasAndroid {
abstract class PuertoAlarmasAndroid {
Stream<EventoAlarmaAndroid> get eventosAlarma;
Future<void> programar(AlarmaMusical alarma);
Future<void> cancelar(String alarmaId);
Future<void> ocultarNotificacionAlarma(String alarmaId);
Future<void> detenerSonidoNativo(String alarmaId);
Future<bool> solicitarPermisoAlarmasExactas();
Future<bool> solicitarPermisoNotificaciones();
Future<bool> solicitarPermisoPantallaCompleta();
Future<void> confirmarAudioFlutter(String alarmaId);
Future<DiagnosticoAlarmasAndroid> diagnostico();
Future<EventoAlarmaAndroid?> obtenerEventoInicial();
}
class ServicioAlarmasAndroid implements PuertoAlarmasAndroid {
ServicioAlarmasAndroid({
MethodChannel channel = const MethodChannel('pluriwave/alarm_scheduler'),
}) : _channel = channel {
@@ -60,10 +95,12 @@ class ServicioAlarmasAndroid {
StreamController<EventoAlarmaAndroid>.broadcast();
static bool _handlerInstalado = false;
@override
Stream<EventoAlarmaAndroid> get eventosAlarma => _eventosController.stream;
@override
Future<void> programar(AlarmaMusical alarma) async {
final proxima = alarma.proximaEjecucion;
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',
@@ -79,7 +116,20 @@ class ServicioAlarmasAndroid {
'title': alarma.nombre,
'triggerAtMillis': proxima.millisecondsSinceEpoch,
'preNoticeAtMillis':
proxima.subtract(const Duration(minutes: 30)).millisecondsSinceEpoch,
alarma.snoozeHasta == null
? proxima.subtract(const Duration(minutes: 30)).millisecondsSinceEpoch
: 0,
'hour': alarma.hora,
'minute': alarma.minuto,
'scheduleType': alarma.tipoProgramacion.name,
'weekdays': alarma.diasSemana,
'oneShotDateMillis': alarma.fechaUnica?.millisecondsSinceEpoch,
'snoozeUntilMillis': alarma.snoozeHasta?.millisecondsSinceEpoch,
'snoozeOriginMillis': alarma.snoozeOrigen?.millisecondsSinceEpoch,
'snoozeMinutes': alarma.snoozeMinutos,
'lastHandledAtMillis':
alarma.ultimaEjecucionGestionada?.millisecondsSinceEpoch,
'soundOnVacation': alarma.sonarEnVacaciones,
'stationName': alarma.emisora?.nombre,
'stationUrl': alarma.emisora?.url,
'fallbackSound': alarma.sonidoInterno.name,
@@ -92,15 +142,23 @@ class ServicioAlarmasAndroid {
}
}
@override
Future<void> cancelar(String alarmaId) =>
_logAndInvokeVoid('cancelAlarm', {'id': alarmaId});
@override
Future<void> ocultarNotificacionAlarma(String alarmaId) =>
_logAndInvokeVoid('dismissAlarmNotification', {'id': alarmaId});
@override
Future<void> detenerSonidoNativo(String alarmaId) =>
_logAndInvokeVoid('stopNativeAlarmSound', {'id': alarmaId});
@override
Future<void> confirmarAudioFlutter(String alarmaId) =>
_logAndInvokeVoid('confirmFlutterAudio', {'id': alarmaId});
@override
Future<bool> solicitarPermisoAlarmasExactas() async {
final abierto = await _channel.invokeMethod<bool>(
'requestExactAlarmPermission',
@@ -108,6 +166,23 @@ class ServicioAlarmasAndroid {
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<DiagnosticoAlarmasAndroid> diagnostico() async {
debugPrint('[PluriWave][alarmas] diagnostico android');
final raw = await _channel.invokeMethod<Map<Object?, Object?>>(
@@ -120,6 +195,7 @@ class ServicioAlarmasAndroid {
return diag;
}
@override
Future<EventoAlarmaAndroid?> obtenerEventoInicial() async {
final raw = await _channel.invokeMethod<Map<Object?, Object?>>(
'getInitialAlarmIntent',