From cf59f3c13955dc653e2f5bc717e5c79f3e899ee6 Mon Sep 17 00:00:00 2001 From: SliDeeN Date: Tue, 30 Jun 2026 20:01:07 +0200 Subject: [PATCH 1/5] feature/volume-doors initial --- app/src/main/AndroidManifest.xml | 3 + .../com/mg4/control/hardware/MG4Hardware.kt | 130 ++++++++++++++++++ .../java/com/mg4/control/ui/AudioFragment.kt | 54 +++++++- .../com/mg4/control/ui/ConsoleFragment.kt | 30 +++- app/src/main/res/layout/fragment_audio.xml | 71 ++++++++++ app/src/main/res/values-de/strings.xml | 3 + app/src/main/res/values-en/strings.xml | 3 + app/src/main/res/values-es/strings.xml | 3 + app/src/main/res/values-it/strings.xml | 3 + app/src/main/res/values-pt/strings.xml | 3 + app/src/main/res/values/strings.xml | 3 + 11 files changed, 303 insertions(+), 3 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c1770520..fd8037a4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -28,6 +28,9 @@ android:theme="@style/Theme.MG4Control" android:supportsRtl="true"> + + + 0 = ouverte) + "mode". Le SDK SAIC (getIntProperty) + // ne les lisait pas. Poll ~700ms, change-only (log MG4_DOOR), baisse à l'ouverture. + // Pas de restauration. Connexion Car async (createCar + ServiceConnection). + + private const val DOORWATCH_TAG = "MG4_DOOR" + // Signal d'ouverture confirmé sur SWI133 : DLOCK_DOOR_OPEN_STS (propriété de zone PORTE), + // areaId 0x1 = porte AV-gauche, 0x4 = AV-droite ; valeur 1 = ouverte, 0 = fermée. + private const val DOOR_OPEN_PROP = 0x2640c623 + private val DOOR_FRONT_AREAS = intArrayOf(0x1, 0x4) + @Volatile private var sDoorWatcherOn = false + @Volatile private var sCarInstance: Any? = null + @Volatile private var sCarPropMgr: Any? = null + @Volatile private var sDoorConnecting = false + @Volatile private var sAnyFrontOpenPrev = false + + /** v1 : feature dispo sur SWI133 uniquement. */ + fun hasDoorVolumeFeature(): Boolean = + FirmwareInfo.getGeneration() == FirmwareInfo.Gen.SWI133 + + private fun doorVolumeEnabled(): Boolean = + sAppContext?.getSharedPreferences("mg4_settings", 0)?.getBoolean("door_volume_enabled", false) ?: false + + private fun doorVolumeLevel(): Int = + sAppContext?.getSharedPreferences("mg4_settings", 0)?.getInt("door_volume_level", 0) ?: 0 + + fun startDoorVolumeWatcher() { + if (!hasDoorVolumeFeature()) return + sDoorWatcherOn = true + connectCarProperty() + } + + fun stopDoorVolumeWatcher() { + sDoorWatcherOn = false // poll conservé ; on ne déclenche plus la baisse + AppLogger.i(TAG, " DoorVolumeWatcher: déclenchement désactivé") + } + + /** Connexion (async) à l'API Car AOSP pour obtenir CarPropertyManager (service "property"). */ + private fun connectCarProperty() { + if (sCarPropMgr != null || sDoorConnecting) return + val ctx = sAppContext ?: return + sDoorConnecting = true + try { + val carCls = ctx.classLoader.loadClass("android.car.Car") + val conn = object : ServiceConnection { + override fun onServiceConnected(name: ComponentName?, binder: IBinder?) { + try { + val car = sCarInstance ?: return + val mgr = carCls.getMethod("getCarManager", String::class.java).invoke(car, "property") + if (mgr != null) { sCarPropMgr = mgr; startDoorPolling(mgr) } + else AppLogger.w(DOORWATCH_TAG, "getCarManager(property) = null") + } catch (e: Exception) { AppLogger.w(DOORWATCH_TAG, "getCarManager échec: ${e.message}") } + } + override fun onServiceDisconnected(name: ComponentName?) { sCarPropMgr = null } + } + val car = carCls.getMethod("createCar", Context::class.java, ServiceConnection::class.java) + .invoke(null, ctx, conn) + sCarInstance = car + try { carCls.getMethod("connect").invoke(car) } catch (_: Exception) {} + AppLogger.i(DOORWATCH_TAG, "connexion Car (property)…") + } catch (e: Exception) { + sDoorConnecting = false + AppLogger.w(DOORWATCH_TAG, "Car createCar échec: ${e.message}") + } + } + + private fun startDoorPolling(mgr: Any) { + val getInt = try { + mgr.javaClass.getMethod("getIntProperty", Int::class.javaPrimitiveType, Int::class.javaPrimitiveType) + } catch (e: Exception) { AppLogger.w(DOORWATCH_TAG, "getIntProperty introuvable: ${e.message}"); return } + AppLogger.i(DOORWATCH_TAG, "watcher porte actif (DLOCK_DOOR_OPEN_STS AV)") + val h = Handler(Looper.getMainLooper()) + val poll = object : Runnable { + override fun run() { + if (sCarPropMgr == null) return + // Une porte AVANT est ouverte si DLOCK_DOOR_OPEN_STS == 1 sur l'areaId 0x1 ou 0x4. + var anyFrontOpen = false + for (area in DOOR_FRONT_AREAS) { + val v = try { getInt.invoke(mgr, DOOR_OPEN_PROP, area) as? Int ?: -1 } catch (_: Exception) { -1 } + if (v == 1) anyFrontOpen = true + } + // Front « fermé→ouvert » → baisse du volume une seule fois. + if (anyFrontOpen && !sAnyFrontOpenPrev && sDoorWatcherOn && doorVolumeEnabled()) { + val level = doorVolumeLevel() + CoroutineScope(Dispatchers.IO).launch { + val ok = setMediaVolume(level) + AppLogger.i(DOORWATCH_TAG, "porte avant ouverte → setMediaVolume($level) = $ok") + } + } + sAnyFrontOpenPrev = anyFrontOpen + h.postDelayed(this, 500L) + } + } + h.post(poll) + } fun getSpeedVolumeLevel(): Int = audioGet(AUDIO_GET_SPEED_VOL) fun setSpeedVolumeLevel(l: Int): Boolean = audioSet(AUDIO_SET_SPEED_VOL, l.coerceIn(AUDIO_TYPE_MIN, AUDIO_TYPE_MAX)) fun getSoundFieldType(): Int = -1 diff --git a/app/src/main/java/com/mg4/control/ui/AudioFragment.kt b/app/src/main/java/com/mg4/control/ui/AudioFragment.kt index dce6aee7..f1dd7113 100644 --- a/app/src/main/java/com/mg4/control/ui/AudioFragment.kt +++ b/app/src/main/java/com/mg4/control/ui/AudioFragment.kt @@ -1,5 +1,6 @@ package com.mg4.control.ui +import android.content.Context import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -7,6 +8,7 @@ import android.view.ViewGroup import android.widget.Switch import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope +import com.google.android.material.slider.Slider import com.mg4.control.R import com.mg4.control.hardware.MG4Hardware import kotlinx.coroutines.Dispatchers @@ -22,8 +24,8 @@ class AudioFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + // ── Loudness ─────────────────────────────────────────────────────── val switch = view.findViewById(R.id.switch_loudness) - // Loudness : l'AIDL vendor est BOOLÉEN → 1 = ON, 0 = OFF (et non 2/1). switch.setOnCheckedChangeListener { _, checked -> if (switch.isPressed) { @@ -32,12 +34,60 @@ class AudioFragment : Fragment() { } } } - viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) { val state = MG4Hardware.getLoudnessState() withContext(Dispatchers.Main) { if (isAdded && state >= 0) switch.isChecked = (state == 1) } } + + // ── Volume à l'ouverture d'une porte (v1 : SWI133) ────────────────── + setupDoorVolume(view) + } + + private fun setupDoorVolume(view: View) { + val card = view.findViewById(R.id.door_volume_card) + if (!MG4Hardware.hasDoorVolumeFeature()) { + card.visibility = View.GONE + return + } + + val prefs = requireContext().getSharedPreferences("mg4_settings", Context.MODE_PRIVATE) + val toggle = view.findViewById(R.id.switch_door_volume) + val slider = view.findViewById(R.id.slider_door_volume) + + val enabled = prefs.getBoolean("door_volume_enabled", false) + toggle.isChecked = enabled + slider.isEnabled = enabled + + // Si déjà activé, (re)démarre le watcher à l'ouverture de l'onglet (idempotent) : + // garantit la connexion Car / les logs MG4_DOOR sans avoir à re-toggler. + if (enabled) { + viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) { MG4Hardware.startDoorVolumeWatcher() } + } + + // Borne le slider sur le max réel de la voiture (sinon valeur XML par défaut). + viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) { + val max = MG4Hardware.getMediaVolumeMax() + withContext(Dispatchers.Main) { + if (!isAdded) return@withContext + if (max > 0) slider.valueTo = max.toFloat() + val level = prefs.getInt("door_volume_level", 0) + slider.value = level.coerceIn(0, slider.valueTo.toInt()).toFloat() + } + } + + toggle.setOnCheckedChangeListener { _, checked -> + prefs.edit().putBoolean("door_volume_enabled", checked).apply() + slider.isEnabled = checked + viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) { + if (checked) MG4Hardware.startDoorVolumeWatcher() + else MG4Hardware.stopDoorVolumeWatcher() + } + } + + slider.addOnChangeListener { _, value, fromUser -> + if (fromUser) prefs.edit().putInt("door_volume_level", value.toInt()).apply() + } } } diff --git a/app/src/main/java/com/mg4/control/ui/ConsoleFragment.kt b/app/src/main/java/com/mg4/control/ui/ConsoleFragment.kt index d5c4e2e0..42ab071c 100644 --- a/app/src/main/java/com/mg4/control/ui/ConsoleFragment.kt +++ b/app/src/main/java/com/mg4/control/ui/ConsoleFragment.kt @@ -2,6 +2,8 @@ package com.mg4.control.ui import android.graphics.Color import android.os.Bundle +import android.os.Handler +import android.os.Looper import android.text.SpannableStringBuilder import android.text.style.ForegroundColorSpan import android.view.LayoutInflater @@ -25,6 +27,19 @@ class ConsoleFragment : Fragment() { activity?.runOnUiThread { if (isAdded) renderLog() } } + // Auto-refresh périodique (status + logs) sans casser la position de scroll. + private val handler = Handler(Looper.getMainLooper()) + private var lastLogCount = -1 + private val refresh = object : Runnable { + override fun run() { + if (isAdded) { + renderStatus() + if (AppLogger.entries.size != lastLogCount) renderLog() + } + handler.postDelayed(this, 800L) + } + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View = inflater.inflate(R.layout.fragment_console, container, false) @@ -45,6 +60,7 @@ class ConsoleFragment : Fragment() { override fun onResume() { super.onResume() AppLogger.addListener(logListener) + handler.post(refresh) renderStatus() renderLog() } @@ -52,6 +68,7 @@ class ConsoleFragment : Fragment() { override fun onPause() { super.onPause() AppLogger.removeListener(logListener) + handler.removeCallbacks(refresh) } // ---- Status banner ---- @@ -77,7 +94,18 @@ class ConsoleFragment : Fragment() { // ---- Log list ---- + /** True si l'utilisateur est (quasi) tout en bas → on suit les nouveaux logs ; sinon on le laisse. */ + private fun isAtBottom(): Boolean { + val child = scrollView.getChildAt(0) ?: return true + val marginPx = (24 * resources.displayMetrics.density).toInt() + return child.bottom - (scrollView.height + scrollView.scrollY) <= marginPx + } + private fun renderLog() { + // Mémorise la position AVANT de remplacer le texte : on ne re-scrolle en bas + // que si l'utilisateur suivait déjà le bas (sinon on préserve sa lecture). + val followBottom = isAtBottom() + lastLogCount = AppLogger.entries.size val sb = SpannableStringBuilder() AppLogger.entries.forEach { entry -> val prefix = "[${entry.time}] " @@ -96,6 +124,6 @@ class ConsoleFragment : Fragment() { sb.setSpan(ForegroundColorSpan(color), start + prefix.length + tag.length, sb.length, 0) } textLog.text = sb - scrollView.post { scrollView.fullScroll(View.FOCUS_DOWN) } + if (followBottom) scrollView.post { scrollView.fullScroll(View.FOCUS_DOWN) } } } diff --git a/app/src/main/res/layout/fragment_audio.xml b/app/src/main/res/layout/fragment_audio.xml index 0e3a74b1..a29f388b 100644 --- a/app/src/main/res/layout/fragment_audio.xml +++ b/app/src/main/res/layout/fragment_audio.xml @@ -9,6 +9,7 @@ android:gravity="center" android:padding="20dp"> + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index df6bd624..ac90a9ab 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -273,6 +273,9 @@ Audio Loudness Klangverstärkung + Lautstärke bei Türöffnung + Senkt die Medienlautstärke beim Öffnen einer Vordertür + Zielpegel Profile wiederherstellen? %d gespeicherte(s) Profil(e) auf diesem Fahrzeug gefunden. Wiederherstellen? diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index 6d449de6..8a7a9e22 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -273,6 +273,9 @@ Audio Loudness Sound intensity boost + Door-open volume + Lowers media volume when a front door opens + Target level Restore profiles? %d saved profile(s) were found on this vehicle. Restore them? diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 44c2a22b..161f2c5e 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -273,6 +273,9 @@ Audio Loudness Refuerzo de intensidad de sonido + Volumen al abrir la puerta + Reduce el volumen multimedia al abrir una puerta delantera + Nivel objetivo ¿Restaurar perfiles? Se encontraron %d perfil(es) guardado(s) en este vehículo. ¿Restaurarlos? diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index c11aaa9d..fe7e986f 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -273,6 +273,9 @@ Audio Loudness Potenziamento dell\'intensità sonora + Volume all\'apertura della porta + Abbassa il volume multimediale all\'apertura di una porta anteriore + Livello target Ripristinare i profili? Trovati %d profilo/i salvato/i su questo veicolo. Ripristinarli? diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index a89d7653..c3adfc46 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -273,6 +273,9 @@ Audio Loudness Reforço de intensidade sonora + Volume ao abrir a porta + Baixa o volume de média ao abrir uma porta dianteira + Nível alvo Restaurar perfis? Foram encontrados %d perfil(is) guardado(s) neste veículo. Restaurar? diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c713c6a2..88f41572 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -275,6 +275,9 @@ Audio Loudness Boost d\'intensité sonore + Volume à l\'ouverture de porte + Baisse le volume média à l\'ouverture d\'une porte avant + Niveau cible Restaurer les profils ? %d profil(s) sauvegardé(s) ont été trouvés sur ce véhicule. Les restaurer ? From f4b997cbb131702e5b7b4f5130269852c6c5c63f Mon Sep 17 00:00:00 2001 From: SliDeeN Date: Tue, 30 Jun 2026 20:07:08 +0200 Subject: [PATCH 2/5] feature/volume-doors add restore audio + select doors --- .../com/mg4/control/hardware/MG4Hardware.kt | 48 ++++++++++++---- .../java/com/mg4/control/ui/AudioFragment.kt | 35 +++++++++--- app/src/main/res/layout/fragment_audio.xml | 55 +++++++++++++++++++ app/src/main/res/values-de/strings.xml | 4 ++ app/src/main/res/values-en/strings.xml | 4 ++ app/src/main/res/values-es/strings.xml | 4 ++ app/src/main/res/values-it/strings.xml | 4 ++ app/src/main/res/values-pt/strings.xml | 4 ++ app/src/main/res/values/strings.xml | 4 ++ 9 files changed, 143 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/com/mg4/control/hardware/MG4Hardware.kt b/app/src/main/java/com/mg4/control/hardware/MG4Hardware.kt index da493a82..f1f80034 100644 --- a/app/src/main/java/com/mg4/control/hardware/MG4Hardware.kt +++ b/app/src/main/java/com/mg4/control/hardware/MG4Hardware.kt @@ -3167,6 +3167,7 @@ object MG4Hardware { @Volatile private var sCarPropMgr: Any? = null @Volatile private var sDoorConnecting = false @Volatile private var sAnyFrontOpenPrev = false + @Volatile private var sVolumeBeforeDrop = -1 // volume mémorisé à l'ouverture (pour restauration) /** v1 : feature dispo sur SWI133 uniquement. */ fun hasDoorVolumeFeature(): Boolean = @@ -3178,6 +3179,18 @@ object MG4Hardware { private fun doorVolumeLevel(): Int = sAppContext?.getSharedPreferences("mg4_settings", 0)?.getInt("door_volume_level", 0) ?: 0 + private fun doorRestoreEnabled(): Boolean = + sAppContext?.getSharedPreferences("mg4_settings", 0)?.getBoolean("door_volume_restore", false) ?: false + + /** areaIds des portes choisies par l'utilisateur (gauche=0x1, droite=0x4). */ + private fun doorTriggerAreas(): IntArray { + val p = sAppContext?.getSharedPreferences("mg4_settings", 0) ?: return DOOR_FRONT_AREAS + val list = ArrayList(2) + if (p.getBoolean("door_volume_left", true)) list += 0x1 + if (p.getBoolean("door_volume_right", true)) list += 0x4 + return list.toIntArray() + } + fun startDoorVolumeWatcher() { if (!hasDoorVolumeFeature()) return sDoorWatcherOn = true @@ -3227,21 +3240,34 @@ object MG4Hardware { val poll = object : Runnable { override fun run() { if (sCarPropMgr == null) return - // Une porte AVANT est ouverte si DLOCK_DOOR_OPEN_STS == 1 sur l'areaId 0x1 ou 0x4. - var anyFrontOpen = false - for (area in DOOR_FRONT_AREAS) { + // Une porte choisie est ouverte si DLOCK_DOOR_OPEN_STS == 1 sur son areaId (0x1/0x4). + var anyOpen = false + for (area in doorTriggerAreas()) { val v = try { getInt.invoke(mgr, DOOR_OPEN_PROP, area) as? Int ?: -1 } catch (_: Exception) { -1 } - if (v == 1) anyFrontOpen = true + if (v == 1) anyOpen = true } - // Front « fermé→ouvert » → baisse du volume une seule fois. - if (anyFrontOpen && !sAnyFrontOpenPrev && sDoorWatcherOn && doorVolumeEnabled()) { - val level = doorVolumeLevel() - CoroutineScope(Dispatchers.IO).launch { - val ok = setMediaVolume(level) - AppLogger.i(DOORWATCH_TAG, "porte avant ouverte → setMediaVolume($level) = $ok") + if (sDoorWatcherOn && doorVolumeEnabled()) { + if (anyOpen && !sAnyFrontOpenPrev) { + // Ouverture : mémorise le volume actuel puis baisse au niveau cible. + val level = doorVolumeLevel() + CoroutineScope(Dispatchers.IO).launch { + sVolumeBeforeDrop = getMediaVolume() + val ok = setMediaVolume(level) + AppLogger.i(DOORWATCH_TAG, "porte ouverte → vol $sVolumeBeforeDrop→$level = $ok") + } + } else if (!anyOpen && sAnyFrontOpenPrev) { + // Fermeture : restaure le volume initial si l'option est activée. + val restore = sVolumeBeforeDrop + sVolumeBeforeDrop = -1 + if (doorRestoreEnabled() && restore >= 0) { + CoroutineScope(Dispatchers.IO).launch { + val ok = setMediaVolume(restore) + AppLogger.i(DOORWATCH_TAG, "porte fermée → restauration vol $restore = $ok") + } + } } } - sAnyFrontOpenPrev = anyFrontOpen + sAnyFrontOpenPrev = anyOpen h.postDelayed(this, 500L) } } diff --git a/app/src/main/java/com/mg4/control/ui/AudioFragment.kt b/app/src/main/java/com/mg4/control/ui/AudioFragment.kt index f1dd7113..d37cff52 100644 --- a/app/src/main/java/com/mg4/control/ui/AudioFragment.kt +++ b/app/src/main/java/com/mg4/control/ui/AudioFragment.kt @@ -5,6 +5,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.CheckBox import android.widget.Switch import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope @@ -52,16 +53,25 @@ class AudioFragment : Fragment() { return } - val prefs = requireContext().getSharedPreferences("mg4_settings", Context.MODE_PRIVATE) - val toggle = view.findViewById(R.id.switch_door_volume) - val slider = view.findViewById(R.id.slider_door_volume) + val prefs = requireContext().getSharedPreferences("mg4_settings", Context.MODE_PRIVATE) + val toggle = view.findViewById(R.id.switch_door_volume) + val slider = view.findViewById(R.id.slider_door_volume) + val restore = view.findViewById(R.id.switch_door_restore) + val cbLeft = view.findViewById(R.id.cb_door_left) + val cbRight = view.findViewById(R.id.cb_door_right) val enabled = prefs.getBoolean("door_volume_enabled", false) - toggle.isChecked = enabled - slider.isEnabled = enabled + toggle.isChecked = enabled + restore.isChecked = prefs.getBoolean("door_volume_restore", false) + cbLeft.isChecked = prefs.getBoolean("door_volume_left", true) + cbRight.isChecked = prefs.getBoolean("door_volume_right", true) - // Si déjà activé, (re)démarre le watcher à l'ouverture de l'onglet (idempotent) : - // garantit la connexion Car / les logs MG4_DOOR sans avoir à re-toggler. + fun setSubEnabled(e: Boolean) { + slider.isEnabled = e; restore.isEnabled = e; cbLeft.isEnabled = e; cbRight.isEnabled = e + } + setSubEnabled(enabled) + + // Si déjà activé, (re)démarre le watcher à l'ouverture de l'onglet (idempotent). if (enabled) { viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) { MG4Hardware.startDoorVolumeWatcher() } } @@ -79,7 +89,7 @@ class AudioFragment : Fragment() { toggle.setOnCheckedChangeListener { _, checked -> prefs.edit().putBoolean("door_volume_enabled", checked).apply() - slider.isEnabled = checked + setSubEnabled(checked) viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) { if (checked) MG4Hardware.startDoorVolumeWatcher() else MG4Hardware.stopDoorVolumeWatcher() @@ -89,5 +99,14 @@ class AudioFragment : Fragment() { slider.addOnChangeListener { _, value, fromUser -> if (fromUser) prefs.edit().putInt("door_volume_level", value.toInt()).apply() } + restore.setOnCheckedChangeListener { _, checked -> + prefs.edit().putBoolean("door_volume_restore", checked).apply() + } + cbLeft.setOnCheckedChangeListener { _, checked -> + prefs.edit().putBoolean("door_volume_left", checked).apply() + } + cbRight.setOnCheckedChangeListener { _, checked -> + prefs.edit().putBoolean("door_volume_right", checked).apply() + } } } diff --git a/app/src/main/res/layout/fragment_audio.xml b/app/src/main/res/layout/fragment_audio.xml index a29f388b..33edaaae 100644 --- a/app/src/main/res/layout/fragment_audio.xml +++ b/app/src/main/res/layout/fragment_audio.xml @@ -120,6 +120,61 @@ android:value="0" android:stepSize="1" /> + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index ac90a9ab..21d80bd6 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -276,6 +276,10 @@ Lautstärke bei Türöffnung Senkt die Medienlautstärke beim Öffnen einer Vordertür Zielpegel + Beim Schließen wiederherstellen + Auslösende Türen + Links + Rechts Profile wiederherstellen? %d gespeicherte(s) Profil(e) auf diesem Fahrzeug gefunden. Wiederherstellen? diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index 8a7a9e22..615ac2a5 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -276,6 +276,10 @@ Door-open volume Lowers media volume when a front door opens Target level + Restore on close + Trigger doors + Left + Right Restore profiles? %d saved profile(s) were found on this vehicle. Restore them? diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 161f2c5e..fef8c87a 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -276,6 +276,10 @@ Volumen al abrir la puerta Reduce el volumen multimedia al abrir una puerta delantera Nivel objetivo + Restaurar al cerrar + Puertas que activan + Izquierda + Derecha ¿Restaurar perfiles? Se encontraron %d perfil(es) guardado(s) en este vehículo. ¿Restaurarlos? diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index fe7e986f..a19b9cc0 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -276,6 +276,10 @@ Volume all\'apertura della porta Abbassa il volume multimediale all\'apertura di una porta anteriore Livello target + Ripristina alla chiusura + Porte di attivazione + Sinistra + Destra Ripristinare i profili? Trovati %d profilo/i salvato/i su questo veicolo. Ripristinarli? diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index c3adfc46..5f722c3e 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -276,6 +276,10 @@ Volume ao abrir a porta Baixa o volume de média ao abrir uma porta dianteira Nível alvo + Restaurar ao fechar + Portas de ativação + Esquerda + Direita Restaurar perfis? Foram encontrados %d perfil(is) guardado(s) neste veículo. Restaurar? diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 88f41572..de3e35df 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -278,6 +278,10 @@ Volume à l\'ouverture de porte Baisse le volume média à l\'ouverture d\'une porte avant Niveau cible + Restaurer à la fermeture + Portes déclencheuses + Gauche + Droite Restaurer les profils ? %d profil(s) sauvegardé(s) ont été trouvés sur ce véhicule. Les restaurer ? From c15d78c2115496756a1dde012007522684ea35f1 Mon Sep 17 00:00:00 2001 From: SliDeeN Date: Wed, 1 Jul 2026 00:32:57 +0200 Subject: [PATCH 3/5] feature/volume-doors add support for all firmware --- .../com/mg4/control/hardware/MG4Hardware.kt | 91 ++++++++++++++----- 1 file changed, 70 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/com/mg4/control/hardware/MG4Hardware.kt b/app/src/main/java/com/mg4/control/hardware/MG4Hardware.kt index f1f80034..e90c70f6 100644 --- a/app/src/main/java/com/mg4/control/hardware/MG4Hardware.kt +++ b/app/src/main/java/com/mg4/control/hardware/MG4Hardware.kt @@ -3119,35 +3119,67 @@ object MG4Hardware { } catch (e: Exception) { AppLogger.w(TAG, " setLoudnessOldSdk exc: ${e.message}"); false } } - // ── Volume média (SmartSoundManager, old-SDK) — VOL_TYPE_MEDIA=0 ──────────── - private const val VOL_TYPE_MEDIA = 0 + // ── Volume média — VOL_TYPE_MEDIA=0 ──────────────────────────────────────── + // Routage par firmware : old-SDK (133/68/165) → SmartSoundManager (reflection) ; + // A9 (69/131/132) → ICarAudioService via binder caradapter (tx getMax=0x5 / getVol=0x6 / setVol=0x7). + private const val VOL_TYPE_MEDIA = 0 + private const val AUDIO_GET_MAX_VOL = 0x5 + private const val AUDIO_GET_VOLUME = 0x6 + private const val AUDIO_SET_VOLUME = 0x7 /** Max du volume média (borne le slider). -1 si indisponible. */ - fun getMediaVolumeMax(): Int { - val m = sSmartSound ?: return -1 - return try { - m.javaClass.getMethod("getMaxVolume", Int::class.javaPrimitiveType).invoke(m, VOL_TYPE_MEDIA) as? Int ?: -1 - } catch (e: Exception) { AppLogger.w(TAG, "getMediaVolumeMax exc: ${e.message}"); -1 } + fun getMediaVolumeMax(): Int = when { + isOldSdkSound() -> smartSoundGetInt("getMaxVolume", VOL_TYPE_MEDIA) + isA9Sound() -> audioGetArg(AUDIO_GET_MAX_VOL, VOL_TYPE_MEDIA) + else -> -1 } /** Volume média courant. -1 si indisponible. */ - fun getMediaVolume(): Int { - val m = sSmartSound ?: return -1 - return try { - m.javaClass.getMethod("getVolume", Int::class.javaPrimitiveType).invoke(m, VOL_TYPE_MEDIA) as? Int ?: -1 - } catch (e: Exception) { -1 } + fun getMediaVolume(): Int = when { + isOldSdkSound() -> smartSoundGetInt("getVolume", VOL_TYPE_MEDIA) + isA9Sound() -> audioGetArg(AUDIO_GET_VOLUME, VOL_TYPE_MEDIA) + else -> -1 } /** Fixe le volume média. setVolume(type, niveau, flags=0). */ - fun setMediaVolume(level: Int): Boolean { + fun setMediaVolume(level: Int): Boolean = when { + isOldSdkSound() -> smartSoundSetVolume(level) + isA9Sound() -> audioSet3(AUDIO_SET_VOLUME, VOL_TYPE_MEDIA, level, 0) + else -> false + } + + // old-SDK : SmartSoundManager (reflection) + private fun smartSoundGetInt(method: String, arg: Int): Int { + val m = sSmartSound ?: return -1 + return try { m.javaClass.getMethod(method, Int::class.javaPrimitiveType).invoke(m, arg) as? Int ?: -1 } + catch (_: Exception) { -1 } + } + private fun smartSoundSetVolume(level: Int): Boolean { val m = sSmartSound ?: return false return try { - m.javaClass.getMethod( - "setVolume", - Int::class.javaPrimitiveType, Int::class.javaPrimitiveType, Int::class.javaPrimitiveType - ).invoke(m, VOL_TYPE_MEDIA, level, 0) - true - } catch (e: Exception) { AppLogger.w(TAG, "setMediaVolume exc: ${e.message}"); false } + m.javaClass.getMethod("setVolume", Int::class.javaPrimitiveType, Int::class.javaPrimitiveType, Int::class.javaPrimitiveType) + .invoke(m, VOL_TYPE_MEDIA, level, 0); true + } catch (e: Exception) { AppLogger.w(TAG, "setMediaVolume oldSdk exc: ${e.message}"); false } + } + + // A9 : ICarAudioService via binder. getVolume(usage)/getMaxVolume(usage) = 1 arg ; setVolume(usage,val,flags) = 3 args. + private fun audioGetArg(txCode: Int, arg: Int): Int { + val h = sAudioHelper ?: return -1 + if (!h.isBinderAlive) { sAudioHelper = null; return -1 } + val data = Parcel.obtain(); val reply = Parcel.obtain() + return try { + data.writeInterfaceToken(sAudioDescriptor); data.writeInt(arg) + if (h.transact(txCode, data, reply, 0)) { reply.readException(); reply.readInt() } else -1 + } catch (_: Exception) { -1 } finally { data.recycle(); reply.recycle() } + } + private fun audioSet3(txCode: Int, a: Int, b: Int, c: Int): Boolean { + val h = sAudioHelper ?: return false + if (!h.isBinderAlive) { sAudioHelper = null; return false } + val data = Parcel.obtain(); val reply = Parcel.obtain() + return try { + data.writeInterfaceToken(sAudioDescriptor); data.writeInt(a); data.writeInt(b); data.writeInt(c) + h.transact(txCode, data, reply, 0).also { if (it) reply.readException() } + } catch (_: Exception) { false } finally { data.recycle(); reply.recycle() } } // ── Baisse du volume à l'ouverture d'une porte avant (v1 : SWI133) ────────── @@ -3169,9 +3201,10 @@ object MG4Hardware { @Volatile private var sAnyFrontOpenPrev = false @Volatile private var sVolumeBeforeDrop = -1 // volume mémorisé à l'ouverture (pour restauration) - /** v1 : feature dispo sur SWI133 uniquement. */ + /** Dispo sur tous les firmwares connus (détection porte = CarPropertyManager AOSP). + * Confirmé SWI133 ; old-SDK 68/165 = même famille ; A9 à valider (dump DISPO en aide). */ fun hasDoorVolumeFeature(): Boolean = - FirmwareInfo.getGeneration() == FirmwareInfo.Gen.SWI133 + FirmwareInfo.getGeneration() != FirmwareInfo.Gen.UNKNOWN private fun doorVolumeEnabled(): Boolean = sAppContext?.getSharedPreferences("mg4_settings", 0)?.getBoolean("door_volume_enabled", false) ?: false @@ -3235,6 +3268,7 @@ object MG4Hardware { val getInt = try { mgr.javaClass.getMethod("getIntProperty", Int::class.javaPrimitiveType, Int::class.javaPrimitiveType) } catch (e: Exception) { AppLogger.w(DOORWATCH_TAG, "getIntProperty introuvable: ${e.message}"); return } + dumpDoorProps(mgr) // découverte ponctuelle : propriétés de zone PORTE dispo (aide multi-firmware) AppLogger.i(DOORWATCH_TAG, "watcher porte actif (DLOCK_DOOR_OPEN_STS AV)") val h = Handler(Looper.getMainLooper()) val poll = object : Runnable { @@ -3273,6 +3307,21 @@ object MG4Hardware { } h.post(poll) } + + /** Dump ponctuel des propriétés de zone PORTE disponibles (aide à l'adaptation par firmware). */ + private fun dumpDoorProps(mgr: Any) { + try { + val list = mgr.javaClass.getMethod("getPropertyList").invoke(mgr) as? List<*> ?: return + for (cfg in list) { + cfg ?: continue + val pid = cfg.javaClass.getMethod("getPropertyId").invoke(cfg) as? Int ?: continue + if ((pid and 0x0f000000) != 0x06000000) continue // zone PORTE uniquement + val areas = (cfg.javaClass.getMethod("getAreaIds").invoke(cfg) as? IntArray) ?: IntArray(0) + AppLogger.i(DOORWATCH_TAG, "DISPO 0x${pid.toString(16)} areas=${areas.joinToString { "0x${it.toString(16)}" }}") + } + } catch (e: Exception) { AppLogger.w(DOORWATCH_TAG, "dumpDoorProps: ${e.message}") } + } + fun getSpeedVolumeLevel(): Int = audioGet(AUDIO_GET_SPEED_VOL) fun setSpeedVolumeLevel(l: Int): Boolean = audioSet(AUDIO_SET_SPEED_VOL, l.coerceIn(AUDIO_TYPE_MIN, AUDIO_TYPE_MAX)) fun getSoundFieldType(): Int = -1 From a99ef398dee48334060eacdfda8820ca6828b246 Mon Sep 17 00:00:00 2001 From: SliDeeN Date: Mon, 13 Jul 2026 14:49:07 +0200 Subject: [PATCH 4/5] feature/volume-doors only SWI133/32 --- .../com/mg4/control/hardware/MG4Hardware.kt | 347 ++++++++++++------ .../java/com/mg4/control/ui/AudioFragment.kt | 19 +- .../com/mg4/control/ui/SettingsFragment.kt | 56 ++- app/src/main/res/layout/fragment_audio.xml | 45 +-- app/src/main/res/values-de/strings.xml | 5 +- app/src/main/res/values-en/strings.xml | 5 +- app/src/main/res/values-es/strings.xml | 5 +- app/src/main/res/values-it/strings.xml | 5 +- app/src/main/res/values-pt/strings.xml | 5 +- app/src/main/res/values/strings.xml | 5 +- 10 files changed, 309 insertions(+), 188 deletions(-) diff --git a/app/src/main/java/com/mg4/control/hardware/MG4Hardware.kt b/app/src/main/java/com/mg4/control/hardware/MG4Hardware.kt index e90c70f6..d82dba1f 100644 --- a/app/src/main/java/com/mg4/control/hardware/MG4Hardware.kt +++ b/app/src/main/java/com/mg4/control/hardware/MG4Hardware.kt @@ -8,6 +8,8 @@ import android.os.Handler import android.os.IBinder import android.os.Looper import android.os.Parcel +import java.lang.reflect.InvocationHandler +import java.lang.reflect.Proxy import com.mg4.control.debug.AppLogger import com.mg4.control.model.DriveMode import com.mg4.control.model.RegenLevel @@ -123,7 +125,7 @@ object MG4Hardware { private const val GENERAL_MANAGER_CLASS = "com.saicmotor.sdk.systemsettings.GeneralManager" private const val BRIGHTNESS_NATIVE_MAX = 255 - // Loudness — ancien SDK (SWI133/68/165) : SmartSoundManager.setLoudnessState(Boolean)/getLoudnessState(). + // Volume média — ancien SDK (SWI133/68/165) : SmartSoundManager.getVolume/setVolume/getMaxVolume(type). // Même SDK systemsettings/BaseManager que GeneralManager (singleton sInstance + init(Context, listener)). private const val SMART_SOUND_MANAGER_CLASS = "com.saicmotor.sdk.systemsettings.SmartSoundManager" private const val BRIGHTNESS_MIN_PERCENT = 5 // plancher de sécurité : ne jamais éteindre l'écran @@ -348,6 +350,12 @@ object MG4Hardware { AppLogger.i(TAG, " ✓ Katman2: vehiclesetting binder OK") else AppLogger.w(TAG, " ✗ Katman2: vehiclesetting null (SELinux — expected)") + + // Démarrage auto du watcher porte au boot si la feature est activée (tous firmwares). + startDoorWatcherIfEnabled() + // Connexion Car établie même si la feature est OFF → la sonde Diagnostic peut lire les portes. + if (hasDoorVolumeFeature()) connectCarProperty() + AppLogger.i(TAG, "========================================") } @@ -2979,8 +2987,8 @@ object MG4Hardware { return gen == FirmwareInfo.Gen.SWI133 || gen == FirmwareInfo.Gen.SWI68 || gen == FirmwareInfo.Gen.SWI165 } - /** Le contrôle audio (loudness) est disponible sur A9 ET sur l'ancien SDK. */ - fun hasAudioControl(): Boolean = isA9Sound() || isOldSdkSound() + /** Onglet Audio (baisse volume à l'ouverture de porte) : uniquement là où c'est fonctionnel. */ + fun hasAudioControl(): Boolean = hasDoorVolumeFeature() private const val DESCRIPTOR_CARADAPTER = "com.saicmotor.carapi.ICarAdapterService" private const val TX_QUERY_AUDIO_CLIENT = 1 @@ -2988,8 +2996,6 @@ object MG4Hardware { private const val AUDIO_SET_FADER_FRONT = 12 private const val AUDIO_SET_BALANCE_RIGHT = 13 - private const val AUDIO_SET_LOUDNESS = 15 - private const val AUDIO_GET_LOUDNESS = 16 private const val AUDIO_SET_SPEED_VOL = 17 private const val AUDIO_GET_SPEED_VOL = 18 private const val AUDIO_SET_3D_EFFECT = 26 @@ -3089,36 +3095,6 @@ object MG4Hardware { fun setAudioBalance(v: Int): Boolean = audioSet(AUDIO_SET_BALANCE_RIGHT, v.coerceIn(AUDIO_LEVEL_MIN, AUDIO_LEVEL_MAX)) fun getAudioFader(): Int = audioGet(AUDIO_GET_FADER) fun setAudioFader(v: Int): Boolean = audioSet(AUDIO_SET_FADER_FRONT, v.coerceIn(AUDIO_LEVEL_MIN, AUDIO_LEVEL_MAX)) - // Loudness : l'API réelle est booléenne (1=ON / 0=OFF) — l'appelant (AudioFragment) envoie 1/0. - // Routage par firmware : A9 → binder caradapter ; old-SDK → SmartSoundManager (reflection). - fun getLoudnessState(): Int = when { - isA9Sound() -> audioGet(AUDIO_GET_LOUDNESS) - isOldSdkSound() -> getLoudnessOldSdk() - else -> -1 - } - fun setLoudnessState(s: Int): Boolean = when { - isA9Sound() -> audioSet(AUDIO_SET_LOUDNESS, s) - isOldSdkSound() -> setLoudnessOldSdk(s != 0) - else -> false - } - - // ── old-SDK : SmartSoundManager.getLoudnessState()/setLoudnessState(boolean) via reflection ── - private fun getLoudnessOldSdk(): Int { - val m = sSmartSound ?: return -1 - return try { - val on = m.javaClass.getMethod("getLoudnessState").invoke(m) as? Boolean ?: return -1 - if (on) 1 else 0 - } catch (e: Exception) { AppLogger.w(TAG, " getLoudnessOldSdk exc: ${e.message}"); -1 } - } - - private fun setLoudnessOldSdk(on: Boolean): Boolean { - val m = sSmartSound ?: return false - return try { - m.javaClass.getMethod("setLoudnessState", Boolean::class.javaPrimitiveType).invoke(m, on) - true - } catch (e: Exception) { AppLogger.w(TAG, " setLoudnessOldSdk exc: ${e.message}"); false } - } - // ── Volume média — VOL_TYPE_MEDIA=0 ──────────────────────────────────────── // Routage par firmware : old-SDK (133/68/165) → SmartSoundManager (reflection) ; // A9 (69/131/132) → ICarAudioService via binder caradapter (tx getMax=0x5 / getVol=0x6 / setVol=0x7). @@ -3127,25 +3103,40 @@ object MG4Hardware { private const val AUDIO_GET_VOLUME = 0x6 private const val AUDIO_SET_VOLUME = 0x7 + private const val VOL_TAG = "MG4_VOL" + /** Max du volume média (borne le slider). -1 si indisponible. */ - fun getMediaVolumeMax(): Int = when { - isOldSdkSound() -> smartSoundGetInt("getMaxVolume", VOL_TYPE_MEDIA) - isA9Sound() -> audioGetArg(AUDIO_GET_MAX_VOL, VOL_TYPE_MEDIA) - else -> -1 + fun getMediaVolumeMax(): Int { + val v = when { + isOldSdkSound() -> smartSoundGetInt("getMaxVolume", VOL_TYPE_MEDIA) + isA9Sound() -> audioGetArg(AUDIO_GET_MAX_VOL, VOL_TYPE_MEDIA) + else -> -1 + } + AppLogger.i(VOL_TAG, "getMediaVolumeMax = $v [oldSdk=${isOldSdkSound()} a9=${isA9Sound()} smartSound=${sSmartSound != null} audioHelper=${sAudioHelper != null}]") + logMediaVolumeDiag() // A9 : compare type-0 vs group-id (no-op ailleurs) + return v } /** Volume média courant. -1 si indisponible. */ - fun getMediaVolume(): Int = when { - isOldSdkSound() -> smartSoundGetInt("getVolume", VOL_TYPE_MEDIA) - isA9Sound() -> audioGetArg(AUDIO_GET_VOLUME, VOL_TYPE_MEDIA) - else -> -1 + fun getMediaVolume(): Int { + val v = when { + isOldSdkSound() -> smartSoundGetInt("getVolume", VOL_TYPE_MEDIA) + isA9Sound() -> audioGetArg(AUDIO_GET_VOLUME, VOL_TYPE_MEDIA) + else -> -1 + } + AppLogger.i(VOL_TAG, "getMediaVolume = $v") + return v } /** Fixe le volume média. setVolume(type, niveau, flags=0). */ - fun setMediaVolume(level: Int): Boolean = when { - isOldSdkSound() -> smartSoundSetVolume(level) - isA9Sound() -> audioSet3(AUDIO_SET_VOLUME, VOL_TYPE_MEDIA, level, 0) - else -> false + fun setMediaVolume(level: Int): Boolean { + val ok = when { + isOldSdkSound() -> smartSoundSetVolume(level) + isA9Sound() -> audioSet3(AUDIO_SET_VOLUME, VOL_TYPE_MEDIA, level, 0) + else -> false + } + AppLogger.i(VOL_TAG, "setMediaVolume($level) = $ok") + return ok } // old-SDK : SmartSoundManager (reflection) @@ -3182,6 +3173,20 @@ object MG4Hardware { } catch (_: Exception) { false } finally { data.recycle(); reply.recycle() } } + // Diagnostic A9 : le param de getVolume/getMaxVolume est-il un "type" (0=media) ou un + // "group id" (AAOS) ? On logge les deux pour comparer au max réel de la voiture. + private const val AUDIO_GET_GROUP_FOR_USAGE = 0xe // getVolumeGroupIdForUsage(usage) + private const val USAGE_MEDIA_AAOS = 1 // AudioAttributes.USAGE_MEDIA + fun logMediaVolumeDiag() { + if (!isA9Sound()) return + val maxT = audioGetArg(AUDIO_GET_MAX_VOL, VOL_TYPE_MEDIA) + val volT = audioGetArg(AUDIO_GET_VOLUME, VOL_TYPE_MEDIA) + val grp = audioGetArg(AUDIO_GET_GROUP_FOR_USAGE, USAGE_MEDIA_AAOS) + val maxG = if (grp in 0..64) audioGetArg(AUDIO_GET_MAX_VOL, grp) else -1 + val volG = if (grp in 0..64) audioGetArg(AUDIO_GET_VOLUME, grp) else -1 + AppLogger.i(VOL_TAG, "A9 diag: parType0[max=$maxT vol=$volT] groupForUsage(MEDIA)=$grp parGroup[max=$maxG vol=$volG]") + } + // ── Baisse du volume à l'ouverture d'une porte avant (v1 : SWI133) ────────── // Détection via l'API Car AOSP **CarPropertyManager** (service "property") + permission // CAR_VENDOR_EXTENSION (déjà déclarée). IDs portes AVANT confirmés par un utilisateur : @@ -3194,17 +3199,23 @@ object MG4Hardware { // areaId 0x1 = porte AV-gauche, 0x4 = AV-droite ; valeur 1 = ouverte, 0 = fermée. private const val DOOR_OPEN_PROP = 0x2640c623 private val DOOR_FRONT_AREAS = intArrayOf(0x1, 0x4) + private val sDoorReadLast = HashMap() + @Volatile private var sDoorSubProperty = false // voie A : property.registerListener attachée + @Volatile private var sDoorSubDoorlock = false // voie B : doorlock.registerCallback attachée @Volatile private var sDoorWatcherOn = false @Volatile private var sCarInstance: Any? = null - @Volatile private var sCarPropMgr: Any? = null + @Volatile private var sCarPropMgr: Any? = null // CarPropertyManager (service "property") + @Volatile private var sCarDoorMgr: Any? = null // CarDoorLockManager (service "doorlock") @Volatile private var sDoorConnecting = false @Volatile private var sAnyFrontOpenPrev = false - @Volatile private var sVolumeBeforeDrop = -1 // volume mémorisé à l'ouverture (pour restauration) + @Volatile private var sVolumeBeforeDrop = -1 // volume mémorisé à l'ouverture (pour restauration) - /** Dispo sur tous les firmwares connus (détection porte = CarPropertyManager AOSP). - * Confirmé SWI133 ; old-SDK 68/165 = même famille ; A9 à valider (dump DISPO en aide). */ - fun hasDoorVolumeFeature(): Boolean = - FirmwareInfo.getGeneration() != FirmwareInfo.Gen.UNKNOWN + /** Baisse du volume à l'ouverture de porte : détection DLOCK_DOOR_OPEN_STS via CarPropertyManager. + * Lisible/fonctionnel uniquement sur SWI132 et SWI133 ; ailleurs le prop n'est pas exposé à l'app. */ + fun hasDoorVolumeFeature(): Boolean { + val gen = FirmwareInfo.getGeneration() + return gen == FirmwareInfo.Gen.SWI132 || gen == FirmwareInfo.Gen.SWI133 + } private fun doorVolumeEnabled(): Boolean = sAppContext?.getSharedPreferences("mg4_settings", 0)?.getBoolean("door_volume_enabled", false) ?: false @@ -3224,6 +3235,11 @@ object MG4Hardware { return list.toIntArray() } + /** Démarrage auto au boot (appelé par init) : ne lance le watcher que si la feature est activée. */ + fun startDoorWatcherIfEnabled() { + if (hasDoorVolumeFeature() && doorVolumeEnabled()) startDoorVolumeWatcher() + } + fun startDoorVolumeWatcher() { if (!hasDoorVolumeFeature()) return sDoorWatcherOn = true @@ -3235,9 +3251,35 @@ object MG4Hardware { AppLogger.i(TAG, " DoorVolumeWatcher: déclenchement désactivé") } - /** Connexion (async) à l'API Car AOSP pour obtenir CarPropertyManager (service "property"). */ + /** Sonde du bouton Diagnostic : logge le volume + l'état des portes à l'instant du clic. */ + fun runDoorVolumeDiag() { + AppLogger.i(VOL_TAG, "── DIAG (bouton Diagnostic) ──") + getMediaVolumeMax() + getMediaVolume() + connectCarProperty() // idempotent ; normalement déjà connecté depuis l'init + registerDoorCallback() // re-tente la souscription si pas encore posée + probeDoorSnapshot() + } + + private fun probeDoorSnapshot() { + if (sCarPropMgr == null && sCarDoorMgr == null) { + AppLogger.w(DOORWATCH_TAG, "DIAG porte: aucun manager Car (createCar échec ?)") + return + } + AppLogger.i(DOORWATCH_TAG, "DIAG porte: property=${sCarPropMgr != null} doorlock=${sCarDoorMgr != null}") + for (area in DOOR_FRONT_AREAS) { + val v = readDoorOpen(area) + AppLogger.i(DOORWATCH_TAG, "DIAG area=0x${area.toString(16)} = ${v ?: "illisible"}") + } + AppLogger.i(DOORWATCH_TAG, "DIAG souscription: property=$sDoorSubProperty doorlock=$sDoorSubDoorlock état=" + + if (sDoorReadLast.isEmpty()) "(aucun event reçu)" + else sDoorReadLast.entries.joinToString { "0x${it.key.toString(16)}=${it.value}" }) + } + + /** Connexion (async) à l'API Car AOSP → CarPropertyManager ("property") ET CarDoorLockManager + * ("doorlock"). Selon le firmware, la porte est exposée par l'un ou l'autre → on lit via les deux. */ private fun connectCarProperty() { - if (sCarPropMgr != null || sDoorConnecting) return + if (sCarPropMgr != null || sCarDoorMgr != null || sDoorConnecting) return val ctx = sAppContext ?: return sDoorConnecting = true try { @@ -3246,80 +3288,175 @@ object MG4Hardware { override fun onServiceConnected(name: ComponentName?, binder: IBinder?) { try { val car = sCarInstance ?: return - val mgr = carCls.getMethod("getCarManager", String::class.java).invoke(car, "property") - if (mgr != null) { sCarPropMgr = mgr; startDoorPolling(mgr) } - else AppLogger.w(DOORWATCH_TAG, "getCarManager(property) = null") + val getMgr = carCls.getMethod("getCarManager", String::class.java) + sCarPropMgr = try { getMgr.invoke(car, "property") } catch (_: Exception) { null } + sCarDoorMgr = try { getMgr.invoke(car, "doorlock") } catch (_: Exception) { null } + AppLogger.i(DOORWATCH_TAG, "managers: property=${sCarPropMgr != null} doorlock=${sCarDoorMgr != null}") + if (sCarPropMgr != null || sCarDoorMgr != null) startDoorPolling() + else AppLogger.w(DOORWATCH_TAG, "aucun manager porte disponible") } catch (e: Exception) { AppLogger.w(DOORWATCH_TAG, "getCarManager échec: ${e.message}") } } - override fun onServiceDisconnected(name: ComponentName?) { sCarPropMgr = null } + override fun onServiceDisconnected(name: ComponentName?) { sCarPropMgr = null; sCarDoorMgr = null; sDoorSubProperty = false; sDoorSubDoorlock = false } } val car = carCls.getMethod("createCar", Context::class.java, ServiceConnection::class.java) .invoke(null, ctx, conn) sCarInstance = car try { carCls.getMethod("connect").invoke(car) } catch (_: Exception) {} - AppLogger.i(DOORWATCH_TAG, "connexion Car (property)…") + AppLogger.i(DOORWATCH_TAG, "connexion Car (property + doorlock)…") } catch (e: Exception) { sDoorConnecting = false AppLogger.w(DOORWATCH_TAG, "Car createCar échec: ${e.message}") } } - private fun startDoorPolling(mgr: Any) { - val getInt = try { - mgr.javaClass.getMethod("getIntProperty", Int::class.javaPrimitiveType, Int::class.javaPrimitiveType) - } catch (e: Exception) { AppLogger.w(DOORWATCH_TAG, "getIntProperty introuvable: ${e.message}"); return } - dumpDoorProps(mgr) // découverte ponctuelle : propriétés de zone PORTE dispo (aide multi-firmware) - AppLogger.i(DOORWATCH_TAG, "watcher porte actif (DLOCK_DOOR_OPEN_STS AV)") + // Lit DLOCK_DOOR_OPEN_STS(areaId) via CarPropertyManager.getIntProperty puis, en repli, + // via CarDoorLockManager.getProperty(Integer, propId, areaId).getValue(). null si aucun. + private fun readDoorOpen(area: Int): Int? { + sCarPropMgr?.let { m -> + try { + return m.javaClass.getMethod("getIntProperty", Int::class.javaPrimitiveType, Int::class.javaPrimitiveType) + .invoke(m, DOOR_OPEN_PROP, area) as? Int + } catch (_: Exception) {} + } + sCarDoorMgr?.let { m -> + try { + val cpv = m.javaClass.getMethod("getProperty", Class::class.java, Int::class.javaPrimitiveType, Int::class.javaPrimitiveType) + .invoke(m, Integer::class.java, DOOR_OPEN_PROP, area) ?: return null + return cpv.javaClass.getMethod("getValue").invoke(cpv) as? Int + } catch (_: Exception) {} + } + return null + } + + private fun startDoorPolling() { + registerDoorCallback() // souscription ON_CHANGE (complète le poll) + AppLogger.i(DOORWATCH_TAG, "watcher porte actif (property=${sCarPropMgr != null} doorlock=${sCarDoorMgr != null})") val h = Handler(Looper.getMainLooper()) val poll = object : Runnable { override fun run() { - if (sCarPropMgr == null) return - // Une porte choisie est ouverte si DLOCK_DOOR_OPEN_STS == 1 sur son areaId (0x1/0x4). - var anyOpen = false - for (area in doorTriggerAreas()) { - val v = try { getInt.invoke(mgr, DOOR_OPEN_PROP, area) as? Int ?: -1 } catch (_: Exception) { -1 } - if (v == 1) anyOpen = true + if (sCarPropMgr == null && sCarDoorMgr == null) return + // DLOCK_DOOR_OPEN_STS sur les portes avant (0x1 gauche / 0x4 droite). + for (area in DOOR_FRONT_AREAS) { + val v = readDoorOpen(area) ?: continue + onDoorAreaValue(area, v) + } + h.postDelayed(this, 500L) + } + } + h.post(poll) + } + + /** Point d'entrée unique pour une valeur porte (poll OU événement de souscription). + * Met à jour l'état, log en change-only, puis réévalue le déclenchement volume. */ + @Synchronized + private fun onDoorAreaValue(area: Int, v: Int) { + val prev = sDoorReadLast[area] + if (prev == null || prev != v) { + sDoorReadLast[area] = v + AppLogger.i(DOORWATCH_TAG, "area=0x${area.toString(16)} ${prev ?: "?"} → $v") + } + evaluateDoorTrigger() + } + + /** Recalcule "au moins une porte choisie ouverte" à partir de l'état accumulé et applique + * la baisse (front d'ouverture) / la restauration (front de fermeture). Idempotent. */ + private fun evaluateDoorTrigger() { + if (!(sDoorWatcherOn && doorVolumeEnabled())) return + val triggerAreas = doorTriggerAreas() + val anyOpen = triggerAreas.any { sDoorReadLast[it] == 1 } + if (anyOpen && !sAnyFrontOpenPrev) { + val level = doorVolumeLevel() + CoroutineScope(Dispatchers.IO).launch { + sVolumeBeforeDrop = getMediaVolume() + val ok = setMediaVolume(level) + AppLogger.i(DOORWATCH_TAG, "porte ouverte → vol $sVolumeBeforeDrop→$level = $ok") + } + } else if (!anyOpen && sAnyFrontOpenPrev) { + val restore = sVolumeBeforeDrop + sVolumeBeforeDrop = -1 + if (doorRestoreEnabled() && restore >= 0) { + CoroutineScope(Dispatchers.IO).launch { + val ok = setMediaVolume(restore) + AppLogger.i(DOORWATCH_TAG, "porte fermée → restauration vol $restore = $ok") } - if (sDoorWatcherOn && doorVolumeEnabled()) { - if (anyOpen && !sAnyFrontOpenPrev) { - // Ouverture : mémorise le volume actuel puis baisse au niveau cible. - val level = doorVolumeLevel() - CoroutineScope(Dispatchers.IO).launch { - sVolumeBeforeDrop = getMediaVolume() - val ok = setMediaVolume(level) - AppLogger.i(DOORWATCH_TAG, "porte ouverte → vol $sVolumeBeforeDrop→$level = $ok") + } + } + sAnyFrontOpenPrev = anyOpen + } + + /** InvocationHandler commun aux deux interfaces de callback (onChangeEvent/onErrorEvent). + * Filtre DLOCK_DOOR_OPEN_STS et alimente onDoorAreaValue. Gère aussi les méthodes Object. */ + private fun doorEventHandler(src: String) = InvocationHandler { proxy, method, args -> + when (method.name) { + "onChangeEvent" -> { + try { + val cpv = args?.getOrNull(0) + if (cpv != null) { + val pid = cpv.javaClass.getMethod("getPropertyId").invoke(cpv) as? Int + val area = cpv.javaClass.getMethod("getAreaId").invoke(cpv) as? Int ?: 0 + val raw = cpv.javaClass.getMethod("getValue").invoke(cpv) + val v = when (raw) { + is Boolean -> if (raw) 1 else 0 + is Number -> raw.toInt() + else -> null } - } else if (!anyOpen && sAnyFrontOpenPrev) { - // Fermeture : restaure le volume initial si l'option est activée. - val restore = sVolumeBeforeDrop - sVolumeBeforeDrop = -1 - if (doorRestoreEnabled() && restore >= 0) { - CoroutineScope(Dispatchers.IO).launch { - val ok = setMediaVolume(restore) - AppLogger.i(DOORWATCH_TAG, "porte fermée → restauration vol $restore = $ok") - } + if (pid == DOOR_OPEN_PROP && v != null) { + AppLogger.i(DOORWATCH_TAG, "EVENT[$src] area=0x${area.toString(16)} = $v") + onDoorAreaValue(area, v) } } - } - sAnyFrontOpenPrev = anyOpen - h.postDelayed(this, 500L) + } catch (e: Exception) { AppLogger.w(DOORWATCH_TAG, "onChangeEvent: ${e.message}") } + null } + "onErrorEvent" -> { AppLogger.w(DOORWATCH_TAG, "EVENT erreur porte"); null } + "hashCode" -> System.identityHashCode(proxy) + "equals" -> proxy === args?.getOrNull(0) + "toString" -> "MG4DoorListener@" + Integer.toHexString(System.identityHashCode(proxy)) + else -> null } - h.post(poll) } - /** Dump ponctuel des propriétés de zone PORTE disponibles (aide à l'adaptation par firmware). */ - private fun dumpDoorProps(mgr: Any) { - try { - val list = mgr.javaClass.getMethod("getPropertyList").invoke(mgr) as? List<*> ?: return - for (cfg in list) { - cfg ?: continue - val pid = cfg.javaClass.getMethod("getPropertyId").invoke(cfg) as? Int ?: continue - if ((pid and 0x0f000000) != 0x06000000) continue // zone PORTE uniquement - val areas = (cfg.javaClass.getMethod("getAreaIds").invoke(cfg) as? IntArray) ?: IntArray(0) - AppLogger.i(DOORWATCH_TAG, "DISPO 0x${pid.toString(16)} areas=${areas.joinToString { "0x${it.toString(16)}" }}") - } - } catch (e: Exception) { AppLogger.w(DOORWATCH_TAG, "dumpDoorProps: ${e.message}") } + /** + * S'abonne aux changements de DLOCK_DOOR_OPEN_STS. Nécessaire sur SWI69/131/68/165 où le prop + * n'est PAS lisible à la demande (getProperty lève IllegalArgumentException "Failed to get value") + * mais poussé en ON_CHANGE. IMPORTANT : le Proxy doit être défini par le classloader de l'APP + * (android.car est en BootClassLoader → Proxy.newProxyInstance y échoue). On tente DEUX voies : + * A) CarPropertyManager.registerListener (service "property") + * B) CarDoorLockManager.registerCallback (service "doorlock", comme la SystemUI d'origine) + */ + private fun registerDoorCallback() { + val cl = sAppContext?.classLoader ?: return + + // Voie A — CarPropertyManager.registerListener(CarPropertyEventListener, propId, rate). + // rate=5f (et non 0f) : si le VHAL déclare la prop CONTINUOUS, un rate 0 = aucune mise à jour. + if (!sDoorSubProperty) sCarPropMgr?.let { m -> + try { + val iface = cl.loadClass("android.car.hardware.property.CarPropertyManager\$CarPropertyEventListener") + val proxy = Proxy.newProxyInstance(cl, arrayOf(iface), doorEventHandler("property")) + val ok = m.javaClass.getMethod("registerListener", iface, + Int::class.javaPrimitiveType, Float::class.javaPrimitiveType) + .invoke(m, proxy, DOOR_OPEN_PROP, 5f) + sDoorSubProperty = true + AppLogger.i(DOORWATCH_TAG, "souscription porte (property.registerListener rate=5) OK=$ok") + } catch (e: Exception) { + val c = e.cause ?: e + AppLogger.w(DOORWATCH_TAG, "property.registerListener échec: ${c.javaClass.simpleName}: ${c.message}") + } + } + + // Voie B — CarDoorLockManager.registerCallback(CarDoorLockEventCallback) — voie de l'OEM + if (!sDoorSubDoorlock) sCarDoorMgr?.let { m -> + try { + val iface = cl.loadClass("android.car.hardware.doorlock.CarDoorLockManager\$CarDoorLockEventCallback") + val proxy = Proxy.newProxyInstance(cl, arrayOf(iface), doorEventHandler("doorlock")) + m.javaClass.getMethod("registerCallback", iface).invoke(m, proxy) + sDoorSubDoorlock = true + AppLogger.i(DOORWATCH_TAG, "souscription porte (doorlock.registerCallback) OK") + } catch (e: Exception) { + val c = e.cause ?: e + AppLogger.w(DOORWATCH_TAG, "doorlock.registerCallback échec: ${c.javaClass.simpleName}: ${c.message}") + } + } } fun getSpeedVolumeLevel(): Int = audioGet(AUDIO_GET_SPEED_VOL) diff --git a/app/src/main/java/com/mg4/control/ui/AudioFragment.kt b/app/src/main/java/com/mg4/control/ui/AudioFragment.kt index d37cff52..7d45d5e9 100644 --- a/app/src/main/java/com/mg4/control/ui/AudioFragment.kt +++ b/app/src/main/java/com/mg4/control/ui/AudioFragment.kt @@ -25,24 +25,7 @@ class AudioFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - // ── Loudness ─────────────────────────────────────────────────────── - val switch = view.findViewById(R.id.switch_loudness) - // Loudness : l'AIDL vendor est BOOLÉEN → 1 = ON, 0 = OFF (et non 2/1). - switch.setOnCheckedChangeListener { _, checked -> - if (switch.isPressed) { - viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) { - MG4Hardware.setLoudnessState(if (checked) 1 else 0) - } - } - } - viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) { - val state = MG4Hardware.getLoudnessState() - withContext(Dispatchers.Main) { - if (isAdded && state >= 0) switch.isChecked = (state == 1) - } - } - - // ── Volume à l'ouverture d'une porte (v1 : SWI133) ────────────────── + // ── Volume à l'ouverture d'une porte (SWI132/133) ─────────────────── setupDoorVolume(view) } diff --git a/app/src/main/java/com/mg4/control/ui/SettingsFragment.kt b/app/src/main/java/com/mg4/control/ui/SettingsFragment.kt index 5e0a6160..ec75d704 100644 --- a/app/src/main/java/com/mg4/control/ui/SettingsFragment.kt +++ b/app/src/main/java/com/mg4/control/ui/SettingsFragment.kt @@ -19,6 +19,7 @@ import android.os.Environment import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.Button import android.widget.ImageView import android.widget.LinearLayout import android.widget.ScrollView @@ -36,6 +37,7 @@ import com.mg4.control.debug.CrashLogger import com.mg4.control.hardware.MG4Hardware import com.mg4.control.update.UpdateChecker import com.mg4.control.update.UpdateDialogManager +import java.io.File import com.mg4.control.util.FirmwareHelper import com.mg4.control.util.LocaleHelper import kotlinx.coroutines.CoroutineScope @@ -351,11 +353,16 @@ class SettingsFragment : Fragment() { private fun showDiagnosticDialog() { val ctx = requireContext() + // Sonde diagnostic : logge volume + état des portes AVANT de rendre les logs, + // pour que le rapport les contienne (indépendant du toggle / de l'onglet Audio). + MG4Hardware.runDoorVolumeDiag() + val appVersion = try { ctx.packageManager.getPackageInfo(ctx.packageName, 0).versionName ?: "?" } catch (e: Exception) { "?" } // ── Layout : crash banner (optionnel) + rapport matériel ────────────── + var btnClearCrash: Button? = null val container = LinearLayout(ctx).apply { orientation = LinearLayout.VERTICAL layoutParams = LinearLayout.LayoutParams( @@ -387,6 +394,12 @@ class SettingsFragment : Fragment() { setBackgroundColor(ctx.getColor(R.color.dash_border)) } container.addView(divider) + + // "Effacer crash" déplacé dans le contenu (le slot neutre sert au Télécharger) + btnClearCrash = Button(ctx).apply { + text = getString(R.string.diag_clear_crash) + } + container.addView(btnClearCrash) } // Section rapport matériel @@ -429,26 +442,36 @@ class SettingsFragment : Fragment() { .setTitle(title) .setView(scrollView) .setPositiveButton(getString(R.string.diag_copy), null) - .setNeutralButton(if (crashLog != null) getString(R.string.diag_clear_crash) else null, null) + .setNeutralButton(getString(R.string.diag_download), null) .setNegativeButton(getString(R.string.nav_close), null) .create() dialog.window?.setBackgroundDrawable(ColorDrawable(ctx.getColor(R.color.dash_card))) + // Rapport court (30 dernières lignes) pour le presse-papier ; rapport complet pour le fichier. + fun buildReport(fullLog: Boolean) = buildString { + if (crashLog != null) { appendLine(crashLog); appendLine() } + appendLine(tvReport.text) + appendLine() + if (fullLog) { + val entries = AppLogger.entries + appendLine("─── AppLogger (${entries.size} entrées) ───") + entries.forEach { e -> appendLine("${e.time} [${e.level.name[0]}] ${e.tag}: ${e.msg}") } + } else appendLine(tvLogs.text) + } + dialog.setOnShowListener { // "Copier" — copie tout sans fermer le dialog dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.setOnClickListener { - val fullReport = buildString { - if (crashLog != null) { appendLine(crashLog); appendLine() } - appendLine(tvReport.text) - appendLine() - appendLine(tvLogs.text) - } val cm = ctx.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - cm.setPrimaryClip(ClipData.newPlainText("MG4Control Diagnostic", fullReport)) + cm.setPrimaryClip(ClipData.newPlainText("MG4Control Diagnostic", buildReport(false))) dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.text = getString(R.string.diag_copied) } - // "Effacer crash" — supprime le fichier et ferme le dialog + // "Télécharger" — écrit le rapport complet dans le dossier Download de la voiture dialog.getButton(AlertDialog.BUTTON_NEUTRAL)?.setOnClickListener { + downloadDiagnostic(ctx, buildReport(true)) + } + // "Effacer crash" (dans le contenu) — supprime le fichier et ferme le dialog + btnClearCrash?.setOnClickListener { CrashLogger.clear(ctx) dialog.dismiss() } @@ -465,6 +488,21 @@ class SettingsFragment : Fragment() { } } + /** Écrit le rapport de diagnostic dans le dossier Download de la voiture (fichier .txt horodaté). */ + private fun downloadDiagnostic(ctx: Context, report: String) { + try { + val dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + if (!dir.exists()) dir.mkdirs() + val ts = java.text.SimpleDateFormat("yyyyMMdd_HHmmss", java.util.Locale.US) + .format(java.util.Date()) + val file = File(dir, "MG4Control_diag_$ts.txt") + file.writeText(report) + Toast.makeText(ctx, getString(R.string.diag_downloaded, file.absolutePath), Toast.LENGTH_LONG).show() + } catch (e: Exception) { + Toast.makeText(ctx, getString(R.string.diag_download_failed, e.message ?: "?"), Toast.LENGTH_LONG).show() + } + } + // ── Dialog À propos ────────────────────────────────────────────────────── private fun showInfosDialog() { diff --git a/app/src/main/res/layout/fragment_audio.xml b/app/src/main/res/layout/fragment_audio.xml index 33edaaae..a4d90e29 100644 --- a/app/src/main/res/layout/fragment_audio.xml +++ b/app/src/main/res/layout/fragment_audio.xml @@ -9,50 +9,7 @@ android:gravity="center" android:padding="20dp"> - - - - - - - - - - - - - - - - + Kopiert! Analysiere… Absturzbericht löschen + Herunterladen + Gespeichert: %1$s + Download fehlgeschlagen: %1$s Nach Updates suchen @@ -271,8 +274,6 @@ Keine gekoppelten Geräte Audio - Loudness - Klangverstärkung Lautstärke bei Türöffnung Senkt die Medienlautstärke beim Öffnen einer Vordertür Zielpegel diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index 615ac2a5..f847f19e 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -168,6 +168,9 @@ Copied! Analysing… Clear crash log + Download + Saved: %1$s + Download failed: %1$s Check for updates @@ -271,8 +274,6 @@ No paired devices Audio - Loudness - Sound intensity boost Door-open volume Lowers media volume when a front door opens Target level diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index fef8c87a..162b3520 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -168,6 +168,9 @@ ¡Copiado! Analizando… Borrar informe de crash + Descargar + Guardado: %1$s + Error de descarga: %1$s Buscar actualizaciones @@ -271,8 +274,6 @@ No hay dispositivos emparejados Audio - Loudness - Refuerzo de intensidad de sonido Volumen al abrir la puerta Reduce el volumen multimedia al abrir una puerta delantera Nivel objetivo diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index a19b9cc0..e4646878 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -168,6 +168,9 @@ Copiato! Analisi in corso… Cancella crash log + Scarica + Salvato: %1$s + Download non riuscito: %1$s Controlla aggiornamenti @@ -271,8 +274,6 @@ Nessun dispositivo associato Audio - Loudness - Potenziamento dell\'intensità sonora Volume all\'apertura della porta Abbassa il volume multimediale all\'apertura di una porta anteriore Livello target diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 5f722c3e..042a572c 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -168,6 +168,9 @@ Copiado! A analisar… Limpar relatório de crash + Baixar + Salvo: %1$s + Falha no download: %1$s Verificar atualizações @@ -271,8 +274,6 @@ Nenhum dispositivo emparelhado Audio - Loudness - Reforço de intensidade sonora Volume ao abrir a porta Baixa o volume de média ao abrir uma porta dianteira Nível alvo diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index de3e35df..4a6340f8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -170,6 +170,9 @@ Copié ! Analyse en cours… Effacer le crash + Télécharger + Enregistré : %1$s + Échec du téléchargement : %1$s Vérifier les mises à jour @@ -273,8 +276,6 @@ Aucun appareil associé Audio - Loudness - Boost d\'intensité sonore Volume à l\'ouverture de porte Baisse le volume média à l\'ouverture d\'une porte avant Niveau cible From c58b08b2f8b6a72027e5f8226f14645efaf636d9 Mon Sep 17 00:00:00 2001 From: SliDeeN Date: Mon, 13 Jul 2026 16:59:07 +0200 Subject: [PATCH 5/5] feature/volume-doors only SWI133/32 --- app/build.gradle.kts | 4 ++-- .../java/com/mg4/control/ui/SettingsFragment.kt | 3 ++- .../main/java/com/mg4/control/update/ApkCleanup.kt | 14 ++++++++++++-- .../com/mg4/control/update/UpdateDialogManager.kt | 2 +- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6de3e1e1..10312989 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -11,8 +11,8 @@ android { applicationId = "com.mg4.control" minSdk = 28 targetSdk = 34 - versionCode = 15 - versionName = "2.6.3" + versionCode = 16 + versionName = "2.6.4" } // Signature avec la clé plateforme de la ROM (requise par sharedUserId=android.uid.system). diff --git a/app/src/main/java/com/mg4/control/ui/SettingsFragment.kt b/app/src/main/java/com/mg4/control/ui/SettingsFragment.kt index ec75d704..fca60192 100644 --- a/app/src/main/java/com/mg4/control/ui/SettingsFragment.kt +++ b/app/src/main/java/com/mg4/control/ui/SettingsFragment.kt @@ -35,6 +35,7 @@ import com.mg4.control.util.QrCode import com.mg4.control.debug.AppLogger import com.mg4.control.debug.CrashLogger import com.mg4.control.hardware.MG4Hardware +import com.mg4.control.update.ApkCleanup import com.mg4.control.update.UpdateChecker import com.mg4.control.update.UpdateDialogManager import java.io.File @@ -248,7 +249,7 @@ class SettingsFragment : Fragment() { Environment.DIRECTORY_DOWNLOADS ) val apkFiles = downloadsDir.listFiles { _, name -> - name.startsWith("MGControl") && name.endsWith(".apk") + ApkCleanup.isAppApk(name) } ?: emptyArray() val count = apkFiles.count { it.delete() } diff --git a/app/src/main/java/com/mg4/control/update/ApkCleanup.kt b/app/src/main/java/com/mg4/control/update/ApkCleanup.kt index 1a994c73..2d2984cb 100644 --- a/app/src/main/java/com/mg4/control/update/ApkCleanup.kt +++ b/app/src/main/java/com/mg4/control/update/ApkCleanup.kt @@ -4,7 +4,7 @@ import android.os.Environment import android.util.Log /** - * Nettoie automatiquement les anciens fichiers MGControl*.apk dans le dossier Téléchargements. + * Nettoie automatiquement les APK de l'app dans le dossier Téléchargements. * Ne conserve que les [MAX_APK] plus récents. */ object ApkCleanup { @@ -12,10 +12,20 @@ object ApkCleanup { private const val TAG = "ApkCleanup" private const val MAX_APK = 5 + /** + * Vrai si [name] est un APK de MG4Control dans les Téléchargements. + * Couvre le naming actuel `MG4Control--.apk` ET l'ancien `MGControl.apk` + * (pour nettoyer aussi les téléchargements déjà présents avant le changement de nom). + */ + fun isAppApk(name: String): Boolean = + (name.startsWith("MG4Control", ignoreCase = true) || + name.startsWith("MGControl", ignoreCase = true)) && + name.endsWith(".apk", ignoreCase = true) + fun cleanIfNeeded() { val downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) val apkFiles = downloadsDir.listFiles { f -> - f.isFile && f.name.startsWith("MGControl") && f.name.endsWith(".apk") + f.isFile && isAppApk(f.name) } ?: return if (apkFiles.size <= MAX_APK) return diff --git a/app/src/main/java/com/mg4/control/update/UpdateDialogManager.kt b/app/src/main/java/com/mg4/control/update/UpdateDialogManager.kt index e7bea922..6a78becf 100644 --- a/app/src/main/java/com/mg4/control/update/UpdateDialogManager.kt +++ b/app/src/main/java/com/mg4/control/update/UpdateDialogManager.kt @@ -168,7 +168,7 @@ object UpdateDialogManager { val fileName = info.apkUrl .substringAfterLast('/') .substringBefore('?') - .ifBlank { "MGControl${info.versionName}.apk" } + .ifBlank { "MG4Control-${info.versionName}.apk" } val request = DownloadManager.Request(Uri.parse(info.apkUrl)).apply { setTitle("MG4Control ${info.versionName}")