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 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 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 get eventosAlarma; Future programar(AlarmaMusical alarma); Future cancelar(String alarmaId); Future ocultarNotificacionAlarma(String alarmaId); Future detenerSonidoNativo(String alarmaId); Future solicitarPermisoAlarmasExactas(); Future solicitarPermisoNotificaciones(); Future solicitarPermisoPantallaCompleta(); Future confirmarAudioFlutter(String alarmaId); Future diagnostico(); Future obtenerEventoInicial(); } class ServicioAlarmasAndroid implements PuertoAlarmasAndroid { ServicioAlarmasAndroid({ MethodChannel channel = const MethodChannel('pluriwave/alarm_scheduler'), }) : _channel = channel { _instalarHandler(_channel); } final MethodChannel _channel; static final _eventosController = StreamController.broadcast(); static bool _handlerInstalado = false; @override Stream get eventosAlarma => _eventosController.stream; @override Future 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('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 cancelar(String alarmaId) => _logAndInvokeVoid('cancelAlarm', {'id': alarmaId}); @override Future ocultarNotificacionAlarma(String alarmaId) => _logAndInvokeVoid('dismissAlarmNotification', {'id': alarmaId}); @override Future detenerSonidoNativo(String alarmaId) => _logAndInvokeVoid('stopNativeAlarmSound', {'id': alarmaId}); @override Future confirmarAudioFlutter(String alarmaId) => _logAndInvokeVoid('confirmFlutterAudio', {'id': alarmaId}); @override Future solicitarPermisoAlarmasExactas() async { final abierto = await _channel.invokeMethod( 'requestExactAlarmPermission', ); return abierto ?? false; } @override Future solicitarPermisoNotificaciones() async { final abierto = await _channel.invokeMethod( 'requestPostNotificationsPermission', ); return abierto ?? false; } @override Future solicitarPermisoPantallaCompleta() async { final abierto = await _channel.invokeMethod( 'requestFullScreenIntentPermission', ); return abierto ?? false; } @override Future diagnostico() async { debugPrint('[PluriWave][alarmas] diagnostico android'); final raw = await _channel.invokeMethod>( '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 obtenerEventoInicial() async { final raw = await _channel.invokeMethod>( '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 _logAndInvokeVoid(String method, Map args) { debugPrint('[PluriWave][alarmas] $method $args'); return _channel.invokeMethod(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); } } }); } }