fix(eq): stop the phone speaker from impersonating a Bluetooth device
deviceToMap handed the builtin_speaker id to EVERY output type its `when` did not name. A car stereo on LE Audio (TYPE_BLE_HEADSET) or an automotive bus (TYPE_BUS) therefore arrived in Dart under the phone speaker's own id, carrying a type that maps to `desconocido` -- which slipped past the type-only esBase guard and persisted a device entry keyed builtin_speaker. From that moment on, every playback through the phone's own speaker matched that entry, so the green active-output dot stayed pinned to whatever the user had renamed it to (a car, in the reported case) whether or not anything was connected. The dot was never wrong; the row was poisoned. Give unnamed output types their own `other:<type>:<address>` id namespace, and match esBase by id as well as by type so no future native regression can re-create the collision. A guarded one-time migration purges what the collision already persisted from all three device-keyed maps. Fix the ranking too: builtin_speaker sat inside the priority list as a peer, so any type absent from that list sorted BELOW the always-present speaker and could never win. The speaker is now the explicit last resort, externally connected outputs outrank it, and virtual or call-only sinks (earpiece, telephony, remote submix, SCO) are ranked below it so they can never be reported as where music is playing. Route every AudioDeviceInfo.getAddress read through a version-guarded helper. It is API 28 with minSdk 24, and two pre-existing unguarded calls in this same method were latent NoSuchMethodError crashes on Android 7-8.1. Android lint for :app goes from 8 errors to 6. Also lets the user manage the list, which is how they recover from a bad entry without waiting for a release: a remove action clears a device's preset, name and matrix entries, unnamed rows show their transport and address tail instead of a raw bt_a2dp:AA:BB:... id, and the green dot finally carries a tooltip and a semantics label saying what it means. Device QA pending for wired and USB outputs: no jack or adapter available to exercise those paths. Their detection is unchanged by this commit.
This commit is contained in:
@@ -264,7 +264,14 @@ class EstadoEcualizador extends ChangeNotifier {
|
||||
// fallback ids for a device whose real MAC is not yet known, so
|
||||
// persisting a preset entry for them would create dead noise that never
|
||||
// resolves to the eventual real-MAC id.
|
||||
final esBase = dispositivo.tipo == TipoDispositivo.altavozInterno;
|
||||
// Matched by id AND by type: the native layer historically reported the
|
||||
// phone-speaker id for output types it could not name (LE Audio, car bus,
|
||||
// dock), which arrive here as `desconocido` and would otherwise slip past a
|
||||
// type-only check and persist an entry that hijacks the active-device
|
||||
// indicator forever.
|
||||
final esBase =
|
||||
dispositivo.tipo == TipoDispositivo.altavozInterno ||
|
||||
dispositivo.id == idAltavozInterno;
|
||||
final esPlaceholderCompuesto = dispositivo.id.startsWith(
|
||||
prefijoPlaceholderBtName,
|
||||
);
|
||||
@@ -424,6 +431,35 @@ class EstadoEcualizador extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Forgets [deviceId] completely: its device preset, its custom name and
|
||||
/// every matrix entry that targets it.
|
||||
///
|
||||
/// Lets the user clear stale or duplicate rows from the known-devices list.
|
||||
/// The device is NOT prevented from coming back: if it connects again it is
|
||||
/// re-registered from scratch, which is exactly how a user recovers from a
|
||||
/// bad entry. When the removed device is the active one, the effective preset
|
||||
/// is re-resolved so playback immediately follows the remaining hierarchy
|
||||
/// instead of keeping the deleted preset applied.
|
||||
Future<void> eliminarDispositivo(String deviceId) async {
|
||||
_presetsDispositivo.remove(deviceId);
|
||||
_nombresDispositivos.remove(deviceId);
|
||||
_nombresPlataforma.remove(deviceId);
|
||||
_presetsMatriz.removeWhere((clave, _) {
|
||||
final separador = clave.indexOf(':');
|
||||
if (separador == -1) return false;
|
||||
return clave.substring(separador + 1) == deviceId;
|
||||
});
|
||||
|
||||
await servicio.eliminarDispositivo(deviceId);
|
||||
|
||||
if (_dispositivoActualId == deviceId) {
|
||||
final resuelto = _resolverPresetActivo();
|
||||
_presetActual = resuelto;
|
||||
await audio.aplicarPreset(resuelto);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Returns the stored custom name for [deviceId], or an empty string if none.
|
||||
String obtenerNombreDispositivo(String deviceId) =>
|
||||
_nombresDispositivos[deviceId] ?? '';
|
||||
|
||||
@@ -666,6 +666,14 @@
|
||||
"eqDeviceNameHint": "مثال: مكبر صوت غرفة المعيشة",
|
||||
"eqDeviceNameConfirm": "حفظ",
|
||||
"eqDeviceConnected": "متصل",
|
||||
"eqDeviceActiveOutput": "يتم إخراج الصوت من هذا الجهاز",
|
||||
"eqDeviceRemove": "إزالة الجهاز",
|
||||
"eqDeviceRemoved": "تمت إزالة {device}",
|
||||
"@eqDeviceRemoved": {
|
||||
"placeholders": {
|
||||
"device": {}
|
||||
}
|
||||
},
|
||||
"localMusicSectionTitle": "الموسيقى المحلية (Android Auto)",
|
||||
"localMusicSectionDescription": "اختر مجلدًا على هذا الجهاز لتصفح ملفاته الصوتية وتشغيلها من السيارة.",
|
||||
"localMusicFolderNotConfigured": "لم يتم تحديد مجلد",
|
||||
|
||||
@@ -666,6 +666,14 @@
|
||||
"eqDeviceNameHint": "যেমন: লিভিং রুমের স্পিকার",
|
||||
"eqDeviceNameConfirm": "সংরক্ষণ করুন",
|
||||
"eqDeviceConnected": "সংযুক্ত",
|
||||
"eqDeviceActiveOutput": "অডিও এই ডিভাইস দিয়ে বাজছে",
|
||||
"eqDeviceRemove": "ডিভাইস সরান",
|
||||
"eqDeviceRemoved": "{device} সরানো হয়েছে",
|
||||
"@eqDeviceRemoved": {
|
||||
"placeholders": {
|
||||
"device": {}
|
||||
}
|
||||
},
|
||||
"localMusicSectionTitle": "স্থানীয় সঙ্গীত (Android Auto)",
|
||||
"localMusicSectionDescription": "গাড়িতে অডিও ফাইল ব্রাউজ ও চালানোর জন্য এই ডিভাইসের একটি ফোল্ডার বেছে নিন।",
|
||||
"localMusicFolderNotConfigured": "কোনো ফোল্ডার নির্বাচিত হয়নি",
|
||||
|
||||
@@ -666,6 +666,14 @@
|
||||
"eqDeviceNameHint": "z. B. Wohnzimmer-Lautsprecher",
|
||||
"eqDeviceNameConfirm": "Speichern",
|
||||
"eqDeviceConnected": "Verbunden",
|
||||
"eqDeviceActiveOutput": "Der Ton wird über dieses Gerät ausgegeben",
|
||||
"eqDeviceRemove": "Gerät entfernen",
|
||||
"eqDeviceRemoved": "{device} entfernt",
|
||||
"@eqDeviceRemoved": {
|
||||
"placeholders": {
|
||||
"device": {}
|
||||
}
|
||||
},
|
||||
"localMusicSectionTitle": "Lokale Musik (Android Auto)",
|
||||
"localMusicSectionDescription": "Wähle einen Ordner auf diesem Gerät aus, um dessen Audiodateien im Auto zu durchsuchen und abzuspielen.",
|
||||
"localMusicFolderNotConfigured": "Kein Ordner ausgewählt",
|
||||
|
||||
@@ -666,6 +666,14 @@
|
||||
"eqDeviceNameHint": "e.g. Living Room Speaker",
|
||||
"eqDeviceNameConfirm": "Save",
|
||||
"eqDeviceConnected": "Connected",
|
||||
"eqDeviceActiveOutput": "Audio is playing through this device",
|
||||
"eqDeviceRemove": "Remove device",
|
||||
"eqDeviceRemoved": "{device} removed",
|
||||
"@eqDeviceRemoved": {
|
||||
"placeholders": {
|
||||
"device": {}
|
||||
}
|
||||
},
|
||||
"localMusicSectionTitle": "Local music (Android Auto)",
|
||||
"localMusicSectionDescription": "Pick a folder on this device to browse and play its audio files from the car.",
|
||||
"localMusicFolderNotConfigured": "No folder selected",
|
||||
|
||||
@@ -625,6 +625,14 @@
|
||||
"eqDeviceNameHint": "Ej: Altavoz del living",
|
||||
"eqDeviceNameConfirm": "Guardar",
|
||||
"eqDeviceConnected": "Conectado",
|
||||
"eqDeviceActiveOutput": "El audio está saliendo por este dispositivo",
|
||||
"eqDeviceRemove": "Quitar dispositivo",
|
||||
"eqDeviceRemoved": "Se quitó {device}",
|
||||
"@eqDeviceRemoved": {
|
||||
"placeholders": {
|
||||
"device": {}
|
||||
}
|
||||
},
|
||||
"localMusicSectionTitle": "Música local (Android Auto)",
|
||||
"localMusicSectionDescription": "Elegí una carpeta de este dispositivo para explorar y reproducir sus archivos de audio desde el auto.",
|
||||
"localMusicFolderNotConfigured": "No hay carpeta seleccionada",
|
||||
|
||||
@@ -666,6 +666,14 @@
|
||||
"eqDeviceNameHint": "ex. : Enceinte salon",
|
||||
"eqDeviceNameConfirm": "Enregistrer",
|
||||
"eqDeviceConnected": "Connecté",
|
||||
"eqDeviceActiveOutput": "Le son sort par cet appareil",
|
||||
"eqDeviceRemove": "Supprimer l’appareil",
|
||||
"eqDeviceRemoved": "{device} supprimé",
|
||||
"@eqDeviceRemoved": {
|
||||
"placeholders": {
|
||||
"device": {}
|
||||
}
|
||||
},
|
||||
"localMusicSectionTitle": "Musique locale (Android Auto)",
|
||||
"localMusicSectionDescription": "Choisissez un dossier sur cet appareil pour parcourir et lire ses fichiers audio depuis la voiture.",
|
||||
"localMusicFolderNotConfigured": "Aucun dossier sélectionné",
|
||||
|
||||
@@ -666,6 +666,14 @@
|
||||
"eqDeviceNameHint": "उदा. लिविंग रूम स्पीकर",
|
||||
"eqDeviceNameConfirm": "सहेजें",
|
||||
"eqDeviceConnected": "कनेक्टेड",
|
||||
"eqDeviceActiveOutput": "ऑडियो इस डिवाइस से चल रहा है",
|
||||
"eqDeviceRemove": "डिवाइस हटाएं",
|
||||
"eqDeviceRemoved": "{device} हटा दिया गया",
|
||||
"@eqDeviceRemoved": {
|
||||
"placeholders": {
|
||||
"device": {}
|
||||
}
|
||||
},
|
||||
"localMusicSectionTitle": "स्थानीय संगीत (Android Auto)",
|
||||
"localMusicSectionDescription": "गाड़ी में ऑडियो फ़ाइलें ब्राउज़ और चलाने के लिए इस डिवाइस का एक फ़ोल्डर चुनें।",
|
||||
"localMusicFolderNotConfigured": "कोई फ़ोल्डर चुना नहीं गया",
|
||||
|
||||
@@ -666,6 +666,14 @@
|
||||
"eqDeviceNameHint": "Mis: Speaker ruang tamu",
|
||||
"eqDeviceNameConfirm": "Simpan",
|
||||
"eqDeviceConnected": "Terhubung",
|
||||
"eqDeviceActiveOutput": "Audio keluar melalui perangkat ini",
|
||||
"eqDeviceRemove": "Hapus perangkat",
|
||||
"eqDeviceRemoved": "{device} dihapus",
|
||||
"@eqDeviceRemoved": {
|
||||
"placeholders": {
|
||||
"device": {}
|
||||
}
|
||||
},
|
||||
"localMusicSectionTitle": "Musik lokal (Android Auto)",
|
||||
"localMusicSectionDescription": "Pilih folder di perangkat ini untuk menjelajahi dan memutar file audio di dalamnya dari mobil.",
|
||||
"localMusicFolderNotConfigured": "Belum ada folder dipilih",
|
||||
|
||||
@@ -666,6 +666,14 @@
|
||||
"eqDeviceNameHint": "Es: Cassa del salotto",
|
||||
"eqDeviceNameConfirm": "Salva",
|
||||
"eqDeviceConnected": "Connesso",
|
||||
"eqDeviceActiveOutput": "L'audio esce da questo dispositivo",
|
||||
"eqDeviceRemove": "Rimuovi dispositivo",
|
||||
"eqDeviceRemoved": "{device} rimosso",
|
||||
"@eqDeviceRemoved": {
|
||||
"placeholders": {
|
||||
"device": {}
|
||||
}
|
||||
},
|
||||
"localMusicSectionTitle": "Musica locale (Android Auto)",
|
||||
"localMusicSectionDescription": "Scegli una cartella su questo dispositivo per sfogliare e riprodurre i suoi file audio dall'auto.",
|
||||
"localMusicFolderNotConfigured": "Nessuna cartella selezionata",
|
||||
|
||||
@@ -666,6 +666,14 @@
|
||||
"eqDeviceNameHint": "例:リビングのスピーカー",
|
||||
"eqDeviceNameConfirm": "保存",
|
||||
"eqDeviceConnected": "接続中",
|
||||
"eqDeviceActiveOutput": "この機器から音声が出力されています",
|
||||
"eqDeviceRemove": "機器を削除",
|
||||
"eqDeviceRemoved": "{device} を削除しました",
|
||||
"@eqDeviceRemoved": {
|
||||
"placeholders": {
|
||||
"device": {}
|
||||
}
|
||||
},
|
||||
"localMusicSectionTitle": "ローカル音楽(Android Auto)",
|
||||
"localMusicSectionDescription": "この端末のフォルダーを選択して、車内でその音声ファイルを閲覧・再生します。",
|
||||
"localMusicFolderNotConfigured": "フォルダーが選択されていません",
|
||||
|
||||
@@ -666,6 +666,14 @@
|
||||
"eqDeviceNameHint": "Ex: Caixa da sala",
|
||||
"eqDeviceNameConfirm": "Salvar",
|
||||
"eqDeviceConnected": "Conectado",
|
||||
"eqDeviceActiveOutput": "O áudio está a sair por este dispositivo",
|
||||
"eqDeviceRemove": "Remover dispositivo",
|
||||
"eqDeviceRemoved": "{device} removido",
|
||||
"@eqDeviceRemoved": {
|
||||
"placeholders": {
|
||||
"device": {}
|
||||
}
|
||||
},
|
||||
"localMusicSectionTitle": "Música local (Android Auto)",
|
||||
"localMusicSectionDescription": "Escolha uma pasta neste dispositivo para navegar e reproduzir os arquivos de áudio dela no carro.",
|
||||
"localMusicFolderNotConfigured": "Nenhuma pasta selecionada",
|
||||
|
||||
@@ -666,6 +666,14 @@
|
||||
"eqDeviceNameHint": "Напр.: Колонка в гостиной",
|
||||
"eqDeviceNameConfirm": "Сохранить",
|
||||
"eqDeviceConnected": "Подключено",
|
||||
"eqDeviceActiveOutput": "Звук выводится через это устройство",
|
||||
"eqDeviceRemove": "Удалить устройство",
|
||||
"eqDeviceRemoved": "{device} удалено",
|
||||
"@eqDeviceRemoved": {
|
||||
"placeholders": {
|
||||
"device": {}
|
||||
}
|
||||
},
|
||||
"localMusicSectionTitle": "Локальная музыка (Android Auto)",
|
||||
"localMusicSectionDescription": "Выберите папку на этом устройстве, чтобы просматривать и воспроизводить её аудиофайлы в автомобиле.",
|
||||
"localMusicFolderNotConfigured": "Папка не выбрана",
|
||||
|
||||
@@ -666,6 +666,14 @@
|
||||
"eqDeviceNameHint": "例:客厅音箱",
|
||||
"eqDeviceNameConfirm": "保存",
|
||||
"eqDeviceConnected": "已连接",
|
||||
"eqDeviceActiveOutput": "音频正通过此设备播放",
|
||||
"eqDeviceRemove": "移除设备",
|
||||
"eqDeviceRemoved": "已移除 {device}",
|
||||
"@eqDeviceRemoved": {
|
||||
"placeholders": {
|
||||
"device": {}
|
||||
}
|
||||
},
|
||||
"localMusicSectionTitle": "本地音乐(Android Auto)",
|
||||
"localMusicSectionDescription": "选择此设备上的一个文件夹,以便在车内浏览和播放其中的音频文件。",
|
||||
"localMusicFolderNotConfigured": "未选择文件夹",
|
||||
|
||||
@@ -2420,6 +2420,24 @@ abstract class AppLocalizations {
|
||||
/// **'Conectado'**
|
||||
String get eqDeviceConnected;
|
||||
|
||||
/// No description provided for @eqDeviceActiveOutput.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'El audio está saliendo por este dispositivo'**
|
||||
String get eqDeviceActiveOutput;
|
||||
|
||||
/// No description provided for @eqDeviceRemove.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Quitar dispositivo'**
|
||||
String get eqDeviceRemove;
|
||||
|
||||
/// No description provided for @eqDeviceRemoved.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Se quitó {device}'**
|
||||
String eqDeviceRemoved(Object device);
|
||||
|
||||
/// No description provided for @localMusicSectionTitle.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
|
||||
@@ -1313,6 +1313,17 @@ class AppLocalizationsAr extends AppLocalizations {
|
||||
@override
|
||||
String get eqDeviceConnected => 'متصل';
|
||||
|
||||
@override
|
||||
String get eqDeviceActiveOutput => 'يتم إخراج الصوت من هذا الجهاز';
|
||||
|
||||
@override
|
||||
String get eqDeviceRemove => 'إزالة الجهاز';
|
||||
|
||||
@override
|
||||
String eqDeviceRemoved(Object device) {
|
||||
return 'تمت إزالة $device';
|
||||
}
|
||||
|
||||
@override
|
||||
String get localMusicSectionTitle => 'الموسيقى المحلية (Android Auto)';
|
||||
|
||||
|
||||
@@ -1321,6 +1321,17 @@ class AppLocalizationsBn extends AppLocalizations {
|
||||
@override
|
||||
String get eqDeviceConnected => 'সংযুক্ত';
|
||||
|
||||
@override
|
||||
String get eqDeviceActiveOutput => 'অডিও এই ডিভাইস দিয়ে বাজছে';
|
||||
|
||||
@override
|
||||
String get eqDeviceRemove => 'ডিভাইস সরান';
|
||||
|
||||
@override
|
||||
String eqDeviceRemoved(Object device) {
|
||||
return '$device সরানো হয়েছে';
|
||||
}
|
||||
|
||||
@override
|
||||
String get localMusicSectionTitle => 'স্থানীয় সঙ্গীত (Android Auto)';
|
||||
|
||||
|
||||
@@ -1331,6 +1331,18 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get eqDeviceConnected => 'Verbunden';
|
||||
|
||||
@override
|
||||
String get eqDeviceActiveOutput =>
|
||||
'Der Ton wird über dieses Gerät ausgegeben';
|
||||
|
||||
@override
|
||||
String get eqDeviceRemove => 'Gerät entfernen';
|
||||
|
||||
@override
|
||||
String eqDeviceRemoved(Object device) {
|
||||
return '$device entfernt';
|
||||
}
|
||||
|
||||
@override
|
||||
String get localMusicSectionTitle => 'Lokale Musik (Android Auto)';
|
||||
|
||||
|
||||
@@ -1317,6 +1317,17 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get eqDeviceConnected => 'Connected';
|
||||
|
||||
@override
|
||||
String get eqDeviceActiveOutput => 'Audio is playing through this device';
|
||||
|
||||
@override
|
||||
String get eqDeviceRemove => 'Remove device';
|
||||
|
||||
@override
|
||||
String eqDeviceRemoved(Object device) {
|
||||
return '$device removed';
|
||||
}
|
||||
|
||||
@override
|
||||
String get localMusicSectionTitle => 'Local music (Android Auto)';
|
||||
|
||||
|
||||
@@ -1326,6 +1326,18 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get eqDeviceConnected => 'Conectado';
|
||||
|
||||
@override
|
||||
String get eqDeviceActiveOutput =>
|
||||
'El audio está saliendo por este dispositivo';
|
||||
|
||||
@override
|
||||
String get eqDeviceRemove => 'Quitar dispositivo';
|
||||
|
||||
@override
|
||||
String eqDeviceRemoved(Object device) {
|
||||
return 'Se quitó $device';
|
||||
}
|
||||
|
||||
@override
|
||||
String get localMusicSectionTitle => 'Música local (Android Auto)';
|
||||
|
||||
|
||||
@@ -1336,6 +1336,17 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get eqDeviceConnected => 'Connecté';
|
||||
|
||||
@override
|
||||
String get eqDeviceActiveOutput => 'Le son sort par cet appareil';
|
||||
|
||||
@override
|
||||
String get eqDeviceRemove => 'Supprimer l’appareil';
|
||||
|
||||
@override
|
||||
String eqDeviceRemoved(Object device) {
|
||||
return '$device supprimé';
|
||||
}
|
||||
|
||||
@override
|
||||
String get localMusicSectionTitle => 'Musique locale (Android Auto)';
|
||||
|
||||
|
||||
@@ -1319,6 +1319,17 @@ class AppLocalizationsHi extends AppLocalizations {
|
||||
@override
|
||||
String get eqDeviceConnected => 'कनेक्टेड';
|
||||
|
||||
@override
|
||||
String get eqDeviceActiveOutput => 'ऑडियो इस डिवाइस से चल रहा है';
|
||||
|
||||
@override
|
||||
String get eqDeviceRemove => 'डिवाइस हटाएं';
|
||||
|
||||
@override
|
||||
String eqDeviceRemoved(Object device) {
|
||||
return '$device हटा दिया गया';
|
||||
}
|
||||
|
||||
@override
|
||||
String get localMusicSectionTitle => 'स्थानीय संगीत (Android Auto)';
|
||||
|
||||
|
||||
@@ -1325,6 +1325,17 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
@override
|
||||
String get eqDeviceConnected => 'Terhubung';
|
||||
|
||||
@override
|
||||
String get eqDeviceActiveOutput => 'Audio keluar melalui perangkat ini';
|
||||
|
||||
@override
|
||||
String get eqDeviceRemove => 'Hapus perangkat';
|
||||
|
||||
@override
|
||||
String eqDeviceRemoved(Object device) {
|
||||
return '$device dihapus';
|
||||
}
|
||||
|
||||
@override
|
||||
String get localMusicSectionTitle => 'Musik lokal (Android Auto)';
|
||||
|
||||
|
||||
@@ -1331,6 +1331,17 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get eqDeviceConnected => 'Connesso';
|
||||
|
||||
@override
|
||||
String get eqDeviceActiveOutput => 'L\'audio esce da questo dispositivo';
|
||||
|
||||
@override
|
||||
String get eqDeviceRemove => 'Rimuovi dispositivo';
|
||||
|
||||
@override
|
||||
String eqDeviceRemoved(Object device) {
|
||||
return '$device rimosso';
|
||||
}
|
||||
|
||||
@override
|
||||
String get localMusicSectionTitle => 'Musica locale (Android Auto)';
|
||||
|
||||
|
||||
@@ -1278,6 +1278,17 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get eqDeviceConnected => '接続中';
|
||||
|
||||
@override
|
||||
String get eqDeviceActiveOutput => 'この機器から音声が出力されています';
|
||||
|
||||
@override
|
||||
String get eqDeviceRemove => '機器を削除';
|
||||
|
||||
@override
|
||||
String eqDeviceRemoved(Object device) {
|
||||
return '$device を削除しました';
|
||||
}
|
||||
|
||||
@override
|
||||
String get localMusicSectionTitle => 'ローカル音楽(Android Auto)';
|
||||
|
||||
|
||||
@@ -1323,6 +1323,17 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get eqDeviceConnected => 'Conectado';
|
||||
|
||||
@override
|
||||
String get eqDeviceActiveOutput => 'O áudio está a sair por este dispositivo';
|
||||
|
||||
@override
|
||||
String get eqDeviceRemove => 'Remover dispositivo';
|
||||
|
||||
@override
|
||||
String eqDeviceRemoved(Object device) {
|
||||
return '$device removido';
|
||||
}
|
||||
|
||||
@override
|
||||
String get localMusicSectionTitle => 'Música local (Android Auto)';
|
||||
|
||||
|
||||
@@ -1327,6 +1327,17 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get eqDeviceConnected => 'Подключено';
|
||||
|
||||
@override
|
||||
String get eqDeviceActiveOutput => 'Звук выводится через это устройство';
|
||||
|
||||
@override
|
||||
String get eqDeviceRemove => 'Удалить устройство';
|
||||
|
||||
@override
|
||||
String eqDeviceRemoved(Object device) {
|
||||
return '$device удалено';
|
||||
}
|
||||
|
||||
@override
|
||||
String get localMusicSectionTitle => 'Локальная музыка (Android Auto)';
|
||||
|
||||
|
||||
@@ -1271,6 +1271,17 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get eqDeviceConnected => '已连接';
|
||||
|
||||
@override
|
||||
String get eqDeviceActiveOutput => '音频正通过此设备播放';
|
||||
|
||||
@override
|
||||
String get eqDeviceRemove => '移除设备';
|
||||
|
||||
@override
|
||||
String eqDeviceRemoved(Object device) {
|
||||
return '已移除 $device';
|
||||
}
|
||||
|
||||
@override
|
||||
String get localMusicSectionTitle => '本地音乐(Android Auto)';
|
||||
|
||||
|
||||
@@ -914,14 +914,28 @@ class _FilaDispositivo extends StatelessWidget {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final eq = context.watch<EstadoEcualizador>();
|
||||
final isActive = eq.dispositivoActualId == deviceId;
|
||||
final displayName = eq.nombreVisible(deviceId, eq.nombrePlataforma(deviceId));
|
||||
final displayName = _nombreLegible(
|
||||
deviceId,
|
||||
eq.nombreVisible(deviceId, eq.nombrePlataforma(deviceId)),
|
||||
);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||
child: Row(
|
||||
children: [
|
||||
// The dot marks where audio is coming out RIGHT NOW, which is not the
|
||||
// same as "paired" or "connected" — it needs a label, both for screen
|
||||
// readers and for anyone wondering what a bare green dot means.
|
||||
if (isActive)
|
||||
const Icon(Icons.circle, size: 10, color: Colors.green)
|
||||
Tooltip(
|
||||
message: l10n.eqDeviceActiveOutput,
|
||||
child: Icon(
|
||||
Icons.circle,
|
||||
size: 10,
|
||||
color: Colors.green,
|
||||
semanticLabel: l10n.eqDeviceActiveOutput,
|
||||
),
|
||||
)
|
||||
else
|
||||
const SizedBox(width: 10),
|
||||
const SizedBox(width: 8),
|
||||
@@ -954,6 +968,34 @@ class _FilaDispositivo extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
/// Turns a device id the user never named into something readable.
|
||||
///
|
||||
/// [nombreVisible] falls back to the raw id when neither a custom name nor a
|
||||
/// platform name is known — which is the normal case for a Bluetooth device
|
||||
/// that is not currently connected, since platform names are cached in memory
|
||||
/// only. Showing `bt_a2dp:AA:BB:CC:DD:EE:FF` tells the user nothing, so keep
|
||||
/// the transport plus the tail of the address, which is what distinguishes
|
||||
/// two otherwise identical rows.
|
||||
static String _nombreLegible(String deviceId, String nombreVisible) {
|
||||
if (nombreVisible != deviceId) return nombreVisible;
|
||||
|
||||
final separador = deviceId.indexOf(':');
|
||||
if (separador == -1) return deviceId;
|
||||
final transporte = deviceId.substring(0, separador);
|
||||
final resto = deviceId.substring(separador + 1);
|
||||
final etiqueta = switch (transporte) {
|
||||
'bt_a2dp' => 'Bluetooth',
|
||||
'usb_headset' => 'USB',
|
||||
_ => transporte,
|
||||
};
|
||||
final cola = resto.split(':').where((p) => p.isNotEmpty).toList();
|
||||
if (cola.isEmpty) return etiqueta;
|
||||
final sufijo = cola.length >= 2
|
||||
? cola.sublist(cola.length - 2).join(':')
|
||||
: cola.last;
|
||||
return '$etiqueta · $sufijo';
|
||||
}
|
||||
|
||||
Future<void> _abrirModal(BuildContext context) async {
|
||||
await showModalBottomSheet<void>(
|
||||
context: context,
|
||||
@@ -1014,6 +1056,22 @@ class _DialogoEdicionDispositivoState
|
||||
if (mounted) Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
Future<void> _eliminar() async {
|
||||
final eq = context.read<EstadoEcualizador>();
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final nombre = _nombreCtrl.text.trim().isEmpty
|
||||
? widget.deviceId
|
||||
: _nombreCtrl.text.trim();
|
||||
|
||||
await eq.eliminarDispositivo(widget.deviceId);
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).pop();
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text(l10n.eqDeviceRemoved(nombre))),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
@@ -1046,10 +1104,24 @@ class _DialogoEdicionDispositivoState
|
||||
onCambio: (p) => setState(() => _presetActual = p),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
FilledButton.icon(
|
||||
onPressed: _guardar,
|
||||
icon: const Icon(Icons.save_rounded),
|
||||
label: Text(l10n.eqDeviceNameConfirm),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FilledButton.icon(
|
||||
onPressed: _guardar,
|
||||
icon: const Icon(Icons.save_rounded),
|
||||
label: Text(l10n.eqDeviceNameConfirm),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// Lets the user clear stale or duplicate rows. The device comes
|
||||
// back on its next connection, so this is recoverable.
|
||||
OutlinedButton.icon(
|
||||
onPressed: _eliminar,
|
||||
icon: const Icon(Icons.delete_outline_rounded),
|
||||
label: Text(l10n.eqDeviceRemove),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -12,6 +12,16 @@ import '../modelos/dispositivo_audio.dart';
|
||||
/// entry.
|
||||
const prefijoPlaceholderBtName = 'bt_a2dp:name:';
|
||||
|
||||
/// Canonical id of the phone's own speaker.
|
||||
///
|
||||
/// Single source of truth shared by the collision guard in `EstadoEcualizador`
|
||||
/// and the one-time purge in `ServicioEcualizador`: the phone speaker is the
|
||||
/// fallback output every hierarchy level falls through to, so it must never own
|
||||
/// a device-level preset entry. The native layer used to hand this id to any
|
||||
/// output type it could not name (LE Audio, car bus, dock), which persisted an
|
||||
/// entry that then marked the wrong device as active forever.
|
||||
const idAltavozInterno = 'builtin_speaker';
|
||||
|
||||
/// Abstract service for audio device detection.
|
||||
///
|
||||
/// Implementations:
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../modelos/preset_ecualizador.dart';
|
||||
import 'persistencia_tolerante.dart';
|
||||
import 'servicio_dispositivo_audio.dart' show idAltavozInterno;
|
||||
|
||||
class ConfiguracionEcualizador {
|
||||
const ConfiguracionEcualizador({
|
||||
@@ -51,6 +52,9 @@ class ServicioEcualizador {
|
||||
/// `bt_a2dp:`-prefixed id shape used across all device-keyed SP maps.
|
||||
static const _placeholderMacLiteral = 'bt_a2dp:02:00:00:00:00:00';
|
||||
|
||||
/// Guard flag for the one-time `builtin_speaker` collision purge.
|
||||
static const _keyColisionBasePurgaHecha = 'eq_builtin_speaker_purge_done_v1';
|
||||
|
||||
final SharedPreferences? _prefs;
|
||||
|
||||
/// Injected startup instance (S3-R4); getInstance() is only a fallback.
|
||||
@@ -59,6 +63,7 @@ class ServicioEcualizador {
|
||||
|
||||
Future<ConfiguracionEcualizador> cargar() async {
|
||||
await migrarClavesPlaceholder();
|
||||
await migrarColisionAltavozInterno();
|
||||
final prefs = await _resolverPrefs();
|
||||
final principal = _leerPresetPrincipal(prefs);
|
||||
final porEmisora = _leerPresetsPorEmisora(prefs);
|
||||
@@ -84,9 +89,33 @@ class ServicioEcualizador {
|
||||
Future<void> migrarClavesPlaceholder() async {
|
||||
final prefs = await _resolverPrefs();
|
||||
if (prefs.getBool(_keyPlaceholderPurgaHecha) ?? false) return;
|
||||
await _purgarDeviceId(prefs, _placeholderMacLiteral);
|
||||
await prefs.setBool(_keyPlaceholderPurgaHecha, true);
|
||||
}
|
||||
|
||||
/// One-time guarded migration: purges entries keyed by [idAltavozInterno].
|
||||
///
|
||||
/// `MainActivity.deviceToMap` used to hand the phone-speaker id to ANY output
|
||||
/// type it could not name (LE Audio, car bus, dock). Dart then created a
|
||||
/// device entry under that id, and from then on every playback through the
|
||||
/// phone's own speaker matched it — permanently flagging whatever the user
|
||||
/// had renamed it to as the active device. The native id is fixed; this
|
||||
/// clears what the collision already persisted. Idempotent via
|
||||
/// [_keyColisionBasePurgaHecha]; every other entry survives byte-for-byte.
|
||||
Future<void> migrarColisionAltavozInterno() async {
|
||||
final prefs = await _resolverPrefs();
|
||||
if (prefs.getBool(_keyColisionBasePurgaHecha) ?? false) return;
|
||||
await _purgarDeviceId(prefs, idAltavozInterno);
|
||||
await prefs.setBool(_keyColisionBasePurgaHecha, true);
|
||||
}
|
||||
|
||||
/// Removes every trace of [deviceId] from the three device-keyed SP maps.
|
||||
///
|
||||
/// Shared by the guarded migrations and by [eliminarDispositivo]; each map is
|
||||
/// only rewritten when the key was actually present.
|
||||
Future<void> _purgarDeviceId(SharedPreferences prefs, String deviceId) async {
|
||||
final presetsPorDispositivo = _leerMapa(prefs, _keyPresetsPorDispositivo);
|
||||
if (presetsPorDispositivo.remove(_placeholderMacLiteral) != null) {
|
||||
if (presetsPorDispositivo.remove(deviceId) != null) {
|
||||
await _guardarMapa(
|
||||
prefs,
|
||||
_keyPresetsPorDispositivo,
|
||||
@@ -101,8 +130,7 @@ class ServicioEcualizador {
|
||||
final clavesMatrizAPurgar = presetsMatriz.keys.where((clave) {
|
||||
final separador = clave.indexOf(':');
|
||||
if (separador == -1) return false;
|
||||
final deviceIdSegmento = clave.substring(separador + 1);
|
||||
return deviceIdSegmento == _placeholderMacLiteral;
|
||||
return clave.substring(separador + 1) == deviceId;
|
||||
}).toList();
|
||||
if (clavesMatrizAPurgar.isNotEmpty) {
|
||||
for (final clave in clavesMatrizAPurgar) {
|
||||
@@ -115,15 +143,21 @@ class ServicioEcualizador {
|
||||
prefs,
|
||||
_keyNombresDispositivos,
|
||||
);
|
||||
if (nombresDispositivos.remove(_placeholderMacLiteral) != null) {
|
||||
if (nombresDispositivos.remove(deviceId) != null) {
|
||||
await _guardarMapaStrings(
|
||||
prefs,
|
||||
_keyNombresDispositivos,
|
||||
nombresDispositivos,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await prefs.setBool(_keyPlaceholderPurgaHecha, true);
|
||||
/// Forgets a device completely: its preset, its custom name and every matrix
|
||||
/// entry that targets it. Used by the "remove device" action so the user can
|
||||
/// clear stale or duplicate entries from the known-devices list.
|
||||
Future<void> eliminarDispositivo(String deviceId) async {
|
||||
final prefs = await _resolverPrefs();
|
||||
await _purgarDeviceId(prefs, deviceId);
|
||||
}
|
||||
|
||||
Future<void> guardarPrincipal(PresetEcualizador preset) async {
|
||||
|
||||
Reference in New Issue
Block a user