234 lines
7.9 KiB
Dart
234 lines
7.9 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/services.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,
|
|
});
|
|
|
|
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,
|
|
);
|
|
}
|
|
}
|
|
|
|
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,
|
|
);
|
|
}
|
|
}
|
|
|
|
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 {
|
|
_instalarHandler(_channel);
|
|
}
|
|
|
|
final MethodChannel _channel;
|
|
static final _eventosController =
|
|
StreamController<EventoAlarmaAndroid>.broadcast();
|
|
static bool _handlerInstalado = false;
|
|
|
|
@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': 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,
|
|
'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,
|
|
'volume': alarma.volumen,
|
|
});
|
|
if (programada != true) {
|
|
throw StateError(
|
|
'Android no pudo programar una alarma exacta. Revisa el permiso de alarmas exactas.',
|
|
);
|
|
}
|
|
}
|
|
|
|
@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',
|
|
);
|
|
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?>>(
|
|
'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;
|
|
}
|
|
|
|
Future<void> _logAndInvokeVoid(String method, Map<String, Object?> args) {
|
|
debugPrint('[PluriWave][alarmas] $method $args');
|
|
return _channel.invokeMethod<void>(method, args);
|
|
}
|
|
|
|
static void _instalarHandler(MethodChannel channel) {
|
|
if (_handlerInstalado) return;
|
|
_handlerInstalado = true;
|
|
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);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|