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
123 changes: 23 additions & 100 deletions app/src/main/java/com/mg4/control/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,18 @@ import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.graphics.Paint
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import com.google.android.material.button.MaterialButton
import com.mg4.control.debug.AppLogger
import com.mg4.control.hardware.MG4Hardware
import com.mg4.control.profile.ProfileManager
import com.mg4.control.service.MG4ControlService
Expand All @@ -36,6 +37,25 @@ class MainActivity : AppCompatActivity() {
super.attachBaseContext(LocaleHelper.applyLocale(newBase))
}

/**
* [THEME-AUTO] Re-resout le theme a chaque retour au premier plan.
*
* Indispensable sur old-SDK : le launcher change bien le night mode (UiModeManager le reflete)
* mais le systeme ne met PAS a jour Configuration.uiMode, donc AUCUN changement de
* configuration n'est delivre et l'activite n'est jamais recreee toute seule. Le retour au
* premier plan est le bon moment : on ne peut pas changer le theme du launcher sans quitter
* MG4Control.
*/
override fun onResume() {
super.onResume()
val target = ThemeHelper.resolveNightMode(this)
if (target != AppCompatDelegate.getDefaultNightMode()) {
AppLogger.i("MG4_THEME", "onResume : theme change -> application du mode $target")
// Recree les activites vivantes : pas de recreate() manuel a ajouter.
AppCompatDelegate.setDefaultNightMode(target)
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

Expand All @@ -44,6 +64,7 @@ class MainActivity : AppCompatActivity() {
FirmwareInfo.initWithContext(this)

// [THEME-AUTO] Recrée l'activité quand le launcher MG change de thème en mode "auto"
// (voie A9 : broadcast com.saicmotor.changeSkin reçu par le service)
ThemeHelper.onThemeChanged = { recreate() }

// Premier lancement : choix de la langue avant tout
Expand All @@ -61,10 +82,9 @@ class MainActivity : AppCompatActivity() {
.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
navController = navHostFragment.navController

setupFirmwareChips()
setupNavButtons()
setupDiagnosticUnlock()
checkUnknownFirmware() // après setupFirmwareChips pour que les chips soient prêtes
checkUnknownFirmware()
navigateToDefaultScreen(savedInstanceState)
checkForUpdates()
checkProfileRestore()
Expand Down Expand Up @@ -159,103 +179,6 @@ class MainActivity : AppCompatActivity() {
)
}

// ── Indicateur firmware (chips SWI133 / SWI68 / SWI69 / SWI131 / SWI165) ──

private fun setupFirmwareChips() {
val chip133 = findViewById<TextView>(R.id.chip_swi133)
val chip132 = findViewById<TextView>(R.id.chip_swi132)
val chip68 = findViewById<TextView>(R.id.chip_swi68)
val chip69 = findViewById<TextView>(R.id.chip_swi69)
val chip131 = findViewById<TextView>(R.id.chip_swi131)
val chip165 = findViewById<TextView>(R.id.chip_swi165)
val gen = FirmwareInfo.getGeneration()
val forced = FirmwareInfo.isForced(this)

fun styleChipActive(tv: TextView) {
tv.setBackgroundResource(R.drawable.bg_chip_active)
tv.setTextColor(getColor(R.color.dash_accent))
tv.alpha = 1f
tv.paintFlags = tv.paintFlags and Paint.STRIKE_THRU_TEXT_FLAG.inv()
}

fun styleChipInactive(tv: TextView) {
tv.setBackgroundResource(R.drawable.bg_chip_inactive)
tv.setTextColor(getColor(R.color.dash_text_lo))
tv.alpha = 0.4f
tv.paintFlags = tv.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG
}

fun styleChipSelectable(tv: TextView) {
// Firmware inconnu sans choix forcé : chip cliquable, surlignée en rouge
tv.setBackgroundResource(R.drawable.bg_chip_inactive)
tv.setTextColor(getColor(R.color.dash_danger))
tv.alpha = 0.75f
tv.paintFlags = tv.paintFlags and Paint.STRIKE_THRU_TEXT_FLAG.inv()
}

val isNaturalUnknown = gen == FirmwareInfo.Gen.UNKNOWN && !forced
val allChips = listOf(chip133, chip132, chip68, chip69, chip131, chip165)

when {
isNaturalUnknown -> {
// Les six chips en mode "à choisir" (rouge dim, aucune barrée)
allChips.forEach { styleChipSelectable(it) }
}
gen == FirmwareInfo.Gen.SWI165 -> {
styleChipActive(chip165)
listOf(chip133, chip132, chip68, chip69, chip131).forEach { styleChipInactive(it) }
}
gen == FirmwareInfo.Gen.SWI131 -> {
styleChipActive(chip131)
listOf(chip133, chip132, chip68, chip69, chip165).forEach { styleChipInactive(it) }
}
gen == FirmwareInfo.Gen.SWI69 -> {
styleChipActive(chip69)
listOf(chip133, chip132, chip68, chip131, chip165).forEach { styleChipInactive(it) }
}
gen == FirmwareInfo.Gen.SWI68 -> {
styleChipActive(chip68)
listOf(chip133, chip132, chip69, chip131, chip165).forEach { styleChipInactive(it) }
}
gen == FirmwareInfo.Gen.SWI132 -> {
styleChipActive(chip132)
listOf(chip133, chip68, chip69, chip131, chip165).forEach { styleChipInactive(it) }
}
else -> { // SWI133 ou forcé SWI133
styleChipActive(chip133)
listOf(chip132, chip68, chip69, chip131, chip165).forEach { styleChipInactive(it) }
}
}

// Chips cliquables si firmware inconnu (naturel ou forcé) pour changer de mode
if (gen == FirmwareInfo.Gen.UNKNOWN || forced) {
chip133.setOnClickListener {
FirmwareInfo.forceGeneration(this, FirmwareInfo.Gen.SWI133)
recreate()
}
chip132.setOnClickListener {
FirmwareInfo.forceGeneration(this, FirmwareInfo.Gen.SWI132)
recreate()
}
chip68.setOnClickListener {
FirmwareInfo.forceGeneration(this, FirmwareInfo.Gen.SWI68)
recreate()
}
chip69.setOnClickListener {
FirmwareInfo.forceGeneration(this, FirmwareInfo.Gen.SWI69)
recreate()
}
chip131.setOnClickListener {
FirmwareInfo.forceGeneration(this, FirmwareInfo.Gen.SWI131)
recreate()
}
chip165.setOnClickListener {
FirmwareInfo.forceGeneration(this, FirmwareInfo.Gen.SWI165)
recreate()
}
}
}

// ── Dialog firmware non reconnu ───────────────────────────────────────────

private fun checkUnknownFirmware() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.mg4.control.automation

/** Décision pure de l'automatisation climatisation (testable sans Android). */
object ClimateAutomationDecision {

/** Règle retenue, ou NONE si rien ne s'applique. */
enum class Outcome { NONE, HOT, COLD }

/**
* Choisit la règle à appliquer d'après la température extérieure.
*
* Conditions inclusives : chaud si `temp >= seuilChaud`, froid si `temp <= seuilFroid`.
* Si les deux se déclenchent (seuils qui se chevauchent — configuration incohérente), on
* retient **CHAUD** de façon déterministe plutôt que de dépendre d'un ordre implicite.
*/
fun evaluate(config: ClimateAutomationSettings.Config, temp: Float?): Outcome = when {
!config.enabled -> Outcome.NONE
temp == null || temp.isNaN() -> Outcome.NONE
config.hot.active && temp >= config.hot.threshold.toFloat() -> Outcome.HOT
config.cold.active && temp <= config.cold.threshold.toFloat() -> Outcome.COLD
else -> Outcome.NONE
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package com.mg4.control.automation

import android.content.Context

/**
* Réglages de l'automatisation « Déclenchement A/C via la température ».
*
* Deux règles INDÉPENDANTES : une quand il fait chaud (temp ≥ seuil), une quand il fait froid
* (temp ≤ seuil). Chacune porte ses propres réglages clim — les mêmes valeurs n'auraient aucun
* sens à 35 °C et à −5 °C.
*
* Volontairement séparée d'[AutomationSettings] : ce n'est pas une application de profil, elle
* n'est donc PAS soumise au toggle « application auto du profil » et a son propre interrupteur.
*/
object ClimateAutomationSettings {

const val PREFS = "mg4_settings"

const val KEY_ENABLED = "ac_auto_enabled"

// Une règle = un préfixe ; les clés sont dérivées pour éviter douze constantes quasi jumelles.
private const val HOT = "ac_auto_hot_"
private const val COLD = "ac_auto_cold_"

const val DEFAULT_HOT_THRESHOLD = 28
const val DEFAULT_COLD_THRESHOLD = 5
const val DEFAULT_HOT_TARGET = 20
const val DEFAULT_COLD_TARGET = 24
const val DEFAULT_FAN = 4

/** Bornes de saisie — larges à dessein, les vraies limites du véhicule sont lues au moment
* d'appliquer (getClimateState) et la consigne y est clampée. */
const val MIN_TEMP = -20
const val MAX_TEMP = 60
const val MIN_TARGET = 15
const val MAX_TARGET = 33
const val MIN_FAN = 1
const val MAX_FAN = 10

/** Une règle : sa condition de déclenchement et les réglages clim à appliquer. */
data class Rule(
val active: Boolean,
val threshold: Int,
val targetTemp: Int,
val fanLevel: Int,
val defrostFront: Boolean,
val defrostRear: Boolean
)

data class Config(
val enabled: Boolean,
val hot: Rule,
val cold: Rule
)

fun read(context: Context): Config {
val p = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
fun rule(prefix: String, defThreshold: Int, defTarget: Int) = Rule(
active = p.getBoolean(prefix + "on", false),
threshold = p.getInt(prefix + "threshold", defThreshold),
targetTemp = p.getInt(prefix + "target", defTarget),
fanLevel = p.getInt(prefix + "fan", DEFAULT_FAN),
defrostFront = p.getBoolean(prefix + "def_front", false),
defrostRear = p.getBoolean(prefix + "def_rear", false)
)
return Config(
enabled = p.getBoolean(KEY_ENABLED, false),
hot = rule(HOT, DEFAULT_HOT_THRESHOLD, DEFAULT_HOT_TARGET),
cold = rule(COLD, DEFAULT_COLD_THRESHOLD, DEFAULT_COLD_TARGET)
)
}

// ── Écriture (utilisée par l'UI) ─────────────────────────────────────────
fun keyOn(hot: Boolean) = (if (hot) HOT else COLD) + "on"
fun keyThreshold(hot: Boolean) = (if (hot) HOT else COLD) + "threshold"
fun keyTarget(hot: Boolean) = (if (hot) HOT else COLD) + "target"
fun keyFan(hot: Boolean) = (if (hot) HOT else COLD) + "fan"
fun keyDefFront(hot: Boolean) = (if (hot) HOT else COLD) + "def_front"
fun keyDefRear(hot: Boolean) = (if (hot) HOT else COLD) + "def_rear"

fun clampThreshold(raw: Int?, hot: Boolean): Int =
(raw ?: if (hot) DEFAULT_HOT_THRESHOLD else DEFAULT_COLD_THRESHOLD)
.coerceIn(MIN_TEMP, MAX_TEMP)

fun clampTarget(raw: Int?, hot: Boolean): Int =
(raw ?: if (hot) DEFAULT_HOT_TARGET else DEFAULT_COLD_TARGET)
.coerceIn(MIN_TARGET, MAX_TARGET)

fun clampFan(raw: Int?): Int = (raw ?: DEFAULT_FAN).coerceIn(MIN_FAN, MAX_FAN)
}
Loading
Loading