fix(eq): stop the phone's FM sink from posing as the active output
Build & Deploy PluriWave / Análisis de código (push) Successful in 24s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 2m23s

Regression from the previous commit. Ranking every AudioDeviceInfo type this
build does not name individually ABOVE the built-in speaker was meant to let
a car stereo on LE Audio or an automotive bus win. It also promoted the
internal sinks a phone exposes permanently: on the Xiaomi test device
AudioManager reports TYPE_FM (14) as an output, so getActiveAudioDevice
picked it over the real speaker with nothing connected at all. Confirmed on
device:

  audio_devices.onListen -> {id=other:14:4, type=14, name=2412DPC0AG}

It then reached Dart under an `other:14:4` id whose type is neither the base
speaker nor a known one, slipped past the collision guard and had a preset
row persisted for it -- reinstating the exact symptom this series set out to
kill: a permanent green active-output dot on a device that was not connected.

Replace the deny-by-omission ranking with an explicit allow list of outputs a
user actually connects. The built-in speaker sits below all of them and above
everything else, so any sink that physically exists but is never where media
plays (TYPE_FM, TYPE_BUILTIN_SPEAKER_SAFE, telephony, remote submix) can no
longer be selected. A one-time purge clears the `other:` rows the bad build
persisted; genuine ones re-register on their next connection.

Fix the USB type constant while here: TYPE_USB_HEADSET is 22, not 14, and 14
is TYPE_FM. The Kotlin USB branch hardcoded 14 and the Dart type table
mirrored the same mistake, so the two cancelled out for real USB headsets
while making a phone's own FM sink decode as USB audio. Both now use 22.

Verified with javap against android.jar (android-36) rather than trusting the
comment that introduced the error.
This commit is contained in:
2026-07-25 20:43:47 +02:00
parent c94bc3d770
commit 4042cf5ffd
5 changed files with 162 additions and 26 deletions
+13 -2
View File
@@ -22,6 +22,13 @@ const prefijoPlaceholderBtName = 'bt_a2dp:name:';
/// entry that then marked the wrong device as active forever.
const idAltavozInterno = 'builtin_speaker';
/// Id prefix for outputs the native layer does not name individually
/// (`other:<AudioDeviceInfo type>:<address>`): LE Audio stereos, car buses,
/// hearing aids, docks. Kept distinct from every other id namespace so such a
/// device can never collide with [idAltavozInterno], and so a bad batch of them
/// can be purged wholesale.
const prefijoDispositivoOtro = 'other:';
/// Abstract service for audio device detection.
///
/// Implementations:
@@ -79,7 +86,11 @@ abstract class ServicioDispositivoAudio {
/// 2 → altavozInterno (builtin_speaker)
/// 3 → auricularesCable (wired_headset)
/// 8 → bluetoothA2dp (`bt_a2dp:<MAC>`)
/// 14 → usbAudio (`usb_headset:<address>`)
/// 22 → usbAudio (`usb_headset:<address>`)
///
/// Values match `AudioDeviceInfo.TYPE_*`. Note 22, NOT 14: type 14 is
/// `TYPE_FM`, an output plenty of phones expose permanently, and mapping it to
/// USB audio made a phone's own FM sink look like a connected headset.
class ServicioDispositivoAudioReal extends ServicioDispositivoAudio {
static const _channelName = 'pluriwave/audio_devices';
@@ -189,7 +200,7 @@ class ServicioDispositivoAudioReal extends ServicioDispositivoAudio {
return TipoDispositivo.auricularesCable;
case 8:
return TipoDispositivo.bluetoothA2dp;
case 14:
case 22:
return TipoDispositivo.usbAudio;
default:
return TipoDispositivo.desconocido;
+57 -1
View File
@@ -4,7 +4,8 @@ import 'package:shared_preferences/shared_preferences.dart';
import '../modelos/preset_ecualizador.dart';
import 'persistencia_tolerante.dart';
import 'servicio_dispositivo_audio.dart' show idAltavozInterno;
import 'servicio_dispositivo_audio.dart'
show idAltavozInterno, prefijoDispositivoOtro;
class ConfiguracionEcualizador {
const ConfiguracionEcualizador({
@@ -106,9 +107,64 @@ class ServicioEcualizador {
final prefs = await _resolverPrefs();
if (prefs.getBool(_keyColisionBasePurgaHecha) ?? false) return;
await _purgarDeviceId(prefs, idAltavozInterno);
await _purgarPrefijoDispositivo(prefs, prefijoDispositivoOtro);
await prefs.setBool(_keyColisionBasePurgaHecha, true);
}
/// Removes every device-keyed entry whose id starts with [prefijo].
///
/// Used for the `other:` namespace: a build that ranked every unlisted
/// `AudioDeviceInfo` type above the phone speaker selected permanent internal
/// sinks (TYPE_FM on Xiaomi, TYPE_BUILTIN_SPEAKER_SAFE elsewhere) as the
/// active output and persisted rows for them. Genuine external outputs in
/// this namespace are re-registered on their next connection, so clearing the
/// whole prefix costs nothing and needs no per-type knowledge here.
Future<void> _purgarPrefijoDispositivo(
SharedPreferences prefs,
String prefijo,
) async {
final presetsPorDispositivo = _leerMapa(prefs, _keyPresetsPorDispositivo);
final dispositivos = presetsPorDispositivo.keys
.where((clave) => clave.startsWith(prefijo))
.toList();
final nombres = _leerMapaStrings(prefs, _keyNombresDispositivos);
final nombresAPurgar = nombres.keys
.where((clave) => clave.startsWith(prefijo))
.toList();
final matriz = _leerMapa(prefs, _keyPresetsMatriz);
final matrizAPurgar = matriz.keys.where((clave) {
final separador = clave.indexOf(':');
if (separador == -1) return false;
return clave.substring(separador + 1).startsWith(prefijo);
}).toList();
for (final clave in dispositivos) {
presetsPorDispositivo.remove(clave);
}
for (final clave in nombresAPurgar) {
nombres.remove(clave);
}
for (final clave in matrizAPurgar) {
matriz.remove(clave);
}
if (dispositivos.isNotEmpty) {
await _guardarMapa(
prefs,
_keyPresetsPorDispositivo,
presetsPorDispositivo,
);
}
if (nombresAPurgar.isNotEmpty) {
await _guardarMapaStrings(prefs, _keyNombresDispositivos, nombres);
}
if (matrizAPurgar.isNotEmpty) {
await _guardarMapa(prefs, _keyPresetsMatriz, matriz);
}
}
/// Removes every trace of [deviceId] from the three device-keyed SP maps.
///
/// Shared by the guarded migrations and by [eliminarDispositivo]; each map is