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