diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 24a4a5d..2479a05 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -68,6 +68,16 @@ + + + + diff --git a/android/app/src/main/kotlin/es/freetimelab/pluriwave/AlarmScheduler.kt b/android/app/src/main/kotlin/es/freetimelab/pluriwave/AlarmScheduler.kt index 11352a4..9650171 100644 --- a/android/app/src/main/kotlin/es/freetimelab/pluriwave/AlarmScheduler.kt +++ b/android/app/src/main/kotlin/es/freetimelab/pluriwave/AlarmScheduler.kt @@ -5,13 +5,19 @@ import android.app.PendingIntent import android.content.Context import android.content.Intent import android.os.Build +import android.util.Log import androidx.core.app.NotificationManagerCompat class AlarmScheduler(private val context: Context) { + private val tag = "PluriWave" private val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager fun scheduleAlarm(id: String, title: String, triggerAtMillis: Long, preNoticeAtMillis: Long) { + Log.d( + tag, + "alarm.schedule id=$id title=$title triggerAtMillis=$triggerAtMillis preNoticeAtMillis=$preNoticeAtMillis canExact=${canScheduleExactAlarms()}" + ) val alarmIntent = PendingIntent.getBroadcast( context, requestCode(id, 1), @@ -39,6 +45,7 @@ class AlarmScheduler(private val context: Context) { AlarmManager.AlarmClockInfo(triggerAtMillis, showIntent), alarmIntent ) + Log.d(tag, "alarm.schedule setAlarmClock OK id=$id") if (preNoticeAtMillis > System.currentTimeMillis()) { try { @@ -56,13 +63,18 @@ class AlarmScheduler(private val context: Context) { PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) ) + Log.d(tag, "alarm.schedule preNotice OK id=$id") } catch (_: SecurityException) { // The main alarm is already scheduled with setAlarmClock. + Log.w(tag, "alarm.schedule preNotice SecurityException id=$id") } + } else { + Log.d(tag, "alarm.schedule preNotice skipped id=$id") } } fun cancelAlarm(id: String) { + Log.d(tag, "alarm.cancel id=$id") for (slot in 1..3) { alarmManager.cancel( PendingIntent.getBroadcast( diff --git a/android/app/src/main/kotlin/es/freetimelab/pluriwave/MainActivity.kt b/android/app/src/main/kotlin/es/freetimelab/pluriwave/MainActivity.kt index 7659f3b..a54e128 100644 --- a/android/app/src/main/kotlin/es/freetimelab/pluriwave/MainActivity.kt +++ b/android/app/src/main/kotlin/es/freetimelab/pluriwave/MainActivity.kt @@ -2,6 +2,7 @@ package es.freetimelab.pluriwave import android.Manifest import android.app.ActivityNotFoundException +import android.content.ClipData import android.content.Intent import android.content.pm.PackageManager import android.net.Uri @@ -11,13 +12,17 @@ import android.os.Environment import android.os.Handler import android.os.Looper import android.provider.DocumentsContract +import android.util.Log import androidx.core.app.NotificationManagerCompat +import androidx.core.content.FileProvider import com.ryanheise.audioservice.AudioServiceActivity import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.MethodChannel +import java.io.File class MainActivity : AudioServiceActivity() { + private val tag = "PluriWave" private val visualizerChannel = "pluriwave/audio_visualizer" private val alarmChannel = "pluriwave/alarm_scheduler" private val fileActionsChannel = "pluriwave/file_actions" @@ -59,7 +64,9 @@ class MainActivity : AudioServiceActivity() { val title = call.argument("title") ?: "PluriWave" val triggerAtMillis = call.argument("triggerAtMillis") val preNoticeAtMillis = call.argument("preNoticeAtMillis") ?: 0L + Log.d(tag, "alarm.channel scheduleAlarm id=$id triggerAtMillis=$triggerAtMillis preNoticeAtMillis=$preNoticeAtMillis") if (id == null || triggerAtMillis == null) { + Log.w(tag, "alarm.channel scheduleAlarm invalid id=$id triggerAtMillis=$triggerAtMillis") result.error("INVALID_ALARM", "Missing alarm id or trigger time", null) } else { alarmScheduler.scheduleAlarm(id, title, triggerAtMillis, preNoticeAtMillis) @@ -68,6 +75,7 @@ class MainActivity : AudioServiceActivity() { } "cancelAlarm" -> { val id = call.argument("id") + Log.d(tag, "alarm.channel cancelAlarm id=$id") if (id == null) { result.error("INVALID_ALARM", "Missing alarm id", null) } else { @@ -77,6 +85,7 @@ class MainActivity : AudioServiceActivity() { } "dismissAlarmNotification" -> { val id = call.argument("id") + Log.d(tag, "alarm.channel dismissAlarmNotification id=$id") if (id == null) { result.error("INVALID_ALARM", "Missing alarm id", null) } else { @@ -85,6 +94,7 @@ class MainActivity : AudioServiceActivity() { } } "diagnostics" -> { + Log.d(tag, "alarm.channel diagnostics") result.success( mapOf( "canScheduleExactAlarms" to alarmScheduler.canScheduleExactAlarms(), @@ -95,7 +105,9 @@ class MainActivity : AudioServiceActivity() { ) } "getInitialAlarmIntent" -> { - result.success(alarmPayload(intent)) + val payload = alarmPayload(intent) + Log.d(tag, "alarm.channel getInitialAlarmIntent payload=$payload") + result.success(payload) intent?.removeExtra(PluriWaveAlarmReceiver.EXTRA_ALARM_ACTION) } else -> result.notImplemented() @@ -109,12 +121,23 @@ class MainActivity : AudioServiceActivity() { when (call.method) { "openDirectory" -> { val path = call.argument("path") + Log.d(tag, "file_actions.openDirectory path=$path") if (path.isNullOrBlank()) { result.success(false) } else { result.success(openDirectory(path)) } } + "openFile" -> { + val path = call.argument("path") + val mimeType = call.argument("mimeType") ?: "audio/*" + Log.d(tag, "file_actions.openFile path=$path mimeType=$mimeType") + if (path.isNullOrBlank()) { + result.success(false) + } else { + result.success(openFile(path, mimeType)) + } + } else -> result.notImplemented() } } @@ -125,6 +148,7 @@ class MainActivity : AudioServiceActivity() { setIntent(intent) val payload = alarmPayload(intent) if (payload.isNotEmpty()) { + Log.d(tag, "alarm.channel onNewIntent payload=$payload") alarmMethodChannel?.invokeMethod("alarmFired", payload) } } @@ -156,14 +180,46 @@ class MainActivity : AudioServiceActivity() { } return try { startActivity(intent) + Log.d(tag, "file_actions.openDirectory launched path=$path") true } catch (_: ActivityNotFoundException) { + Log.w(tag, "file_actions.openDirectory no activity for path=$path") false - } catch (_: Throwable) { + } catch (error: Throwable) { + Log.e(tag, "file_actions.openDirectory failed path=$path", error) false } } + private fun openFile(path: String, mimeType: String): Boolean { + val file = File(path) + if (!file.exists()) { + Log.w(tag, "file_actions.openFile missing path=$path") + return false + } + return try { + val uri = FileProvider.getUriForFile( + this, + "$packageName.fileprovider", + file + ) + val intent = Intent(Intent.ACTION_VIEW).apply { + setDataAndType(uri, mimeType) + clipData = ClipData.newUri(contentResolver, "recording", uri) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + startActivity(Intent.createChooser(intent, "Abrir grabación")) + Log.d(tag, "file_actions.openFile launched path=$path") + true + } catch (_: ActivityNotFoundException) { + Log.w(tag, "file_actions.openFile no viewer path=$path; opening parent") + openDirectory(file.parentFile?.absolutePath ?: path) + } catch (error: Throwable) { + Log.e(tag, "file_actions.openFile failed path=$path; opening parent", error) + openDirectory(file.parentFile?.absolutePath ?: path) + } + } + private fun directoryTreeUri(path: String): Uri? { val external = Environment.getExternalStorageDirectory()?.absolutePath ?: return null if (!path.startsWith(external)) return null diff --git a/android/app/src/main/kotlin/es/freetimelab/pluriwave/PluriWaveAlarmReceiver.kt b/android/app/src/main/kotlin/es/freetimelab/pluriwave/PluriWaveAlarmReceiver.kt index 3f6378b..7394356 100644 --- a/android/app/src/main/kotlin/es/freetimelab/pluriwave/PluriWaveAlarmReceiver.kt +++ b/android/app/src/main/kotlin/es/freetimelab/pluriwave/PluriWaveAlarmReceiver.kt @@ -7,13 +7,18 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.os.Build +import android.util.Log import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat class PluriWaveAlarmReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { - val alarmId = intent.getStringExtra(EXTRA_ALARM_ID) ?: return + val alarmId = intent.getStringExtra(EXTRA_ALARM_ID) ?: run { + Log.w(TAG, "alarm.receiver missing alarmId action=${intent.action}") + return + } val title = intent.getStringExtra(EXTRA_ALARM_TITLE) ?: "PluriWave" + Log.d(TAG, "alarm.receiver action=${intent.action} id=$alarmId title=$title") when (intent.action) { ACTION_FIRE -> { @@ -24,7 +29,12 @@ class PluriWaveAlarmReceiver : BroadcastReceiver() { putExtra(EXTRA_ALARM_ACTION, ACTION_FIRE) } showFireNotification(context, alarmId, title, launch) - context.startActivity(launch) + try { + context.startActivity(launch) + Log.d(TAG, "alarm.receiver fire startActivity OK id=$alarmId") + } catch (error: Throwable) { + Log.e(TAG, "alarm.receiver fire startActivity ERROR id=$alarmId", error) + } } ACTION_PRE_NOTICE -> { showPreNoticeNotification(context, alarmId, title) @@ -37,8 +47,14 @@ class PluriWaveAlarmReceiver : BroadcastReceiver() { putExtra(EXTRA_ALARM_TITLE, title) putExtra(EXTRA_ALARM_ACTION, ACTION_SKIP_NEXT) } - context.startActivity(launch) + try { + context.startActivity(launch) + Log.d(TAG, "alarm.receiver skipNext startActivity OK id=$alarmId") + } catch (error: Throwable) { + Log.e(TAG, "alarm.receiver skipNext startActivity ERROR id=$alarmId", error) + } } + else -> Log.w(TAG, "alarm.receiver unknown action=${intent.action} id=$alarmId") } } @@ -72,7 +88,9 @@ class PluriWaveAlarmReceiver : BroadcastReceiver() { fireNotificationIdForAlarm(alarmId), notification, ) - } catch (_: SecurityException) { + Log.d(TAG, "alarm.notification fire shown id=$alarmId") + } catch (error: SecurityException) { + Log.e(TAG, "alarm.notification fire SecurityException id=$alarmId", error) } } @@ -114,7 +132,12 @@ class PluriWaveAlarmReceiver : BroadcastReceiver() { .addAction(0, "Omitir siguiente", skipNextIntent) .build() - NotificationManagerCompat.from(context).notify(notificationIdForAlarm(alarmId), notification) + try { + NotificationManagerCompat.from(context).notify(notificationIdForAlarm(alarmId), notification) + Log.d(TAG, "alarm.notification preNotice shown id=$alarmId") + } catch (error: SecurityException) { + Log.e(TAG, "alarm.notification preNotice SecurityException id=$alarmId", error) + } } private fun ensureFireChannel(context: Context) { @@ -155,6 +178,7 @@ class PluriWaveAlarmReceiver : BroadcastReceiver() { private fun requestCode(id: String, slot: Int): Int = 47 * id.hashCode() + slot companion object { + const val TAG = "PluriWave" const val CHANNEL_ID = "pluriwave_alarm_pre_notice" const val FIRE_CHANNEL_ID = "pluriwave_alarm_fire" const val ACTION_FIRE = "es.freetimelab.pluriwave.alarm.FIRE" diff --git a/android/app/src/main/res/xml/pluriwave_file_paths.xml b/android/app/src/main/res/xml/pluriwave_file_paths.xml new file mode 100644 index 0000000..8c6c1ea --- /dev/null +++ b/android/app/src/main/res/xml/pluriwave_file_paths.xml @@ -0,0 +1,15 @@ + + + + + + + diff --git a/lib/estado/estado_alarmas.dart b/lib/estado/estado_alarmas.dart index 31e9fcb..01e9429 100644 --- a/lib/estado/estado_alarmas.dart +++ b/lib/estado/estado_alarmas.dart @@ -49,17 +49,20 @@ class EstadoAlarmas extends ChangeNotifier { } Future inicializar() async { + debugPrint('[PluriWave][alarmas] inicializar'); _cargando = true; _error = null; notifyListeners(); try { final config = await servicio.cargar(); _aplicar(config); + debugPrint('[PluriWave][alarmas] cargadas=${_alarmas.length} vacaciones=${_vacaciones.length} excepciones=${_excepciones.length}'); await _sincronizarTodas(); await cargarDiagnostico(); _activarRefresco(); } catch (e) { _error = 'No se pudieron cargar las alarmas: $e'; + debugPrint('[PluriWave][alarmas] inicializar ERROR $e'); } finally { _cargando = false; notifyListeners(); @@ -67,10 +70,13 @@ class EstadoAlarmas extends ChangeNotifier { } Future guardarAlarma(AlarmaMusical alarma) async { + debugPrint('[PluriWave][alarmas] guardar id=${alarma.id} activa=${alarma.activa} hora=${alarma.hora}:${alarma.minuto} tipo=${alarma.tipoProgramacion.name}'); final config = await servicio.guardarAlarma(alarma); _aplicar(config); try { - await android.programar(_alarmas.firstWhere((a) => a.id == alarma.id)); + final guardada = _alarmas.firstWhere((a) => a.id == alarma.id); + debugPrint('[PluriWave][alarmas] guardada id=${guardada.id} proxima=${guardada.proximaEjecucion?.toIso8601String()}'); + await android.programar(guardada); } catch (e) { _error = 'Alarma guardada, pero Android no pudo programarla todavía: $e'; @@ -79,6 +85,7 @@ class EstadoAlarmas extends ChangeNotifier { } Future refrescarProgramacion() async { + debugPrint('[PluriWave][alarmas] refrescar programacion'); final config = await servicio.recalcularTodas(); _aplicar(config); await _sincronizarTodas(); @@ -86,6 +93,7 @@ class EstadoAlarmas extends ChangeNotifier { } Future eliminarAlarma(String id) async { + debugPrint('[PluriWave][alarmas] eliminar id=$id'); final config = await servicio.eliminarAlarma(id); _aplicar(config); await android.cancelar(id); @@ -97,6 +105,7 @@ class EstadoAlarmas extends ChangeNotifier { } Future saltarProxima(String alarmaId) async { + debugPrint('[PluriWave][alarmas] saltar proxima id=$alarmaId'); final config = await servicio.saltarProxima(alarmaId); _aplicar(config); AlarmaMusical? alarma; @@ -113,6 +122,7 @@ class EstadoAlarmas extends ChangeNotifier { } Future guardarVacaciones(List vacaciones) async { + debugPrint('[PluriWave][alarmas] guardar vacaciones count=${vacaciones.length}'); final config = await servicio.guardarVacaciones(vacaciones); _aplicar(config); await _sincronizarTodas(); @@ -121,11 +131,13 @@ class EstadoAlarmas extends ChangeNotifier { Future posponerAlarma(AlarmaMusical alarma, int minutos) async { final proxima = DateTime.now().add(Duration(minutes: minutos)); + debugPrint('[PluriWave][alarmas] posponer id=${alarma.id} minutos=$minutos proxima=${proxima.toIso8601String()}'); await android.ocultarNotificacionAlarma(alarma.id); await android.programar(alarma.copyWith(proximaEjecucion: proxima)); } Future finalizarEjecucion(String alarmaId) async { + debugPrint('[PluriWave][alarmas] finalizar ejecucion id=$alarmaId'); await android.ocultarNotificacionAlarma(alarmaId); await refrescarProgramacion(); } @@ -150,13 +162,15 @@ class EstadoAlarmas extends ChangeNotifier { Future cargarDiagnostico() async { try { _diagnostico = await android.diagnostico(); - } catch (_) { + } catch (e) { + debugPrint('[PluriWave][alarmas] diagnostico ERROR $e'); _diagnostico = null; } notifyListeners(); } Future _sincronizarTodas() async { + debugPrint('[PluriWave][alarmas] sincronizar todas count=${_alarmas.length}'); for (final alarma in _alarmas) { await android.programar(alarma); } @@ -188,6 +202,7 @@ class EstadoAlarmas extends ChangeNotifier { if (proxima.isAfter(ahora)) continue; final key = '${alarma.id}:${proxima.millisecondsSinceEpoch}'; if (_ejecucionesEmitidas.add(key)) { + debugPrint('[PluriWave][alarmas] vencida local id=${alarma.id} proxima=${proxima.toIso8601String()}'); _alarmasVencidasController.add(alarma); } } diff --git a/lib/estado/estado_radio.dart b/lib/estado/estado_radio.dart index ef222c4..fc3303a 100644 --- a/lib/estado/estado_radio.dart +++ b/lib/estado/estado_radio.dart @@ -464,7 +464,7 @@ class EstadoRadio extends ChangeNotifier { ); } _resultadosBusqueda = nuevaLista; - // _buscarPaginaFiltrada actualiza offset/hayMas usando p?ginas crudas. + // _buscarPaginaFiltrada actualiza offset/hayMas usando páginas crudas. _hayMasBusqueda = _hayMasBusqueda && pagina.isNotEmpty; } catch (_) { _errorController.add('No se pudieron cargar mas emisoras.'); @@ -644,7 +644,21 @@ class EstadoRadio extends ChangeNotifier { Future abrirUltimaGrabacion() async { final archivo = ultimaGrabacion; - if (archivo == null || !await archivo.exists()) return false; + if (archivo == null || !await archivo.exists()) { + debugPrint('[PluriWave][recordings] last recording missing'); + return false; + } + debugPrint('[PluriWave][recordings] opening last file: ${archivo.path}'); + if (!kIsWeb && Platform.isAndroid) { + final abierto = await _fileActionsChannel.invokeMethod( + 'openFile', + { + 'path': archivo.path, + 'mimeType': 'audio/*', + }, + ); + return abierto ?? false; + } return launchUrl( Uri.file(archivo.path), mode: LaunchMode.externalApplication, diff --git a/lib/servicios/servicio_alarmas_android.dart b/lib/servicios/servicio_alarmas_android.dart index 33611dc..5efa275 100644 --- a/lib/servicios/servicio_alarmas_android.dart +++ b/lib/servicios/servicio_alarmas_android.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import '../modelos/alarma_musical.dart'; @@ -64,9 +65,15 @@ class ServicioAlarmasAndroid { Future programar(AlarmaMusical alarma) async { final proxima = alarma.proximaEjecucion; 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()}', + ); await _channel.invokeMethod('scheduleAlarm', { 'id': alarma.id, 'title': alarma.nombre, @@ -77,16 +84,21 @@ class ServicioAlarmasAndroid { } Future cancelar(String alarmaId) => - _channel.invokeMethod('cancelAlarm', {'id': alarmaId}); + _logAndInvokeVoid('cancelAlarm', {'id': alarmaId}); - Future ocultarNotificacionAlarma(String alarmaId) => _channel - .invokeMethod('dismissAlarmNotification', {'id': alarmaId}); + Future ocultarNotificacionAlarma(String alarmaId) => + _logAndInvokeVoid('dismissAlarmNotification', {'id': alarmaId}); Future diagnostico() async { + debugPrint('[PluriWave][alarmas] diagnostico android'); final raw = await _channel.invokeMethod>( 'diagnostics', ); - return DiagnosticoAlarmasAndroid.fromMap(raw ?? const {}); + final diag = DiagnosticoAlarmasAndroid.fromMap(raw ?? const {}); + debugPrint( + '[PluriWave][alarmas] diagnostico exactas=${diag.puedeProgramarExactas} notificaciones=${diag.notificacionesPermitidas} sdk=${diag.versionSdk} fabricante=${diag.fabricante}', + ); + return diag; } Future obtenerEventoInicial() async { @@ -95,9 +107,17 @@ class ServicioAlarmasAndroid { ); 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; @@ -107,6 +127,9 @@ class ServicioAlarmasAndroid { 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); } }