Skip to content

Commit aa266a3

Browse files
committed
Add new shortcut : Custom Regen Cycle
1 parent f7ed650 commit aa266a3

16 files changed

Lines changed: 628 additions & 27 deletions

File tree

‎README.md‎

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,16 @@ d'appui**, la **fonction**, puis *Modifier* et *Supprimer*. Réattribuer un bout
122122
le même type d'appui demande confirmation et nomme la fonction qui va être remplacée.
123123

124124
Les réglages associés à une action ne s'affichent que si cette action est réellement attribuée
125-
(niveau de repli du mode 1 Pédale, crans du cycle ADAS).
125+
(niveau de repli du mode 1 Pédale, crans du cycle ADAS). Le **cycle de régénération** va plus loin :
126+
dès que la fonction est attribuée — ou même simplement choisie dans le formulaire avancé — une
127+
sous-entrée apparaît dans le rail de gauche et ouvre sa propre page.
128+
129+
On y compose la séquence parcourue par la touche : toucher un mode l'ajoute **en fin de cycle**,
130+
le toucher à nouveau le retire, et l'ordre des appuis est celui du cycle. Les cinq modes proposés
131+
sont Faible, Moyen, Fort, Adaptatif et 1 Pédale ; deux au minimum, faute de quoi le raccourci
132+
n'aurait plus rien à parcourir après le premier appui. Le réglage est **global** — la même
133+
séquence pour tous les boutons qui déclenchent la fonction — et sans réglage, le comportement
134+
d'origine reste inchangé (Faible → Moyen → Fort → Adaptatif).
126135

127136
### Automatisation
128137
- **Application d'un profil selon la température extérieure** : seuil, sens
@@ -254,7 +263,7 @@ MG4Control/
254263
│ │ ├── model/
255264
│ │ │ ├── DrivingProfile.kt # Modèle de données d'un profil
256265
│ │ │ ├── DriveMode.kt # Enum modes de conduite (ECO/NORMAL/SPORT/SNOW/CUSTOM)
257-
│ │ │ ├── RegenLevel.kt # Enum niveaux de régénération + ordre d'usage du cycle
266+
│ │ │ ├── RegenLevel.kt # Enum niveaux de régénération + ordre du cycle par défaut
258267
│ │ │ └── ProfileBackup.kt # Format de la sauvegarde véhicule
259268
│ │ │
260269
│ │ ├── profile/
@@ -271,6 +280,7 @@ MG4Control/
271280
│ │ │ └── AdvancedShortcuts.kt # Stockage (touche, type d'appui) → action
272281
│ │ │
273282
│ │ ├── shortcut/
283+
│ │ │ ├── RegenCycle.kt # Séquence du cycle de régénération choisie par l'utilisateur
274284
│ │ │ └── ShortcutAction.kt # Catalogue des actions, partagé par les deux systèmes
275285
│ │ │
276286
│ │ ├── bluetooth/
@@ -843,7 +853,16 @@ Each row of the advanced list shows the **button** (name and code), the **press
843853
asks for confirmation and names the action about to be replaced.
844854

845855
Settings attached to an action appear only when that action is actually assigned (One Pedal
846-
fallback level, ADAS cycle notches).
856+
fallback level, ADAS cycle notches). The **regeneration cycle** goes further: as soon as the action
857+
is assigned — or merely picked in the advanced form — a sub-entry appears in the left rail and
858+
opens a page of its own.
859+
860+
There you compose the sequence the key walks through: tapping a mode appends it **at the end of the
861+
cycle**, tapping it again removes it, and the order of your taps is the order of the cycle. The five
862+
modes on offer are Low, Medium, High, Adaptive and One Pedal; two at the very least, otherwise the
863+
shortcut would have nothing left to walk through after the first press. The setting is **global** —
864+
the same sequence for every button triggering the action — and with no setting at all, the original
865+
behaviour stands unchanged (Low → Medium → High → Adaptive).
847866

848867
### Automation
849868
- **Apply a profile from the outside temperature**: threshold, direction (below/above), profile to
@@ -971,7 +990,7 @@ MG4Control/
971990
│ │ ├── model/
972991
│ │ │ ├── DrivingProfile.kt # Profile data model
973992
│ │ │ ├── DriveMode.kt # Drive mode enum (ECO/NORMAL/SPORT/SNOW/CUSTOM)
974-
│ │ │ ├── RegenLevel.kt # Regen level enum + shortcut cycle order
993+
│ │ │ ├── RegenLevel.kt # Regen level enum + default shortcut cycle order
975994
│ │ │ └── ProfileBackup.kt # Vehicle backup format
976995
│ │ │
977996
│ │ ├── profile/
@@ -988,6 +1007,7 @@ MG4Control/
9881007
│ │ │ └── AdvancedShortcuts.kt # Storage for (key, press type) → action
9891008
│ │ │
9901009
│ │ ├── shortcut/
1010+
│ │ │ ├── RegenCycle.kt # User-composed regeneration cycle sequence
9911011
│ │ │ └── ShortcutAction.kt # Action catalogue, shared by both systems
9921012
│ │ │
9931013
│ │ ├── bluetooth/

‎app/build.gradle.kts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ android {
1111
applicationId = "com.mg4.control"
1212
minSdk = 28
1313
targetSdk = 34
14-
versionCode = 18
14+
versionCode = 19
1515
// Suffixe injecte par la CI beta : -Pmg4.versionSuffix=-beta42 produit "2.6.6-beta42".
1616
// L'APK installe annonce alors EXACTEMENT ce que dit le tag de la release, sans quoi
1717
// l'OTA reproposerait la meme mise a jour indefiniment.
18-
versionName = "2.6.6" + (project.findProperty("mg4.versionSuffix") as String? ?: "")
18+
versionName = "2.6.7" + (project.findProperty("mg4.versionSuffix") as String? ?: "")
1919
}
2020

2121
// Signature avec la clé plateforme de la ROM (requise par sharedUserId=android.uid.system).

‎app/src/main/java/com/mg4/control/hardware/MG4Hardware.kt‎

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4226,6 +4226,47 @@ object MG4Hardware {
42264226
* 4. **La touche média Android**, dernier recours : c'est la seule voie sur les firmwares A9,
42274227
* où ce service SAIC n'existe pas, et la seule qui atteigne une session Bluetooth.
42284228
*/
4229+
/** `getCurrentAudioType` de `ICarAudioService` (A9). */
4230+
private const val AUDIO_GET_CURRENT_TYPE = 0x2c
4231+
4232+
/**
4233+
* SONDE — source audio déclarée par le véhicule sur A9, ou `null` si indisponible.
4234+
*
4235+
* Elle ne sert encore À RIEN dans les décisions : elle est là pour être relevée dans les
4236+
* rapports, parce qu'il nous manque une information et une seule.
4237+
*
4238+
* Le problème mesuré sur SWI132 : quand un téléphone est connecté, la session Bluetooth et
4239+
* celle de la radio se déclarent **toutes deux en lecture**. Relevé sur un essai radio :
4240+
* 18 fois la radio retenue, 16 fois le Bluetooth — un tirage au sort. Départager par
4241+
* l'ordre de la liste ne peut pas marcher.
4242+
*
4243+
* `getCurrentAudioType` est la seule source qui sache dire quelle source est réellement
4244+
* audible. Il reste à apprendre son espace de valeurs : d'où cette lecture, journalisée
4245+
* dans les cas ambigus et dans le rapport de diagnostic. Deux rapports — un sur radio, un
4246+
* sur Bluetooth — suffiront à établir la table, et l'aiguillage deviendra déterministe.
4247+
*
4248+
* ⚠️ Absent du launcher SWI131 (interface plus ancienne, 0x2c hors plage) : la transaction
4249+
* y est rejetée sans effet de bord. Vérifié dans le smali avant d'oser l'appel.
4250+
*/
4251+
private fun typeAudioCourant(): Int? {
4252+
if (!isA9Sound()) return null
4253+
val h = sAudioHelper ?: return null
4254+
if (!h.isBinderAlive) return null
4255+
val data = Parcel.obtain()
4256+
val reply = Parcel.obtain()
4257+
return try {
4258+
data.writeInterfaceToken(sAudioDescriptor)
4259+
if (!h.transact(AUDIO_GET_CURRENT_TYPE, data, reply, 0)) return null
4260+
reply.readException()
4261+
reply.readInt()
4262+
} catch (e: Exception) {
4263+
null
4264+
} finally {
4265+
data.recycle()
4266+
reply.recycle()
4267+
}
4268+
}
4269+
42294270
/**
42304271
* Une session est-elle en lecture ? `null` si on ne peut pas les consulter.
42314272
*
@@ -4275,7 +4316,16 @@ object MG4Hardware {
42754316
return false
42764317
}
42774318

4278-
val joue = sessions.firstOrNull { it.playbackState?.state == PlaybackState.STATE_PLAYING }
4319+
// SONDE : plusieurs sessions qui se déclarent en lecture EN MÊME TEMPS, c'est le cas
4320+
// qu'on ne sait pas trancher. On le journalise avec le type audio du véhicule — le
4321+
// choix ci-dessous, lui, est inchangé.
4322+
val enLecture = sessions.filter { it.playbackState?.state == PlaybackState.STATE_PLAYING }
4323+
if (enLecture.size > 1) {
4324+
AppLogger.w(MEDIA_TAG, "AMBIGU : ${enLecture.size} sessions se déclarent en lecture " +
4325+
"(${enLecture.joinToString { it.packageName ?: "?" }}) — type audio véhicule = " +
4326+
"${typeAudioCourant() ?: "non exposé"} — retenue : ${enLecture[0].packageName}")
4327+
}
4328+
val joue = enLecture.firstOrNull()
42794329
val cible = joue
42804330
?: if (cmd == CmdMedia.LECTURE_PAUSE)
42814331
// ⚠️ Pour REPRENDRE, on ne demande pas un état précis mais une CAPACITÉ.
@@ -4587,6 +4637,9 @@ object MG4Hardware {
45874637
val am = ctx.getSystemService(Context.AUDIO_SERVICE) as? AudioManager
45884638
AppLogger.i(MEDIA_TAG, "── DIAG média ────────────────────────────────")
45894639
AppLogger.i(MEDIA_TAG, "lecture en cours (isMusicActive) = ${am?.isMusicActive}")
4640+
// Type audio déclaré par le véhicule (A9). À relever une fois par source pour établir
4641+
// la table — voir [typeAudioCourant].
4642+
AppLogger.i(MEDIA_TAG, "type audio véhicule (A9) = ${typeAudioCourant() ?: "non exposé"}")
45904643

45914644
// Sessions média : c'est ce qui a montré que les touches média ne pouvaient pas aboutir.
45924645
try {

‎app/src/main/java/com/mg4/control/model/RegenLevel.kt‎

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,34 @@ enum class RegenLevel(val value: Int, val label: String) {
2323
val CYCLE_ORDER = listOf(LOW, MEDIUM, HIGH, ADAPTIVE)
2424

2525
/**
26-
* Niveau suivant dans [CYCLE_ORDER], en rebouclant à la fin.
26+
* Modes qu'on accepte de voir figurer dans un cycle composé par l'utilisateur.
2727
*
28-
* Un niveau hors cycle ([OFF] ou [ONE_PEDAL]) fait entrer par le premier cran : sur une
29-
* touche de volant, ne rien faire passerait pour une panne.
28+
* [ONE_PEDAL] en fait partie : il reste hors du cycle PAR DÉFAUT ci-dessus, mais rien
29+
* n'interdit à quelqu'un de le vouloir dans sa séquence — c'est un mode de conduite qui
30+
* s'écrit exactement comme les autres.
31+
*
32+
* [OFF] non : couper la régénération n'est pas un cran de dosage, le launcher d'origine
33+
* ne le propose pas davantage, et le traverser à chaque tour serait subi plus que choisi.
34+
*/
35+
val CYCLE_SELECTABLE = listOf(LOW, MEDIUM, HIGH, ADAPTIVE, ONE_PEDAL)
36+
37+
/**
38+
* Niveau suivant dans le cycle, en rebouclant à la fin.
39+
*
40+
* [order] est la séquence à parcourir — [CYCLE_ORDER] par défaut, ou celle que
41+
* l'utilisateur a composée (voir `RegenCycle`). Une séquence vide retombe sur l'ordre
42+
* d'origine : un cycle sans mode ne pourrait produire aucun niveau à écrire.
43+
*
44+
* Un niveau absent du cycle (parce qu'il n'y figure pas, ou plus) fait entrer par le
45+
* premier cran : sur une touche de volant, ne rien faire passerait pour une panne.
3046
*/
31-
fun nextInCycle(current: RegenLevel): RegenLevel {
32-
val idx = CYCLE_ORDER.indexOf(current)
33-
return if (idx < 0) CYCLE_ORDER.first()
34-
else CYCLE_ORDER[(idx + 1) % CYCLE_ORDER.size]
47+
fun nextInCycle(
48+
current: RegenLevel,
49+
order: List<RegenLevel> = CYCLE_ORDER
50+
): RegenLevel {
51+
val cycle = if (order.isEmpty()) CYCLE_ORDER else order
52+
val idx = cycle.indexOf(current)
53+
return if (idx < 0) cycle.first() else cycle[(idx + 1) % cycle.size]
3554
}
3655
}
3756
}

‎app/src/main/java/com/mg4/control/service/MG4ControlService.kt‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import com.mg4.control.hardware.MG4Hardware.Swi68Mode
3535
import com.mg4.control.model.RegenLevel
3636
import com.mg4.control.profile.ProfileApplier
3737
import com.mg4.control.profile.ProfileManager
38+
import com.mg4.control.shortcut.RegenCycle
3839
import com.mg4.control.shortcut.ShortcutAction
3940
import com.mg4.control.util.FirmwareInfo
4041
import com.mg4.control.util.ThemeHelper
@@ -575,10 +576,16 @@ class MG4ControlService : Service() {
575576
"imposé par le véhicule")
576577
return
577578
}
578-
// L'ordre du cycle vit à côté de l'enum, où se trouve le piège : l'ordre de
579+
// La séquence est celle que l'utilisateur a composée, et à défaut l'ordre
580+
// d'origine. Elle est relue À CHAQUE APPUI : le réglage vit dans un autre écran,
581+
// et le service n'est pas redémarré quand on le modifie.
582+
//
583+
// L'ordre par défaut vit à côté de l'enum, où se trouve le piège : l'ordre de
579584
// déclaration n'est pas l'ordre d'usage. Il y est couvert par des tests.
580-
val suivant = RegenLevel.nextInCycle(actuel)
581-
AppLogger.i(TAG, "SHORTCUT régénération : ${actuel.label} → ${suivant.label}")
585+
val ordre = RegenCycle.order(this)
586+
val suivant = RegenLevel.nextInCycle(actuel, ordre)
587+
AppLogger.i(TAG, "SHORTCUT régénération : ${actuel.label} → ${suivant.label} " +
588+
"(cycle ${ordre.joinToString("/") { it.label }})")
582589
MG4Hardware.setRegenLevel(suivant)
583590
}
584591

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package com.mg4.control.shortcut
2+
3+
import android.content.Context
4+
import com.mg4.control.model.RegenLevel
5+
6+
/**
7+
* Ordre du cycle de régénération, tel que l'utilisateur l'a composé.
8+
*
9+
* Réglage GLOBAL et non par emplacement : la même touche, qu'elle vienne des raccourcis
10+
* classiques ou des avancés, doit parcourir la même séquence. Deux cycles concurrents sur deux
11+
* boutons se contrediraient au premier appui, puisque le cycle repart toujours du niveau
12+
* réellement lu sur le véhicule.
13+
*
14+
* Rien n'est écrit tant que l'utilisateur n'a rien composé : l'absence de clé signifie
15+
* « comportement d'origine », soit [RegenLevel.CYCLE_ORDER]. C'est ce qui garantit qu'une mise à
16+
* jour ne change le cycle de personne.
17+
*/
18+
object RegenCycle {
19+
20+
/** Même fichier que le reste des raccourcis — un seul endroit à sauvegarder. */
21+
private const val PREFS = "mg4_shortcuts"
22+
private const val KEY = "shortcut_regen_cycle_order"
23+
24+
/**
25+
* En dessous de deux modes, il n'y a plus de cycle mais une consigne fixe : le premier appui
26+
* agirait, tous les suivants seraient sans effet, et le raccourci passerait pour cassé.
27+
*/
28+
const val MIN_MODES = 2
29+
30+
/**
31+
* Séquence à parcourir. Toujours exploitable : une valeur illisible, un mode inconnu ou un
32+
* cycle trop court retombent sur l'ordre d'origine plutôt que de rendre le raccourci muet.
33+
*/
34+
fun order(ctx: Context): List<RegenLevel> {
35+
val brut = prefs(ctx).getString(KEY, null) ?: return RegenLevel.CYCLE_ORDER
36+
// On ne passe PAS par RegenLevel.fromValue : elle rend MEDIUM pour toute valeur inconnue,
37+
// ce qui transformerait une préférence corrompue en cycle plausible mais faux.
38+
val lus = brut.split(',').mapNotNull { morceau ->
39+
morceau.trim().toIntOrNull()?.let { v ->
40+
RegenLevel.CYCLE_SELECTABLE.firstOrNull { it.value == v }
41+
}
42+
}.distinct()
43+
return if (lus.size >= MIN_MODES) lus else RegenLevel.CYCLE_ORDER
44+
}
45+
46+
fun save(ctx: Context, ordre: List<RegenLevel>) {
47+
prefs(ctx).edit().putString(KEY, ordre.joinToString(",") { it.value.toString() }).apply()
48+
}
49+
50+
/** Revient au comportement d'origine en RETIRANT la clé, pas en écrivant l'ordre par défaut :
51+
* le réglage suivra ainsi une éventuelle évolution de [RegenLevel.CYCLE_ORDER]. */
52+
fun reset(ctx: Context) {
53+
prefs(ctx).edit().remove(KEY).apply()
54+
}
55+
56+
private fun prefs(ctx: Context) =
57+
ctx.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
58+
}

0 commit comments

Comments
 (0)