Skip to content

Commit faedd26

Browse files
committed
add new advanced shortcut to cycle profils
1 parent eb1323a commit faedd26

21 files changed

Lines changed: 868 additions & 26 deletions

File tree

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1018,6 +1018,13 @@ class MG4ControlService : Service() {
10181018
return
10191019
}
10201020

1021+
// PROFILE_CYCLE : profil suivant du cycle composé dans l'onglet Raccourcis. Annonce,
1022+
// délai contre les appuis rapprochés et application sont dans [ProfileCycleShortcut].
1023+
if (action == ShortcutAction.PROFILE_CYCLE) {
1024+
ProfileCycleShortcut.press(this@MG4ControlService)
1025+
return
1026+
}
1027+
10211028
// Vitres : action directe, aucune bascule — une pression = une course complète.
10221029
// La FERMETURE emprunte le chemin de la fermeture automatique, le seul qui aille au bout
10231030
// sans personne devant l'écran : au volant, l'utilisateur ne regarde pas l'application.
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package com.mg4.control.service
2+
3+
import android.content.Context
4+
import android.os.Handler
5+
import android.os.Looper
6+
import android.widget.Toast
7+
import com.mg4.control.R
8+
import com.mg4.control.debug.AppLogger
9+
import com.mg4.control.hardware.VehicleWriteGate
10+
import com.mg4.control.profile.ActiveProfile
11+
import com.mg4.control.profile.ProfileApplier
12+
import com.mg4.control.profile.ProfileManager
13+
import com.mg4.control.shortcut.ProfileCycle
14+
import com.mg4.control.util.LocaleHelper
15+
16+
/**
17+
* Raccourci « Cycle de profils » : chaque appui vise le profil suivant du cycle composé dans
18+
* l'onglet Raccourcis, l'annonce à l'écran, et l'applique après un court délai sans nouvel appui.
19+
*
20+
* Le délai ([ProfileCycle.APPLY_DELAY_MS]) est ce qui permet de SAUTER un profil : appliquer un
21+
* profil écrit une série de réglages sur la voiture, et deux appuis rapprochés appliqueraient
22+
* sinon le profil intermédiaire au passage. L'annonce dit où l'on en est pendant ce temps.
23+
*
24+
* La règle elle-même — quel profil vient ensuite — est dans [ProfileCycle], sans Android.
25+
*/
26+
object ProfileCycleShortcut {
27+
28+
private const val TAG = "MG4_SVC"
29+
30+
private val main = Handler(Looper.getMainLooper())
31+
32+
// Ces trois champs ne se lisent et ne s'écrivent que sur le thread principal.
33+
34+
/** Profil visé, tant qu'il attend d'être appliqué. */
35+
private var cible: String? = null
36+
private var application: Runnable? = null
37+
private var message: Toast? = null
38+
39+
fun press(context: Context) {
40+
val app = context.applicationContext
41+
main.post { appui(app) }
42+
}
43+
44+
private fun appui(app: Context) {
45+
// Sécurité conduite : si elle refuse, RIEN ne bouge — ni le profil visé, ni le profil
46+
// actif. Laisser faire appliquerait la moitié du profil (le confort passe, la conduite
47+
// non) et ferait avancer le cycle sur un profil qui n'est pas en place. C'est la
48+
// sécurité qui affiche son message habituel.
49+
if (!VehicleWriteGate.allow("cycle de profils")) return
50+
51+
val profils = ProfileManager(app).getAll()
52+
val cycle = ProfileCycle.order(app, profils.map { it.id })
53+
val vise = ProfileCycle.target(cycle, ActiveProfile.id(app), cible)
54+
val profil = profils.firstOrNull { it.id == vise }
55+
if (profil == null) {
56+
AppLogger.i(TAG, "SHORTCUT cycle de profils — aucun profil à appliquer")
57+
annoncer(app, R.string.shortcuts_no_profiles)
58+
return
59+
}
60+
AppLogger.i(TAG, "SHORTCUT cycle de profils → '${profil.name}' " +
61+
"(${cycle.indexOf(profil.id) + 1}/${cycle.size})")
62+
cible = profil.id
63+
annoncer(app, R.string.profile_cycle_target, profil.name)
64+
65+
application?.let(main::removeCallbacks)
66+
application = Runnable { appliquer(app, profil.id) }
67+
.also { main.postDelayed(it, ProfileCycle.APPLY_DELAY_MS) }
68+
}
69+
70+
private fun appliquer(app: Context, id: String) {
71+
cible = null
72+
application = null
73+
// Relu plutôt que gardé depuis l'appui : le profil a pu être modifié entre-temps.
74+
val profil = ProfileManager(app).getById(id) ?: return
75+
ProfileApplier.apply(profil) { ok ->
76+
main.post {
77+
// Un nouvel appui est arrivé pendant l'application : son annonce fait foi, et
78+
// « appliqué » par-dessus dirait le contraire de ce qui va se passer.
79+
if (cible != null) return@post
80+
annoncer(app,
81+
if (ok) R.string.profile_cycle_applied else R.string.profile_cycle_incomplete,
82+
profil.name)
83+
}
84+
}
85+
}
86+
87+
/** Un seul message à la fois : le nouveau REMPLACE l'ancien au lieu d'attendre son tour. */
88+
private fun annoncer(app: Context, texte: Int, vararg args: Any) {
89+
// Langue choisie dans l'application : le contexte du service garde celle du système.
90+
val loc = LocaleHelper.applyLocale(app)
91+
message?.cancel()
92+
message = runCatching {
93+
Toast.makeText(loc, loc.getString(texte, *args), Toast.LENGTH_SHORT).also { it.show() }
94+
}.getOrNull()
95+
}
96+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package com.mg4.control.shortcut
2+
3+
import android.content.Context
4+
5+
/**
6+
* Cycle de profils — les profils que parcourt le raccourci du même nom, dans l'ordre choisi par
7+
* l'utilisateur.
8+
*
9+
* Réglage GLOBAL, comme [RegenCycle] et pour la même raison : deux boutons qui déclenchent le
10+
* raccourci doivent parcourir la même séquence, puisque le cycle repart toujours du profil actif.
11+
*
12+
* Les profils sont désignés par leur IDENTIFIANT : renommer un profil ne le sort pas du cycle.
13+
* Rien n'est écrit tant que l'utilisateur n'a rien composé — l'absence de clé signifie « tous les
14+
* profils, dans l'ordre de leur liste », et un profil créé ensuite y entre de lui-même.
15+
*/
16+
object ProfileCycle {
17+
18+
/** Même fichier que le reste des raccourcis — un seul endroit à sauvegarder. */
19+
private const val PREFS = "mg4_shortcuts"
20+
private const val KEY = "shortcut_profile_cycle_order"
21+
22+
/** Sous deux profils il n'y a plus de cycle : l'écran refuse d'enregistrer. */
23+
const val MIN_PROFILES = 2
24+
25+
/**
26+
* Délai sans nouvel appui avant d'appliquer le profil visé. Appliquer un profil écrit une
27+
* série de réglages sur la voiture : sans ce délai, passer du premier au troisième profil
28+
* appliquerait le deuxième au passage.
29+
*/
30+
const val APPLY_DELAY_MS = 1_000L
31+
32+
/** Longueur d'un nom sur son bouton, points de suspension compris. */
33+
const val LABEL_MAX = 16
34+
35+
// ── La règle, sans Android ───────────────────────────────────────────────
36+
37+
/**
38+
* Cycle à parcourir : le réglage [enregistre] confronté aux profils qui [existants] encore.
39+
*
40+
* - aucun réglage → tous les profils, dans l'ordre de leur liste ;
41+
* - un profil supprimé sort du cycle, les autres gardent leur rang ;
42+
* - s'il n'en reste qu'UN, le cycle est ce profil seul : le compléter appliquerait des
43+
* profils que l'utilisateur avait laissés dehors ;
44+
* - s'il n'en reste AUCUN, le réglage ne désigne plus rien et vaut « aucun réglage ».
45+
*/
46+
fun resolve(enregistre: String?, existants: List<String>): List<String> {
47+
val choisis = enregistre.orEmpty().split(',')
48+
.map { it.trim() }
49+
.filter { it in existants }
50+
.distinct()
51+
return choisis.ifEmpty { existants }
52+
}
53+
54+
/** Forme enregistrée d'un cycle — les identifiants de profil ne contiennent pas de virgule. */
55+
fun encode(ordre: List<String>): String = ordre.joinToString(",")
56+
57+
/**
58+
* Profil qui suit [depuis] dans [cycle], en rebouclant. Un profil absent du cycle — appliqué
59+
* par Bluetooth, par une automatisation, ou aucun profil appliqué — fait entrer par le
60+
* premier : sur une touche de volant, ne rien faire passerait pour une panne.
61+
*
62+
* `null` seulement s'il n'existe aucun profil.
63+
*/
64+
fun next(cycle: List<String>, depuis: String?): String? {
65+
if (cycle.isEmpty()) return null
66+
val rang = cycle.indexOf(depuis)
67+
return if (rang < 0) cycle.first() else cycle[(rang + 1) % cycle.size]
68+
}
69+
70+
/**
71+
* Profil visé par un appui. Tant qu'un profil est [enAttente] d'application (voir
72+
* [APPLY_DELAY_MS]), le profil [actif] n'a pas encore changé : on repart du profil visé,
73+
* sinon deux appuis rapprochés viseraient deux fois le même.
74+
*/
75+
fun target(cycle: List<String>, actif: String?, enAttente: String?): String? =
76+
next(cycle, enAttente ?: actif)
77+
78+
/**
79+
* Nom d'un profil tel qu'il tient sur son bouton. Un nom fait jusqu'à 30 caractères ; le
80+
* bouton n'a que deux lignes et la seconde porte le rang, qui est tout l'objet de l'écran.
81+
*/
82+
fun label(nom: String): String {
83+
val net = nom.trim()
84+
return if (net.length <= LABEL_MAX) net else net.take(LABEL_MAX - 1).trimEnd() + "\u2026"
85+
}
86+
87+
// ── Le réglage enregistré ────────────────────────────────────────────────
88+
89+
/** Cycle en vigueur, relu à chaque appui : le réglage vit dans un autre écran. */
90+
fun order(ctx: Context, existants: List<String>): List<String> =
91+
resolve(prefs(ctx).getString(KEY, null), existants)
92+
93+
fun save(ctx: Context, ordre: List<String>) {
94+
prefs(ctx).edit().putString(KEY, encode(ordre)).apply()
95+
}
96+
97+
/** Revient à « tous les profils » en RETIRANT la clé, comme [RegenCycle.reset]. */
98+
fun reset(ctx: Context) {
99+
prefs(ctx).edit().remove(KEY).apply()
100+
}
101+
102+
private fun prefs(ctx: Context) =
103+
ctx.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
104+
}

‎app/src/main/java/com/mg4/control/shortcut/ShortcutAction.kt‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,11 @@ enum class ShortcutAction(val id: Int) {
6767

6868
// Pop-up HVAC (issues #120 et #125) : réglage rapide de la clim au joystick droit. Comme
6969
// PROFILE_PICKER, l'action n'écrit rien elle-même — elle ouvre la fenêtre, ou la referme.
70-
HVAC_POPUP(41);
70+
HVAC_POPUP(41),
71+
72+
// Cycle de profils : applique le profil suivant d'une séquence composée par l'utilisateur
73+
// (voir ProfileCycle). Le profil visé dépend du profil actif, pas d'un état mémorisé ici.
74+
PROFILE_CYCLE(42);
7175

7276
companion object {
7377
fun fromId(id: Int) = entries.firstOrNull { it.id == id } ?: NONE

0 commit comments

Comments
 (0)