fix(alarms): harden native playback and pre-notice actions
This commit is contained in:
@@ -199,6 +199,81 @@ class ServicioAlarmas {
|
||||
return nuevo;
|
||||
}
|
||||
|
||||
Future<ConfiguracionAlarmas> posponerEjecucion(
|
||||
String alarmaId,
|
||||
DateTime ejecucion,
|
||||
int minutos,
|
||||
) async {
|
||||
final snoozeHasta = _programacion.calcularSnooze(_reloj(), minutos);
|
||||
return posponerEjecucionHasta(alarmaId, ejecucion, snoozeHasta);
|
||||
}
|
||||
|
||||
Future<ConfiguracionAlarmas> posponerEjecucionHasta(
|
||||
String alarmaId,
|
||||
DateTime ejecucion,
|
||||
DateTime snoozeHasta,
|
||||
) async {
|
||||
final config = await cargar();
|
||||
final ahora = _reloj();
|
||||
final alarmas =
|
||||
config.alarmas
|
||||
.map(
|
||||
(a) =>
|
||||
a.id == alarmaId
|
||||
? a.copyWith(
|
||||
snoozeHasta: snoozeHasta,
|
||||
snoozeOrigen: ejecucion,
|
||||
ultimaEjecucionGestionada: ejecucion,
|
||||
actualizadaEn: ahora,
|
||||
)
|
||||
: a,
|
||||
)
|
||||
.toList();
|
||||
final nuevo = ConfiguracionAlarmas(
|
||||
alarmas: alarmas,
|
||||
vacaciones: config.vacaciones,
|
||||
excepciones: config.excepciones,
|
||||
);
|
||||
await _guardar(nuevo);
|
||||
return nuevo;
|
||||
}
|
||||
|
||||
Future<ConfiguracionAlarmas> completarEjecucion(
|
||||
String alarmaId,
|
||||
DateTime ejecucion,
|
||||
) async {
|
||||
final config = await cargar();
|
||||
final ahora = _reloj();
|
||||
final alarmas =
|
||||
config.alarmas.map((a) {
|
||||
if (a.id != alarmaId) return a;
|
||||
final siguiente = _programacion.calcularSiguienteDespuesDeEjecucion(
|
||||
alarma: a,
|
||||
ejecucion: ejecucion,
|
||||
vacaciones: config.vacaciones,
|
||||
excepciones: config.excepciones,
|
||||
);
|
||||
return a.copyWith(
|
||||
activa:
|
||||
a.tipoProgramacion == TipoProgramacionAlarma.unica
|
||||
? false
|
||||
: a.activa,
|
||||
proximaEjecucion: siguiente,
|
||||
limpiarProximaEjecucion: true,
|
||||
limpiarSnooze: true,
|
||||
ultimaEjecucionGestionada: ejecucion,
|
||||
actualizadaEn: ahora,
|
||||
);
|
||||
}).toList();
|
||||
final nuevo = ConfiguracionAlarmas(
|
||||
alarmas: alarmas,
|
||||
vacaciones: config.vacaciones,
|
||||
excepciones: config.excepciones,
|
||||
);
|
||||
await _guardar(nuevo);
|
||||
return nuevo;
|
||||
}
|
||||
|
||||
AlarmaMusical crearAlarma({
|
||||
required String nombre,
|
||||
required int hora,
|
||||
@@ -250,15 +325,19 @@ class ServicioAlarmas {
|
||||
List<RangoVacaciones> vacaciones,
|
||||
List<ExcepcionAlarma> excepciones,
|
||||
) {
|
||||
final ahora = _reloj();
|
||||
final snoozeActivo =
|
||||
alarma.snoozeHasta != null && alarma.snoozeHasta!.isAfter(ahora);
|
||||
final proxima = _programacion.calcularProxima(
|
||||
alarma: alarma,
|
||||
desde: _reloj(),
|
||||
desde: ahora,
|
||||
vacaciones: vacaciones,
|
||||
excepciones: excepciones,
|
||||
);
|
||||
return alarma.copyWith(
|
||||
proximaEjecucion: proxima,
|
||||
limpiarProximaEjecucion: true,
|
||||
limpiarSnooze: !snoozeActivo,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -58,6 +58,23 @@ class ServicioProgramacionAlarmas {
|
||||
return desde.add(Duration(minutes: seguro));
|
||||
}
|
||||
|
||||
DateTime? calcularSiguienteDespuesDeEjecucion({
|
||||
required AlarmaMusical alarma,
|
||||
required DateTime ejecucion,
|
||||
List<RangoVacaciones> vacaciones = const [],
|
||||
List<ExcepcionAlarma> excepciones = const [],
|
||||
}) {
|
||||
if (!alarma.activa) return null;
|
||||
if (alarma.tipoProgramacion == TipoProgramacionAlarma.unica) return null;
|
||||
|
||||
return calcularProxima(
|
||||
alarma: alarma.copyWith(limpiarSnooze: true),
|
||||
desde: ejecucion.add(const Duration(minutes: 1)),
|
||||
vacaciones: vacaciones,
|
||||
excepciones: excepciones,
|
||||
);
|
||||
}
|
||||
|
||||
bool estaEnVacaciones(DateTime fecha, List<RangoVacaciones> vacaciones) =>
|
||||
vacaciones.any((rango) => rango.contiene(fecha));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user