|
| 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 | +} |
0 commit comments