Skip to content

Commit eb1323a

Browse files
committed
Fix the theme for all popup
1 parent e175c9e commit eb1323a

9 files changed

Lines changed: 131 additions & 11 deletions

File tree

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import com.mg4.control.model.HvacPopup
2828
import com.mg4.control.model.SeatHeat
2929
import com.mg4.control.util.FirmwareInfo
3030
import com.mg4.control.util.LocaleHelper
31+
import com.mg4.control.util.ThemeHelper
3132
import java.util.Locale
3233
import java.util.concurrent.Executors
3334
import kotlin.math.roundToInt
@@ -173,8 +174,10 @@ object HvacPopupOverlay {
173174
// pictos et texte restent nets, et la fenêtre prend la même part de l'écran partout.
174175
val mesures = localized.resources.displayMetrics
175176
val echelle = HvacPopup.echelle(mesures.widthPixels, mesures.heightPixels, mesures.density)
176-
val agrandi = localized.createConfigurationContext(
177-
Configuration(localized.resources.configuration).apply {
177+
// Même contexte pour le thème : clair ou sombre comme l'application, pas comme le système.
178+
val auTheme = ThemeHelper.withAppTheme(localized)
179+
val agrandi = auTheme.createConfigurationContext(
180+
Configuration(auTheme.resources.configuration).apply {
178181
densityDpi = (mesures.densityDpi * echelle).roundToInt()
179182
})
180183
val themed = ContextThemeWrapper(agrandi, R.style.Theme_MG4Control)

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1158,7 +1158,9 @@ class MG4ControlService : Service() {
11581158
val inPark = MG4Hardware.isVehicleInPark()
11591159
Handler(Looper.getMainLooper()).post {
11601160
if (inPark == true) {
1161-
val themed = ContextThemeWrapper(LocaleHelper.applyLocale(this@MG4ControlService), R.style.Theme_MG4Control)
1161+
val themed = ContextThemeWrapper(
1162+
ThemeHelper.withAppTheme(LocaleHelper.applyLocale(this@MG4ControlService)),
1163+
R.style.Theme_MG4Control)
11621164
val dialog = AlertDialog.Builder(themed)
11631165
.setTitle(R.string.vehicle_power_dialog_title)
11641166
.setMessage(R.string.vehicle_power_dialog_msg)

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import com.mg4.control.debug.AppLogger
2424
*
2525
* Vit et meurt avec la vue du popup. Thread principal seulement.
2626
*
27+
* @param context contexte THÉMÉ du popup : l'anneau prend la couleur d'accent de son thème.
28+
*
2729
* @param ligneCurseur cellule d'un curseur : gauche/droite y appellent [reglerCurseur] (-1 / +1)
2830
* au lieu de déplacer le focus, et la validation n'y fait rien.
2931
* @param defilement conteneur à faire défiler quand la cellule surlignée en dépasse.

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import com.mg4.control.debug.AppLogger
1818
import com.mg4.control.hardware.VehicleWriteGate
1919
import com.mg4.control.model.DrivingProfile
2020
import com.mg4.control.util.LocaleHelper
21+
import com.mg4.control.util.ThemeHelper
2122

2223
/**
2324
* Popup OUI/NON demandant s'il faut appliquer [profile] parce qu'un seuil est franchi :
@@ -107,7 +108,7 @@ object ProfileConfirmOverlay {
107108
dismiss(context)
108109

109110
val localized = LocaleHelper.applyLocale(context)
110-
val themed = ContextThemeWrapper(localized, R.style.Theme_MG4Control)
111+
val themed = ContextThemeWrapper(ThemeHelper.withAppTheme(localized), R.style.Theme_MG4Control)
111112
val view = LayoutInflater.from(themed).inflate(R.layout.overlay_profile_confirm, null)
112113

113114
view.findViewById<TextView>(R.id.confirm_message).text = message(localized)
@@ -160,7 +161,7 @@ object ProfileConfirmOverlay {
160161

161162
// Joystick : chaque déplacement du focus relance le délai, comme dans le popup profils.
162163
navigation = OverlayNavigation(
163-
context = context,
164+
context = themed,
164165
grille = listOf(listOf(btnYes, btnNo)),
165166
depart = JoystickFocus.Position(0, 0),
166167
surDeplacement = {

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

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import com.mg4.control.model.DrivingProfile
2828
import com.mg4.control.profile.ProfileApplier
2929
import com.mg4.control.profile.ProfileManager
3030
import com.mg4.control.util.LocaleHelper
31+
import com.mg4.control.util.ThemeHelper
3132
import kotlinx.coroutines.CoroutineScope
3233
import kotlinx.coroutines.Dispatchers
3334
import kotlinx.coroutines.launch
@@ -128,15 +129,17 @@ object ProfilePickerOverlay {
128129

129130
// Le contexte du Service n'a pas de thème Material → on l'enveloppe
130131
// avec le thème de l'app pour que MaterialButton puisse s'instancier.
131-
val themedContext = ContextThemeWrapper(localizedContext, R.style.Theme_MG4Control)
132+
val themedContext = ContextThemeWrapper(ThemeHelper.withAppTheme(localizedContext), R.style.Theme_MG4Control)
132133

133134
// Inflate la vue depuis le layout XML (utilise le contexte thémé)
134135
val view = LayoutInflater.from(themedContext).inflate(R.layout.overlay_profile_picker, null)
135136

136137
// ── Grille 2 colonnes de profils ─────────────────────────────────
137138
val container = view.findViewById<LinearLayout>(R.id.overlay_profiles_container)
138-
val accentColor = context.getColor(R.color.dash_accent)
139-
val accentDimColor = context.getColor(R.color.dash_accent_dim)
139+
// Sur le contexte THÉMÉ : pris sur celui du service, ces deux bleus resteraient ceux du
140+
// thème sombre dans une fenêtre claire.
141+
val accentColor = themedContext.getColor(R.color.dash_accent)
142+
val accentDimColor = themedContext.getColor(R.color.dash_accent_dim)
140143
val dm = context.resources.displayMetrics
141144

142145
fun dp(value: Float) = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value, dm).toInt()
@@ -371,7 +374,7 @@ object ProfilePickerOverlay {
371374
// c'est aussi celui qu'applique le délai d'un conflit Bluetooth.
372375
val premierProfil = grille.indexOfFirst { it.firstOrNull() === lignesProfils.firstOrNull()?.firstOrNull() }
373376
navigation = OverlayNavigation(
374-
context = context,
377+
context = themedContext,
375378
grille = grille,
376379
depart = JoystickFocus.Position(premierProfil.coerceAtLeast(0), 0),
377380
surDeplacement = { resetTimers() },
@@ -391,7 +394,8 @@ object ProfilePickerOverlay {
391394
val inPark = MG4Hardware.isVehicleInPark()
392395
handler.post {
393396
if (inPark == true) {
394-
val themed = ContextThemeWrapper(LocaleHelper.applyLocale(context), R.style.Theme_MG4Control)
397+
val themed = ContextThemeWrapper(
398+
ThemeHelper.withAppTheme(LocaleHelper.applyLocale(context)), R.style.Theme_MG4Control)
395399
val dialog = AlertDialog.Builder(themed)
396400
.setTitle(R.string.vehicle_power_dialog_title)
397401
.setMessage(R.string.vehicle_power_dialog_msg)

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import com.mg4.control.debug.AppLogger
1616
import com.mg4.control.hardware.VehicleWriteGate
1717
import com.mg4.control.update.UpdateInfo
1818
import com.mg4.control.util.LocaleHelper
19+
import com.mg4.control.util.ThemeHelper
1920

2021
/**
2122
* Popup « une mise à jour est disponible », par-dessus l'infodivertissement.
@@ -69,7 +70,7 @@ object UpdateOverlay {
6970
dismiss(context)
7071

7172
val localized = LocaleHelper.applyLocale(context)
72-
val themed = ContextThemeWrapper(localized, R.style.Theme_MG4Control)
73+
val themed = ContextThemeWrapper(ThemeHelper.withAppTheme(localized), R.style.Theme_MG4Control)
7374
val view = LayoutInflater.from(themed).inflate(R.layout.overlay_update, null)
7475

7576
view.findViewById<TextView>(R.id.update_versions).text =
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.mg4.control.util
2+
3+
import android.content.res.Configuration
4+
import androidx.appcompat.app.AppCompatDelegate
5+
6+
/**
7+
* Mode jour/nuit à donner à une fenêtre affichée par le SERVICE, pour qu'elle suive le thème de
8+
* l'application — la partie sans contexte, donc testable.
9+
*
10+
* Le thème choisi dans l'application passe par `AppCompatDelegate.setDefaultNightMode`, qui ne
11+
* touche que ses ACTIVITÉS. Une fenêtre du service gardait le mode du système, et sur SWI133
12+
* celui-ci reste figé sur « nuit » même quand le launcher passe en clair (voir [ThemeHelper]) :
13+
* les popups étaient donc toujours sombres.
14+
*/
15+
object PopupUiMode {
16+
17+
/**
18+
* [uiMode] du système, avec ses deux bits jour/nuit remplacés par le choix de l'application
19+
* ([nightMode], une constante `AppCompatDelegate.MODE_NIGHT_*`). Tout le reste — le type
20+
* d'appareil — est conservé. Quand l'application elle-même suit le système, rien ne change.
21+
*/
22+
fun apply(nightMode: Int, uiMode: Int): Int {
23+
val nuit = when (nightMode) {
24+
AppCompatDelegate.MODE_NIGHT_YES -> Configuration.UI_MODE_NIGHT_YES
25+
AppCompatDelegate.MODE_NIGHT_NO -> Configuration.UI_MODE_NIGHT_NO
26+
else -> return uiMode
27+
}
28+
return (uiMode and Configuration.UI_MODE_NIGHT_MASK.inv()) or nuit
29+
}
30+
}

‎app/src/main/java/com/mg4/control/util/ThemeHelper.kt‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.mg4.control.util
22

33
import android.app.UiModeManager
44
import android.content.Context
5+
import android.content.res.Configuration
56
import android.provider.Settings
67
import androidx.appcompat.app.AppCompatDelegate
78

@@ -112,6 +113,26 @@ object ThemeHelper {
112113
}
113114
}
114115

116+
// ── Fenêtres affichées par le service ────────────────────────────────────
117+
118+
/**
119+
* [context] avec le mode clair/sombre de l'APPLICATION, pour une fenêtre affichée par le
120+
* service : sélecteur de profils, confirmations, popup de mise à jour, pop-up HVAC.
121+
*
122+
* Sans ça ces fenêtres prennent le mode jour/nuit du système, qui n'est pas celui de
123+
* l'application et reste figé sur « nuit » sur SWI133 ([PopupUiMode]). Le mode est résolu à
124+
* CHAQUE ouverture : en « Auto », la fenêtre suit donc le launcher même si l'application n'est
125+
* pas revenue au premier plan depuis son changement de thème.
126+
*
127+
* À poser avant le `ContextThemeWrapper`, et à utiliser aussi pour toute couleur lue par le
128+
* code — une couleur prise sur le contexte du service resterait celle du thème sombre.
129+
*/
130+
fun withAppTheme(context: Context): Context {
131+
val config = Configuration(context.resources.configuration)
132+
config.uiMode = PopupUiMode.apply(resolveNightMode(context), config.uiMode)
133+
return context.createConfigurationContext(config)
134+
}
135+
115136
// ── Notification de changement (SWI69/131/132) ───────────────────────────
116137

117138
/** Appelé depuis le service (déjà sur le main thread) pour déclencher recreate(). */
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package com.mg4.control.util
2+
3+
import android.content.res.Configuration
4+
import androidx.appcompat.app.AppCompatDelegate
5+
import org.junit.Assert.assertEquals
6+
import org.junit.Test
7+
8+
/**
9+
* Thème des fenêtres affichées par le SERVICE (sélecteur de profils, confirmations, mise à jour,
10+
* pop-up HVAC).
11+
*
12+
* Le thème choisi dans l'application n'atteint que ses écrans ; ces fenêtres prenaient donc le
13+
* réglage jour/nuit du système — figé sur « nuit » sur SWI133 (0x23 mesuré, que le launcher soit
14+
* clair ou sombre). Elles restaient sombres quel que soit le thème de l'application.
15+
*/
16+
class PopupUiModeTest {
17+
18+
// Valeurs relevées sur la voiture : type « voiture » (0x03) + nuit (0x20) ou jour (0x10).
19+
private val nuit = Configuration.UI_MODE_TYPE_CAR or Configuration.UI_MODE_NIGHT_YES
20+
private val jour = Configuration.UI_MODE_TYPE_CAR or Configuration.UI_MODE_NIGHT_NO
21+
22+
@Test
23+
fun `le theme clair de l application s impose a un systeme fige en nuit`() {
24+
assertEquals(jour, PopupUiMode.apply(AppCompatDelegate.MODE_NIGHT_NO, nuit))
25+
}
26+
27+
@Test
28+
fun `le theme sombre de l application s impose a un systeme en jour`() {
29+
assertEquals(nuit, PopupUiMode.apply(AppCompatDelegate.MODE_NIGHT_YES, jour))
30+
}
31+
32+
@Test
33+
fun `le reste du mode d interface n est pas touche`() {
34+
// Seuls les deux bits jour/nuit changent : le type d'appareil doit survivre.
35+
val tele = Configuration.UI_MODE_TYPE_TELEVISION or Configuration.UI_MODE_NIGHT_YES
36+
assertEquals(
37+
Configuration.UI_MODE_TYPE_TELEVISION or Configuration.UI_MODE_NIGHT_NO,
38+
PopupUiMode.apply(AppCompatDelegate.MODE_NIGHT_NO, tele))
39+
}
40+
41+
@Test
42+
fun `un systeme sans reglage jour nuit recoit celui de l application`() {
43+
val indefini = Configuration.UI_MODE_TYPE_CAR or Configuration.UI_MODE_NIGHT_UNDEFINED
44+
assertEquals(jour, PopupUiMode.apply(AppCompatDelegate.MODE_NIGHT_NO, indefini))
45+
assertEquals(nuit, PopupUiMode.apply(AppCompatDelegate.MODE_NIGHT_YES, indefini))
46+
}
47+
48+
@Test
49+
fun `quand l application suit le systeme rien ne change`() {
50+
// Thème « Auto » sans valeur exploitable : l'application laisse faire le système, la
51+
// fenêtre aussi.
52+
assertEquals(nuit, PopupUiMode.apply(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM, nuit))
53+
assertEquals(jour, PopupUiMode.apply(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM, jour))
54+
assertEquals(nuit, PopupUiMode.apply(AppCompatDelegate.MODE_NIGHT_UNSPECIFIED, nuit))
55+
}
56+
}

0 commit comments

Comments
 (0)