fix(alarms): harden native playback and pre-notice actions
This commit is contained in:
+21
-1
@@ -241,6 +241,27 @@ class _PaginaPrincipalState extends State<_PaginaPrincipal> {
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (evento.accion.endsWith('.POSTPONE_NEXT')) {
|
||||
final ejecucion =
|
||||
evento.occurrenceAtMillis > 0
|
||||
? DateTime.fromMillisecondsSinceEpoch(evento.occurrenceAtMillis)
|
||||
: alarma.proximaEjecucion ?? DateTime.now();
|
||||
await estado.posponerProximaDesdePreaviso(
|
||||
alarma,
|
||||
evento.snoozeMinutes,
|
||||
ejecucion,
|
||||
);
|
||||
if (!mounted) return;
|
||||
setState(() => _indice = 3);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Alarma pospuesta ${evento.snoozeMinutes} min para esta ejecución.',
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (evento.accion.endsWith('.PRE_NOTICE')) {
|
||||
setState(() => _indice = 3);
|
||||
return;
|
||||
@@ -268,7 +289,6 @@ class _PaginaPrincipalState extends State<_PaginaPrincipal> {
|
||||
_alarmaSonandoId = alarma.id;
|
||||
|
||||
try {
|
||||
await alarmas.android.detenerSonidoNativo(alarma.id);
|
||||
await _prearrancarAudioAlarma(alarma);
|
||||
if (!mounted) return;
|
||||
await Navigator.of(context).push(
|
||||
|
||||
@@ -9,7 +9,7 @@ import '../servicios/servicio_alarmas_android.dart';
|
||||
class EstadoAlarmas extends ChangeNotifier {
|
||||
EstadoAlarmas({
|
||||
ServicioAlarmas? servicio,
|
||||
ServicioAlarmasAndroid? android,
|
||||
PuertoAlarmasAndroid? android,
|
||||
bool iniciarAutomaticamente = true,
|
||||
}) : servicio = servicio ?? ServicioAlarmas(),
|
||||
android = android ?? ServicioAlarmasAndroid() {
|
||||
@@ -19,7 +19,7 @@ class EstadoAlarmas extends ChangeNotifier {
|
||||
}
|
||||
|
||||
final ServicioAlarmas servicio;
|
||||
final ServicioAlarmasAndroid android;
|
||||
final PuertoAlarmasAndroid android;
|
||||
|
||||
List<AlarmaMusical> _alarmas = [];
|
||||
List<RangoVacaciones> _vacaciones = [];
|
||||
@@ -45,8 +45,10 @@ class EstadoAlarmas extends ChangeNotifier {
|
||||
|
||||
AlarmaMusical? get proximaAlarma {
|
||||
final candidatas =
|
||||
_alarmas.where((a) => a.activa && a.proximaEjecucion != null).toList()
|
||||
..sort((a, b) => a.proximaEjecucion!.compareTo(b.proximaEjecucion!));
|
||||
_alarmas.where((a) => a.activa && a.proximaProgramable != null).toList()
|
||||
..sort(
|
||||
(a, b) => a.proximaProgramable!.compareTo(b.proximaProgramable!),
|
||||
);
|
||||
return candidatas.isEmpty ? null : candidatas.first;
|
||||
}
|
||||
|
||||
@@ -109,7 +111,7 @@ class EstadoAlarmas extends ChangeNotifier {
|
||||
}
|
||||
|
||||
void marcarEjecucionGestionada(AlarmaMusical alarma) {
|
||||
final proxima = alarma.proximaEjecucion;
|
||||
final proxima = alarma.proximaProgramable;
|
||||
if (proxima == null) return;
|
||||
final key = '${alarma.id}:${proxima.millisecondsSinceEpoch}';
|
||||
_ejecucionesEmitidas.add(key);
|
||||
@@ -159,18 +161,62 @@ class EstadoAlarmas extends ChangeNotifier {
|
||||
}
|
||||
|
||||
Future<void> posponerAlarma(AlarmaMusical alarma, int minutos) async {
|
||||
final proxima = DateTime.now().add(Duration(minutes: minutos));
|
||||
final ejecucion =
|
||||
alarma.snoozeOrigen ?? alarma.proximaEjecucion ?? DateTime.now();
|
||||
debugPrint(
|
||||
'[PluriWave][alarmas] posponer id=${alarma.id} minutos=$minutos proxima=${proxima.toIso8601String()}',
|
||||
'[PluriWave][alarmas] posponer id=${alarma.id} minutos=$minutos ejecucion=${ejecucion.toIso8601String()}',
|
||||
);
|
||||
await android.ocultarNotificacionAlarma(alarma.id);
|
||||
await android.programar(alarma.copyWith(proximaEjecucion: proxima));
|
||||
final config = await servicio.posponerEjecucion(
|
||||
alarma.id,
|
||||
ejecucion,
|
||||
minutos,
|
||||
);
|
||||
_aplicar(config);
|
||||
final actualizada = _buscarAlarma(alarma.id);
|
||||
if (actualizada != null) {
|
||||
await android.programar(actualizada);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> posponerProximaDesdePreaviso(
|
||||
AlarmaMusical alarma,
|
||||
int minutos,
|
||||
DateTime ejecucion,
|
||||
) async {
|
||||
final seguros = _snoozeSeguro(minutos);
|
||||
final snoozeHasta = ejecucion.add(Duration(minutes: seguros));
|
||||
debugPrint(
|
||||
'[PluriWave][alarmas] posponer desde preaviso id=${alarma.id} minutos=$seguros ejecucion=${ejecucion.toIso8601String()} hasta=${snoozeHasta.toIso8601String()}',
|
||||
);
|
||||
await android.ocultarNotificacionAlarma(alarma.id);
|
||||
final config = await servicio.posponerEjecucionHasta(
|
||||
alarma.id,
|
||||
ejecucion,
|
||||
snoozeHasta,
|
||||
);
|
||||
_aplicar(config);
|
||||
final actualizada = _buscarAlarma(alarma.id);
|
||||
if (actualizada != null) {
|
||||
await android.programar(actualizada);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> finalizarEjecucion(String alarmaId) async {
|
||||
debugPrint('[PluriWave][alarmas] finalizar ejecucion id=$alarmaId');
|
||||
final alarma = _buscarAlarma(alarmaId);
|
||||
final ejecucion =
|
||||
alarma?.snoozeOrigen ??
|
||||
alarma?.proximaEjecucion ??
|
||||
alarma?.snoozeHasta ??
|
||||
DateTime.now();
|
||||
await android.ocultarNotificacionAlarma(alarmaId);
|
||||
await refrescarProgramacion();
|
||||
final config = await servicio.completarEjecucion(alarmaId, ejecucion);
|
||||
_aplicar(config);
|
||||
await _sincronizarTodas();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> crearRangoVacaciones(RangoVacaciones rango) async {
|
||||
@@ -209,6 +255,16 @@ class EstadoAlarmas extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
AlarmaMusical? _buscarAlarma(String id) {
|
||||
for (final alarma in _alarmas) {
|
||||
if (alarma.id == id) return alarma;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
int _snoozeSeguro(int minutos) =>
|
||||
minutos == 3 || minutos == 5 || minutos == 10 ? minutos : 5;
|
||||
|
||||
void _aplicar(ConfiguracionAlarmas config) {
|
||||
_alarmas = config.alarmas;
|
||||
_vacaciones = config.vacaciones;
|
||||
@@ -230,7 +286,7 @@ class EstadoAlarmas extends ChangeNotifier {
|
||||
void _vigilarAlarmasVencidas() {
|
||||
final ahora = DateTime.now();
|
||||
for (final alarma in _alarmas) {
|
||||
final proxima = alarma.proximaEjecucion;
|
||||
final proxima = alarma.proximaProgramable;
|
||||
if (!alarma.activa || proxima == null) continue;
|
||||
if (proxima.isAfter(ahora)) continue;
|
||||
final key = '${alarma.id}:${proxima.millisecondsSinceEpoch}';
|
||||
|
||||
@@ -21,6 +21,9 @@ class AlarmaMusical {
|
||||
this.volumen = 0.85,
|
||||
this.sonidoInterno = SonidoInternoAlarma.amanecer,
|
||||
this.proximaEjecucion,
|
||||
this.snoozeHasta,
|
||||
this.snoozeOrigen,
|
||||
this.ultimaEjecucionGestionada,
|
||||
this.creadaEn,
|
||||
this.actualizadaEn,
|
||||
});
|
||||
@@ -40,6 +43,9 @@ class AlarmaMusical {
|
||||
final double volumen;
|
||||
final SonidoInternoAlarma sonidoInterno;
|
||||
final DateTime? proximaEjecucion;
|
||||
final DateTime? snoozeHasta;
|
||||
final DateTime? snoozeOrigen;
|
||||
final DateTime? ultimaEjecucionGestionada;
|
||||
final DateTime? creadaEn;
|
||||
final DateTime? actualizadaEn;
|
||||
|
||||
@@ -61,6 +67,11 @@ class AlarmaMusical {
|
||||
SonidoInternoAlarma? sonidoInterno,
|
||||
DateTime? proximaEjecucion,
|
||||
bool limpiarProximaEjecucion = false,
|
||||
DateTime? snoozeHasta,
|
||||
DateTime? snoozeOrigen,
|
||||
bool limpiarSnooze = false,
|
||||
DateTime? ultimaEjecucionGestionada,
|
||||
bool limpiarUltimaEjecucionGestionada = false,
|
||||
DateTime? creadaEn,
|
||||
DateTime? actualizadaEn,
|
||||
}) {
|
||||
@@ -83,11 +94,20 @@ class AlarmaMusical {
|
||||
limpiarProximaEjecucion
|
||||
? proximaEjecucion
|
||||
: proximaEjecucion ?? this.proximaEjecucion,
|
||||
snoozeHasta: limpiarSnooze ? snoozeHasta : snoozeHasta ?? this.snoozeHasta,
|
||||
snoozeOrigen:
|
||||
limpiarSnooze ? snoozeOrigen : snoozeOrigen ?? this.snoozeOrigen,
|
||||
ultimaEjecucionGestionada:
|
||||
limpiarUltimaEjecucionGestionada
|
||||
? ultimaEjecucionGestionada
|
||||
: ultimaEjecucionGestionada ?? this.ultimaEjecucionGestionada,
|
||||
creadaEn: creadaEn ?? this.creadaEn,
|
||||
actualizadaEn: actualizadaEn ?? this.actualizadaEn,
|
||||
);
|
||||
}
|
||||
|
||||
DateTime? get proximaProgramable => snoozeHasta ?? proximaEjecucion;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'nombre': nombre,
|
||||
@@ -104,6 +124,9 @@ class AlarmaMusical {
|
||||
'volumen': volumen,
|
||||
'sonidoInterno': sonidoInterno.name,
|
||||
'proximaEjecucion': proximaEjecucion?.toIso8601String(),
|
||||
'snoozeHasta': snoozeHasta?.toIso8601String(),
|
||||
'snoozeOrigen': snoozeOrigen?.toIso8601String(),
|
||||
'ultimaEjecucionGestionada': ultimaEjecucionGestionada?.toIso8601String(),
|
||||
'creadaEn': creadaEn?.toIso8601String(),
|
||||
'actualizadaEn': actualizadaEn?.toIso8601String(),
|
||||
};
|
||||
@@ -137,6 +160,11 @@ class AlarmaMusical {
|
||||
SonidoInternoAlarma.amanecer,
|
||||
),
|
||||
proximaEjecucion: _dateFromJson(json['proximaEjecucion']),
|
||||
snoozeHasta: _dateFromJson(json['snoozeHasta']),
|
||||
snoozeOrigen: _dateFromJson(json['snoozeOrigen']),
|
||||
ultimaEjecucionGestionada: _dateFromJson(
|
||||
json['ultimaEjecucionGestionada'],
|
||||
),
|
||||
creadaEn: _dateFromJson(json['creadaEn']),
|
||||
actualizadaEn: _dateFromJson(json['actualizadaEn']),
|
||||
);
|
||||
|
||||
@@ -30,6 +30,7 @@ class _PantallaAlarmaSonandoState extends State<PantallaAlarmaSonando> {
|
||||
Timer? _fallbackTimer;
|
||||
bool _fallbackActivo = false;
|
||||
bool _radioIntentada = false;
|
||||
bool _audioFlutterConfirmado = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -57,6 +58,7 @@ class _PantallaAlarmaSonandoState extends State<PantallaAlarmaSonando> {
|
||||
_estadoSub = radio.estadoStream.listen((estado) {
|
||||
if (estado == EstadoReproduccion.reproduciendo && mounted) {
|
||||
_fallbackTimer?.cancel();
|
||||
_confirmarAudioFlutterListo();
|
||||
}
|
||||
if (estado == EstadoReproduccion.error && mounted) {
|
||||
_iniciarFallback();
|
||||
@@ -76,9 +78,18 @@ class _PantallaAlarmaSonandoState extends State<PantallaAlarmaSonando> {
|
||||
_fallbackActivo = true;
|
||||
await _fallbackPlayer.setAsset(_assetFallback(widget.alarma.sonidoInterno));
|
||||
await _fallbackPlayer.play();
|
||||
await _confirmarAudioFlutterListo();
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
Future<void> _confirmarAudioFlutterListo() async {
|
||||
if (_audioFlutterConfirmado) return;
|
||||
_audioFlutterConfirmado = true;
|
||||
await context.read<EstadoAlarmas>().android.confirmarAudioFlutter(
|
||||
widget.alarma.id,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _detener() async {
|
||||
final radio = context.read<EstadoRadio>();
|
||||
final alarmas = context.read<EstadoAlarmas>();
|
||||
|
||||
@@ -82,8 +82,9 @@ class _PanelProximaAlarma extends StatelessWidget {
|
||||
final proxima = estado.proximaAlarma;
|
||||
final activasSinProxima =
|
||||
estado.alarmas
|
||||
.where((a) => a.activa && a.proximaEjecucion == null)
|
||||
.where((a) => a.activa && a.proximaProgramable == null)
|
||||
.length;
|
||||
final proximaProgramable = proxima?.proximaProgramable;
|
||||
return PluriGlassSurface(
|
||||
glowColor: const Color(0xFFFFB86B).withValues(alpha: 0.28),
|
||||
child: Row(
|
||||
@@ -110,7 +111,7 @@ class _PanelProximaAlarma extends StatelessWidget {
|
||||
? activasSinProxima > 0
|
||||
? 'Hay $activasSinProxima alarma(s) activas, pero ahora mismo no tienen una fecha futura válida. Revisá fecha, días y vacaciones.'
|
||||
: 'Creá una alarma y PluriWave calculará la siguiente ejecución automáticamente.'
|
||||
: '${proxima.nombre} · ${_fechaHora(proxima.proximaEjecucion!)}',
|
||||
: '${proxima.nombre} · ${_fechaHora(proximaProgramable!)}',
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -193,11 +194,11 @@ class _TarjetaAlarma extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (alarma.proximaEjecucion != null)
|
||||
if (alarma.proximaProgramable != null)
|
||||
_NoticeLine(
|
||||
icon: Icons.event_available_rounded,
|
||||
text:
|
||||
'Siguiente ejecución: ${_fechaHora(alarma.proximaEjecucion!)}',
|
||||
'Siguiente ejecución: ${_fechaHora(alarma.proximaProgramable!)}',
|
||||
)
|
||||
else
|
||||
const _NoticeLine(
|
||||
@@ -231,7 +232,7 @@ class _TarjetaAlarma extends StatelessWidget {
|
||||
icon: const Icon(Icons.skip_next_rounded),
|
||||
label: const Text('Omitir siguiente'),
|
||||
onPressed:
|
||||
alarma.proximaEjecucion == null
|
||||
alarma.proximaProgramable == null
|
||||
? null
|
||||
: () async {
|
||||
await estado.saltarProxima(alarma.id);
|
||||
@@ -248,9 +249,9 @@ class _TarjetaAlarma extends StatelessWidget {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
actualizada?.proximaEjecucion == null
|
||||
actualizada?.proximaProgramable == null
|
||||
? 'Alarma omitida. No queda próxima ejecución.'
|
||||
: 'Alarma omitida. Volverá el ${_fechaHora(actualizada!.proximaEjecucion!)}.',
|
||||
: 'Alarma omitida. Volverá el ${_fechaHora(actualizada!.proximaProgramable!)}.',
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -281,13 +282,13 @@ class _TarjetaAlarma extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
if (actual != null) {
|
||||
if (alarma.proximaEjecucion == null) {
|
||||
if (alarma.proximaProgramable == null) {
|
||||
return 'Está pausada por vacaciones (${actual.nombre}) y sin próxima ejecución.';
|
||||
}
|
||||
return 'Está pausada por vacaciones (${actual.nombre}) y vuelve el ${_fechaHora(alarma.proximaEjecucion!)}.';
|
||||
return 'Está pausada por vacaciones (${actual.nombre}) y vuelve el ${_fechaHora(alarma.proximaProgramable!)}.';
|
||||
}
|
||||
if (alarma.proximaEjecucion != null) {
|
||||
return 'Con vacaciones activas, volverá a sonar el ${_fechaHora(alarma.proximaEjecucion!)}.';
|
||||
if (alarma.proximaProgramable != null) {
|
||||
return 'Con vacaciones activas, volverá a sonar el ${_fechaHora(alarma.proximaProgramable!)}.';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -690,12 +691,18 @@ class _AccesoDiagnostico extends StatelessWidget {
|
||||
label: Text(
|
||||
diag == null
|
||||
? 'Revisar fiabilidad Android'
|
||||
: 'Fiabilidad: exactas ${diag.puedeProgramarExactas ? 'OK' : 'pendiente'} · notificaciones ${diag.notificacionesPermitidas ? 'OK' : 'pendiente'}',
|
||||
: 'Fiabilidad: exactas ${diag.puedeProgramarExactas ? 'OK' : 'pendiente'} ? notificaciones ${diag.notificacionesPermitidas ? 'OK' : 'pendiente'} ? pantalla ${diag.puedeUsarPantallaCompleta ? 'OK' : 'pendiente'}',
|
||||
),
|
||||
onPressed: () async {
|
||||
if (diag != null && !diag.puedeProgramarExactas) {
|
||||
await estado.android.solicitarPermisoAlarmasExactas();
|
||||
}
|
||||
if (diag != null && !diag.notificacionesPermitidas) {
|
||||
await estado.android.solicitarPermisoNotificaciones();
|
||||
}
|
||||
if (diag != null && !diag.puedeUsarPantallaCompleta) {
|
||||
await estado.android.solicitarPermisoPantallaCompleta();
|
||||
}
|
||||
await estado.cargarDiagnostico();
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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