feat(eq): android auto custom equalizer and robust device detection
- MainActivity: onListen re-emits the current active device and registers the audio device callback idempotently, so recreated activities resync instead of freezing the active-device id on a disconnected device. - servicio_dispositivo_audio: resubscribir() re-opens the event channel; estado_ecualizador exposes refrescarDispositivoActual() with an in-flight guard, invoked on app resume and when opening advanced EQ options, clearing stale green-dot device selections. - navegacion_auto/servicio_audio: new 'Personalizado' browse tree in Android Auto (5 band folders, 13 gain steps each) applied live via setBanda; preset and gain taps persist at device level when multi-device EQ is active and respect station/matrix overrides, with apply-before-persist ordering and children-changed notifications. - l10n: regenerate stale generated localizations; add rxdart as direct dependency for the subscribeToChildren override.
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pluriwave/estado/estado_ecualizador.dart';
|
||||
import 'package:pluriwave/estado/estado_radio.dart';
|
||||
@@ -1402,4 +1404,153 @@ void main() {
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Fix: robust active-device detection (stale green dot)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
group('EstadoEcualizador.refrescarDispositivoActual', () {
|
||||
const btDevice = DispositivoAudio(
|
||||
id: 'bt_a2dp:AA:BB:CC:DD:EE:FF',
|
||||
tipo: TipoDispositivo.bluetoothA2dp,
|
||||
nombre: 'BT Speaker',
|
||||
);
|
||||
|
||||
test(
|
||||
'resubscribes the event channel and re-seeds the id from a fresh '
|
||||
'device query',
|
||||
() async {
|
||||
final fakeDispositivo = FakeServicioDispositivoAudio();
|
||||
final eq = EstadoEcualizador(
|
||||
audio: FakeServicioAudio(),
|
||||
servicio: FakeServicioEcualizador(eqMultiDeviceEnabled: true),
|
||||
dispositivoAudio: fakeDispositivo,
|
||||
emisoraActualUuid: () => null,
|
||||
);
|
||||
await eq.cargarPersistido();
|
||||
expect(eq.dispositivoActualId, equals('builtin_speaker'));
|
||||
|
||||
// The native side changed device but the stale subscription lost the
|
||||
// event (activity recreation): only the fake's cached value moved.
|
||||
fakeDispositivo.establecerDispositivoSinEvento(btDevice);
|
||||
|
||||
await eq.refrescarDispositivoActual();
|
||||
|
||||
expect(fakeDispositivo.resubscribirLlamadas, equals(1));
|
||||
expect(eq.dispositivoActualId, equals(btDevice.id));
|
||||
eq.dispose();
|
||||
},
|
||||
);
|
||||
|
||||
test('is a no-op when the multi-device toggle is off', () async {
|
||||
final fakeDispositivo = FakeServicioDispositivoAudio();
|
||||
final eq = EstadoEcualizador(
|
||||
audio: FakeServicioAudio(),
|
||||
servicio: FakeServicioEcualizador(eqMultiDeviceEnabled: false),
|
||||
dispositivoAudio: fakeDispositivo,
|
||||
emisoraActualUuid: () => null,
|
||||
);
|
||||
await eq.cargarPersistido();
|
||||
|
||||
await eq.refrescarDispositivoActual();
|
||||
|
||||
expect(fakeDispositivo.resubscribirLlamadas, equals(0));
|
||||
expect(eq.dispositivoActualId, isNull);
|
||||
eq.dispose();
|
||||
});
|
||||
|
||||
test('is a no-op when no device service is injected', () async {
|
||||
final eq = EstadoEcualizador(
|
||||
audio: FakeServicioAudio(),
|
||||
servicio: FakeServicioEcualizador(eqMultiDeviceEnabled: true),
|
||||
emisoraActualUuid: () => null,
|
||||
);
|
||||
await eq.cargarPersistido();
|
||||
|
||||
// Must not throw despite the missing service.
|
||||
await eq.refrescarDispositivoActual();
|
||||
|
||||
expect(eq.dispositivoActualId, isNull);
|
||||
eq.dispose();
|
||||
});
|
||||
|
||||
test('is safe to call repeatedly', () async {
|
||||
final fakeDispositivo = FakeServicioDispositivoAudio();
|
||||
final eq = EstadoEcualizador(
|
||||
audio: FakeServicioAudio(),
|
||||
servicio: FakeServicioEcualizador(eqMultiDeviceEnabled: true),
|
||||
dispositivoAudio: fakeDispositivo,
|
||||
emisoraActualUuid: () => null,
|
||||
);
|
||||
await eq.cargarPersistido();
|
||||
fakeDispositivo.establecerDispositivoSinEvento(btDevice);
|
||||
|
||||
await eq.refrescarDispositivoActual();
|
||||
await eq.refrescarDispositivoActual();
|
||||
await eq.refrescarDispositivoActual();
|
||||
|
||||
expect(fakeDispositivo.resubscribirLlamadas, equals(3));
|
||||
expect(eq.dispositivoActualId, equals(btDevice.id));
|
||||
eq.dispose();
|
||||
});
|
||||
|
||||
test(
|
||||
'a failing resubscribe still falls back to builtin_speaker via the '
|
||||
'seeding error path instead of throwing',
|
||||
() async {
|
||||
final fakeDispositivo = FakeServicioDispositivoAudioThrows();
|
||||
final eq = EstadoEcualizador(
|
||||
audio: FakeServicioAudio(),
|
||||
servicio: FakeServicioEcualizador(eqMultiDeviceEnabled: true),
|
||||
dispositivoAudio: fakeDispositivo,
|
||||
emisoraActualUuid: () => null,
|
||||
);
|
||||
await eq.cargarPersistido();
|
||||
|
||||
await eq.refrescarDispositivoActual();
|
||||
|
||||
expect(fakeDispositivo.resubscribirLlamadas, equals(1));
|
||||
expect(eq.dispositivoActualId, equals('builtin_speaker'));
|
||||
eq.dispose();
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'overlapping calls share one in-flight refresh (single resubscribe)',
|
||||
() async {
|
||||
final fakeDispositivo = _FakeDispositivoAudioResubscribirLento();
|
||||
final eq = EstadoEcualizador(
|
||||
audio: FakeServicioAudio(),
|
||||
servicio: FakeServicioEcualizador(eqMultiDeviceEnabled: true),
|
||||
dispositivoAudio: fakeDispositivo,
|
||||
emisoraActualUuid: () => null,
|
||||
);
|
||||
await eq.cargarPersistido();
|
||||
|
||||
// Second call lands while the first is still awaiting resubscribir.
|
||||
final primero = eq.refrescarDispositivoActual();
|
||||
final segundo = eq.refrescarDispositivoActual();
|
||||
fakeDispositivo.completarResubscribir();
|
||||
await Future.wait([primero, segundo]);
|
||||
|
||||
expect(fakeDispositivo.resubscribirLlamadas, equals(1));
|
||||
eq.dispose();
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// Fake whose [resubscribir] stays pending until [completarResubscribir]
|
||||
/// runs — creates the overlap window for the in-flight-guard test above.
|
||||
class _FakeDispositivoAudioResubscribirLento
|
||||
extends FakeServicioDispositivoAudio {
|
||||
final _resubscribir = Completer<void>();
|
||||
|
||||
void completarResubscribir() => _resubscribir.complete();
|
||||
|
||||
@override
|
||||
Future<void> resubscribir() async {
|
||||
resubscribirLlamadas++;
|
||||
await _resubscribir.future;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user