Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@
android:theme="@style/Theme.MG4Control"
android:supportsRtl="true">

<!-- API Car AOSP (CarDoorLockManager pour la détection d'ouverture de porte) -->
<uses-library android:name="android.car" android:required="false" />

<activity
android:name=".MainActivity"
android:exported="true"
Expand Down
394 changes: 368 additions & 26 deletions app/src/main/java/com/mg4/control/hardware/MG4Hardware.kt

Large diffs are not rendered by default.

72 changes: 62 additions & 10 deletions app/src/main/java/com/mg4/control/ui/AudioFragment.kt
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package com.mg4.control.ui

import android.content.Context
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
import com.google.android.material.slider.Slider
import com.mg4.control.R
import com.mg4.control.hardware.MG4Hardware
import kotlinx.coroutines.Dispatchers
Expand All @@ -22,22 +25,71 @@
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

val switch = view.findViewById<Switch>(R.id.switch_loudness)
// ── Volume à l'ouverture d'une porte (SWI132/133) ───────────────────
setupDoorVolume(view)
}

// 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)
}
}
private fun setupDoorVolume(view: View) {
val card = view.findViewById<View>(R.id.door_volume_card)
if (!MG4Hardware.hasDoorVolumeFeature()) {
card.visibility = View.GONE

Check failure

Code scanning / mobsfscan

Hidden elements in view can be used to hide data from user. But this data can be leaked. Error

Hidden elements in view can be used to hide data from user. But this data can be leaked.
Comment thread
SliDeeN marked this conversation as resolved.
Dismissed
return
}

val prefs = requireContext().getSharedPreferences("mg4_settings", Context.MODE_PRIVATE)
val toggle = view.findViewById<Switch>(R.id.switch_door_volume)
val slider = view.findViewById<Slider>(R.id.slider_door_volume)
val restore = view.findViewById<Switch>(R.id.switch_door_restore)
val cbLeft = view.findViewById<CheckBox>(R.id.cb_door_left)
val cbRight = view.findViewById<CheckBox>(R.id.cb_door_right)

val enabled = prefs.getBoolean("door_volume_enabled", false)
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)

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() }
}

// Borne le slider sur le max réel de la voiture (sinon valeur XML par défaut).
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
val state = MG4Hardware.getLoudnessState()
val max = MG4Hardware.getMediaVolumeMax()
withContext(Dispatchers.Main) {
if (isAdded && state >= 0) switch.isChecked = (state == 1)
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()
setSubEnabled(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()
}
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()
}
}
}
30 changes: 29 additions & 1 deletion app/src/main/java/com/mg4/control/ui/ConsoleFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -45,13 +60,15 @@ class ConsoleFragment : Fragment() {
override fun onResume() {
super.onResume()
AppLogger.addListener(logListener)
handler.post(refresh)
renderStatus()
renderLog()
}

override fun onPause() {
super.onPause()
AppLogger.removeListener(logListener)
handler.removeCallbacks(refresh)
}

// ---- Status banner ----
Expand All @@ -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}] "
Expand All @@ -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) }
}
}
59 changes: 49 additions & 10 deletions app/src/main/java/com/mg4/control/ui/SettingsFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -34,8 +35,10 @@ 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
import com.mg4.control.util.FirmwareHelper
import com.mg4.control.util.LocaleHelper
import kotlinx.coroutines.CoroutineScope
Expand Down Expand Up @@ -246,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() }
Expand Down Expand Up @@ -351,11 +354,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(
Expand Down Expand Up @@ -387,6 +395,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
Expand Down Expand Up @@ -429,26 +443,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()
}
Expand All @@ -465,6 +489,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() {
Expand Down
14 changes: 12 additions & 2 deletions app/src/main/java/com/mg4/control/update/ApkCleanup.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,28 @@ 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 {

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-<flavor>-<version>.apk` ET l'ancien `MGControl<version>.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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
Expand Down
Loading
Loading