- 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.
207 lines
6.8 KiB
Dart
207 lines
6.8 KiB
Dart
import 'package:flutter/services.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:pluriwave/modelos/dispositivo_audio.dart';
|
|
import 'package:pluriwave/servicios/servicio_dispositivo_audio.dart';
|
|
|
|
// Type int constants from the platform channel protocol (design doc):
|
|
// 2 = builtin_speaker, 3 = wired_headset, 8 = bt_a2dp, 14 = usb_headset
|
|
|
|
void main() {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
|
|
group('ServicioDispositivoAudioReal', () {
|
|
const methodChannelName = 'pluriwave/audio_devices';
|
|
|
|
late ServicioDispositivoAudioReal servicio;
|
|
|
|
setUp(() {
|
|
servicio = ServicioDispositivoAudioReal();
|
|
});
|
|
|
|
tearDown(() async {
|
|
await servicio.dispose();
|
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
|
.setMockMethodCallHandler(
|
|
const MethodChannel(methodChannelName),
|
|
null,
|
|
);
|
|
});
|
|
|
|
void stubGetActiveDevice(Map<String, dynamic> response) {
|
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
|
.setMockMethodCallHandler(const MethodChannel(methodChannelName), (
|
|
call,
|
|
) async {
|
|
if (call.method == 'getActiveDevice') return response;
|
|
return null;
|
|
});
|
|
}
|
|
|
|
void stubRequestBluetoothConnect(bool? response) {
|
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
|
.setMockMethodCallHandler(const MethodChannel(methodChannelName), (
|
|
call,
|
|
) async {
|
|
if (call.method == 'requestBluetoothConnect') return response;
|
|
return null;
|
|
});
|
|
}
|
|
|
|
test('obtenerDispositivoActual maps builtin_speaker (type=2)', () async {
|
|
stubGetActiveDevice({
|
|
'id': 'builtin_speaker',
|
|
'type': 2,
|
|
'name': 'Speaker',
|
|
});
|
|
final device = await servicio.obtenerDispositivoActual();
|
|
expect(device.id, 'builtin_speaker');
|
|
expect(device.tipo, TipoDispositivo.altavozInterno);
|
|
expect(device.nombre, 'Speaker');
|
|
});
|
|
|
|
test('obtenerDispositivoActual maps wired_headset (type=3)', () async {
|
|
stubGetActiveDevice({
|
|
'id': 'wired_headset',
|
|
'type': 3,
|
|
'name': 'Wired Headset',
|
|
});
|
|
final device = await servicio.obtenerDispositivoActual();
|
|
expect(device.id, 'wired_headset');
|
|
expect(device.tipo, TipoDispositivo.auricularesCable);
|
|
});
|
|
|
|
test('obtenerDispositivoActual maps bt_a2dp (type=8)', () async {
|
|
stubGetActiveDevice({
|
|
'id': 'bt_a2dp:AA:BB:CC:DD:EE:FF',
|
|
'type': 8,
|
|
'name': 'My BT Device',
|
|
});
|
|
final device = await servicio.obtenerDispositivoActual();
|
|
expect(device.id, 'bt_a2dp:AA:BB:CC:DD:EE:FF');
|
|
expect(device.tipo, TipoDispositivo.bluetoothA2dp);
|
|
});
|
|
|
|
test('obtenerDispositivoActual maps usb_headset (type=14)', () async {
|
|
stubGetActiveDevice({
|
|
'id': 'usb_headset:0001:0002',
|
|
'type': 14,
|
|
'name': 'USB Headset',
|
|
});
|
|
final device = await servicio.obtenerDispositivoActual();
|
|
expect(device.id, 'usb_headset:0001:0002');
|
|
expect(device.tipo, TipoDispositivo.usbAudio);
|
|
});
|
|
|
|
test('obtenerDispositivoActual maps unknown type to desconocido', () async {
|
|
stubGetActiveDevice({
|
|
'id': 'unknown_device',
|
|
'type': 99,
|
|
'name': 'Unknown',
|
|
});
|
|
final device = await servicio.obtenerDispositivoActual();
|
|
expect(device.tipo, TipoDispositivo.desconocido);
|
|
});
|
|
|
|
test('obtenerDispositivoActual updates dispositivoActual cache', () async {
|
|
stubGetActiveDevice({
|
|
'id': 'builtin_speaker',
|
|
'type': 2,
|
|
'name': 'Speaker',
|
|
});
|
|
await servicio.obtenerDispositivoActual();
|
|
expect(servicio.dispositivoActual?.id, 'builtin_speaker');
|
|
});
|
|
|
|
test('onDispositivoCambiado is a broadcast stream', () {
|
|
expect(servicio.onDispositivoCambiado.isBroadcast, isTrue);
|
|
});
|
|
|
|
// ── bt-device-identity Phase 2: permission contract ──────────────────────
|
|
|
|
test(
|
|
'solicitarPermisoBluetooth invokes requestBluetoothConnect and '
|
|
'returns true when granted',
|
|
() async {
|
|
stubRequestBluetoothConnect(true);
|
|
final granted = await servicio.solicitarPermisoBluetooth();
|
|
expect(granted, isTrue);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'solicitarPermisoBluetooth returns false when the platform denies',
|
|
() async {
|
|
stubRequestBluetoothConnect(false);
|
|
final granted = await servicio.solicitarPermisoBluetooth();
|
|
expect(granted, isFalse);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'solicitarPermisoBluetooth returns false when the platform channel '
|
|
'returns null',
|
|
() async {
|
|
stubRequestBluetoothConnect(null);
|
|
final granted = await servicio.solicitarPermisoBluetooth();
|
|
expect(granted, isFalse);
|
|
},
|
|
);
|
|
|
|
// ── Fix: robust active-device detection (stale green dot) ────────────────
|
|
|
|
test(
|
|
'resubscribir sends cancel+listen to the event channel and forwards '
|
|
'the device the fresh onListen emits',
|
|
() async {
|
|
var listens = 0;
|
|
var cancels = 0;
|
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
|
.setMockStreamHandler(
|
|
const EventChannel(methodChannelName),
|
|
MockStreamHandler.inline(
|
|
onListen: (arguments, events) {
|
|
listens++;
|
|
// Mirrors the native onListen immediate resync: every
|
|
// (re)subscription receives the current active device.
|
|
events.success({
|
|
'id': 'bt_a2dp:AA:BB:CC:DD:EE:FF',
|
|
'type': 8,
|
|
'name': 'My BT Device',
|
|
});
|
|
},
|
|
onCancel: (arguments) => cancels++,
|
|
),
|
|
);
|
|
addTearDown(() {
|
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
|
.setMockStreamHandler(
|
|
const EventChannel(methodChannelName),
|
|
null,
|
|
);
|
|
});
|
|
|
|
final servicioLocal = ServicioDispositivoAudioReal();
|
|
addTearDown(servicioLocal.dispose);
|
|
|
|
// The constructor's initial subscription delivers the first resync.
|
|
final primero = await servicioLocal.onDispositivoCambiado.first;
|
|
expect(primero.id, 'bt_a2dp:AA:BB:CC:DD:EE:FF');
|
|
expect(listens, 1);
|
|
expect(cancels, 0);
|
|
|
|
final segundoFuturo = servicioLocal.onDispositivoCambiado.first;
|
|
await servicioLocal.resubscribir();
|
|
final segundo = await segundoFuturo;
|
|
|
|
expect(cancels, 1);
|
|
expect(listens, 2);
|
|
expect(segundo.tipo, TipoDispositivo.bluetoothA2dp);
|
|
expect(
|
|
servicioLocal.dispositivoActual?.id,
|
|
'bt_a2dp:AA:BB:CC:DD:EE:FF',
|
|
);
|
|
},
|
|
);
|
|
});
|
|
}
|