fix(devices): cache platform device names, dedupe placeholder ids, purge collided EQ entries
Build & Deploy PluriWave / Análisis de código (push) Successful in 36s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 1m47s

Dart half of bt-device-identity. EstadoEcualizador now caches each
device's platform-reported name in memory so the settings screen
shows the device's own Bluetooth name instead of its raw id when no
custom rename exists, and skips auto-creating preset entries for the
composite-placeholder sentinel. Enabling multi-device EQ triggers the
Bluetooth permission request through the new channel contract. A
flag-guarded one-time migration purges only entries keyed by the
exact literal placeholder id from the three per-device preference
maps, since those collided entries cannot be attributed to a device.

Work unit 2/2 of bt-device-identity (Dart state + migration).
This commit is contained in:
2026-07-11 00:25:00 +02:00
parent 224763bca3
commit b17c582572
11 changed files with 711 additions and 7 deletions
@@ -37,6 +37,16 @@ void main() {
});
}
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',
@@ -105,5 +115,36 @@ void main() {
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);
},
);
});
}
@@ -78,5 +78,17 @@ void main() {
test('dispose closes the stream without error', () async {
await expectLater(fake.dispose(), completes);
});
// bt-device-identity Task 2.5: interface-completeness — every concrete
// ServicioDispositivoAudio must implement solicitarPermisoBluetooth.
test(
'solicitarPermisoBluetooth increments the call counter and returns '
'the configured value',
() async {
final granted = await fake.solicitarPermisoBluetooth();
expect(granted, isTrue);
expect(fake.solicitarPermisoBluetoothCalls, equals(1));
},
);
});
}
@@ -24,6 +24,9 @@ class NullServicioDispositivoAudio extends ServicioDispositivoAudio {
);
}
@override
Future<bool> solicitarPermisoBluetooth() async => true;
@override
Future<void> dispose() async {}
}
@@ -229,4 +229,181 @@ void main() {
expect(restored.nombresDispositivos['bt_a2dp:AA:BB'], equals('My Speaker'));
});
});
// ---------------------------------------------------------------------------
// bt-device-identity Phase 5: one-time guarded migration purge
// ---------------------------------------------------------------------------
group('ServicioEcualizador — placeholder migration purge (bt-device-identity Phase 5)', () {
const placeholderKey = 'bt_a2dp:02:00:00:00:00:00';
const stableKey = 'bt_a2dp:AA:BB:CC:DD:EE:FF';
test(
'5.1 migration removes only exact placeholder entries from presetsPorDispositivo',
() async {
final prefs = await SharedPreferences.getInstance();
final servicio = ServicioEcualizador(prefs: prefs);
await servicio.guardarPresetDispositivo(
placeholderKey,
PresetEcualizador.jazz,
);
await servicio.guardarPresetDispositivo(
stableKey,
PresetEcualizador.rock,
);
await servicio.migrarClavesPlaceholder();
final config = await servicio.cargar();
expect(
config.presetsDispositivo.containsKey(placeholderKey),
isFalse,
);
expect(
config.presetsDispositivo[stableKey],
equals(PresetEcualizador.rock),
);
},
);
test('5.2 matrix keys purge only the placeholder segment', () async {
final prefs = await SharedPreferences.getInstance();
final servicio = ServicioEcualizador(prefs: prefs);
await servicio.guardarPresetMatriz(
'station1:$placeholderKey',
PresetEcualizador.jazz,
);
await servicio.guardarPresetMatriz(
'station1:$stableKey',
PresetEcualizador.rock,
);
await servicio.migrarClavesPlaceholder();
final config = await servicio.cargar();
expect(
config.presetsMatriz.containsKey('station1:$placeholderKey'),
isFalse,
);
expect(
config.presetsMatriz['station1:$stableKey'],
equals(PresetEcualizador.rock),
);
});
test('5.3 near-miss keys are preserved (differs by one digit)', () async {
const nearMissKey = 'bt_a2dp:02:00:00:00:00:01';
final prefs = await SharedPreferences.getInstance();
final servicio = ServicioEcualizador(prefs: prefs);
await servicio.guardarPresetDispositivo(
nearMissKey,
PresetEcualizador.jazz,
);
await servicio.migrarClavesPlaceholder();
final config = await servicio.cargar();
expect(
config.presetsDispositivo[nearMissKey],
equals(PresetEcualizador.jazz),
);
});
test('5.4 migration runs once (second run is a true no-op)', () async {
final prefs = await SharedPreferences.getInstance();
final servicio = ServicioEcualizador(prefs: prefs);
await servicio.guardarPresetDispositivo(
placeholderKey,
PresetEcualizador.jazz,
);
await servicio.guardarPresetDispositivo(
stableKey,
PresetEcualizador.rock,
);
await servicio.migrarClavesPlaceholder();
// Re-seed the placeholder key directly, bypassing the flag, to prove
// the SECOND migration call is a true no-op (flag short-circuits)
// rather than merely finding nothing left to purge.
await servicio.guardarPresetDispositivo(
placeholderKey,
PresetEcualizador.bassBoost,
);
await servicio.migrarClavesPlaceholder();
final config = await servicio.cargar();
expect(
config.presetsDispositivo[placeholderKey],
equals(PresetEcualizador.bassBoost),
);
expect(
config.presetsDispositivo[stableKey],
equals(PresetEcualizador.rock),
);
});
test(
'5.5 no placeholder entries when BLUETOOTH_CONNECT was never '
'requested is a no-op',
() async {
final prefs = await SharedPreferences.getInstance();
final servicio = ServicioEcualizador(prefs: prefs);
await servicio.guardarPresetDispositivo(
stableKey,
PresetEcualizador.rock,
);
await servicio.migrarClavesPlaceholder();
final config = await servicio.cargar();
expect(
config.presetsDispositivo[stableKey],
equals(PresetEcualizador.rock),
);
expect(config.presetsDispositivo.length, equals(1));
},
);
test('5.6 nombresDispositivos also purges the exact placeholder key', () async {
final prefs = await SharedPreferences.getInstance();
final servicio = ServicioEcualizador(prefs: prefs);
await servicio.guardarNombresDispositivos({
placeholderKey: 'Ghost Device',
stableKey: 'Living Room BT',
});
await servicio.migrarClavesPlaceholder();
final config = await servicio.cargar();
expect(config.nombresDispositivos.containsKey(placeholderKey), isFalse);
expect(
config.nombresDispositivos[stableKey],
equals('Living Room BT'),
);
});
test('5.9 cargar() runs the migration automatically on first load', () async {
final prefs = await SharedPreferences.getInstance();
final servicio = ServicioEcualizador(prefs: prefs);
await servicio.guardarPresetDispositivo(
placeholderKey,
PresetEcualizador.jazz,
);
await servicio.guardarPresetDispositivo(
stableKey,
PresetEcualizador.rock,
);
// No explicit migrarClavesPlaceholder() call: cargar() must run it.
final config = await servicio.cargar();
expect(config.presetsDispositivo.containsKey(placeholderKey), isFalse);
expect(
config.presetsDispositivo[stableKey],
equals(PresetEcualizador.rock),
);
});
});
}