11package com.mg4.control.hardware
22
33import android.content.Context
4+ import android.media.AudioManager
5+ import android.media.ToneGenerator
46import android.os.Handler
57import android.os.Looper
68import android.os.SystemClock
@@ -35,9 +37,21 @@ object WindowAutoClose {
3537 private const val KEY_TIME_MIN = " win_autoclose_time_min"
3638 private const val KEY_BOTH = " win_autoclose_both"
3739 private const val KEY_DELAY_S = " win_autoclose_delay_s"
40+ private const val KEY_BEEP = " win_autoclose_beep"
41+ private const val KEY_BEEP_VOLUME = " win_autoclose_beep_volume"
3842
3943 private const val TICK_MS = 1_000L
4044
45+ /* * Bip d'avertissement, repris de winclose : un par seconde pendant le délai. */
46+ private const val BEEP_MS = 150
47+ /* * Volume du [ToneGenerator], en pour cent. Le minimum n'est pas 0 : couper, c'est décocher. */
48+ const val BEEP_VOLUME_MIN = 10
49+ const val BEEP_VOLUME_MAX = 100
50+ const val BEEP_VOLUME_STEP = 5
51+ const val BEEP_VOLUME_DEFAULT = 80
52+ /* * Bip d'essai hors surveillance : on rend la piste audio peu après. */
53+ private const val PREVIEW_RELEASE_MS = 2_000L
54+
4155 enum class Result { PENDING , DONE , PARTIAL , CANCELLED }
4256
4357 data class Status (
@@ -58,6 +72,8 @@ object WindowAutoClose {
5872 val timeMin : Int ,
5973 val requireBoth : Boolean ,
6074 val delayS : Int ,
75+ val beep : Boolean ,
76+ val beepVolume : Int ,
6177 ) {
6278 fun toArming () = Arming (
6379 speedKmh = if (speedOn) speedKmh.toFloat() else null ,
@@ -71,6 +87,12 @@ object WindowAutoClose {
7187 private var running = false
7288 private var lastResult: Result ? = null
7389 private var lastResultAt = 0L
90+ /* * Recopié depuis les réglages : le tic ne doit pas relire les préférences chaque seconde. */
91+ private var beepOn = false
92+ private var beepVolume = BEEP_VOLUME_DEFAULT
93+ private var tone: ToneGenerator ? = null
94+ /* * Volume du générateur en place : il se fixe à la construction, pas à l'appel. */
95+ private var toneVolume = - 1
7496
7597 fun isEnabled (context : Context ): Boolean =
7698 context.getSharedPreferences(PREFS_NAME , Context .MODE_PRIVATE ).getBoolean(KEY_ENABLED , false )
@@ -94,6 +116,11 @@ object WindowAutoClose {
94116 requireBoth = p.getBoolean(KEY_BOTH , false ),
95117 delayS = p.getInt(KEY_DELAY_S , WindowAutoCloseTrigger .DELAY_DEFAULT_S )
96118 .coerceIn(WindowAutoCloseTrigger .DELAY_MIN_S , WindowAutoCloseTrigger .DELAY_MAX_S ),
119+ // Décoché par défaut : le bip est une gêne pour qui n'en veut pas, et la fermeture
120+ // automatique est déjà une option qu'on active en connaissance de cause.
121+ beep = p.getBoolean(KEY_BEEP , false ),
122+ beepVolume = p.getInt(KEY_BEEP_VOLUME , BEEP_VOLUME_DEFAULT )
123+ .coerceIn(BEEP_VOLUME_MIN , BEEP_VOLUME_MAX ),
97124 )
98125 }
99126
@@ -106,6 +133,8 @@ object WindowAutoClose {
106133 putInt(KEY_TIME_MIN , s.timeMin)
107134 putBoolean(KEY_BOTH , s.requireBoth)
108135 putInt(KEY_DELAY_S , s.delayS)
136+ putBoolean(KEY_BEEP , s.beep)
137+ putInt(KEY_BEEP_VOLUME , s.beepVolume)
109138 }
110139 main.post { apply (s) }
111140 AppLogger .i(TAG , " fermeture auto : réglages = ${describe(s)} " )
@@ -122,6 +151,9 @@ object WindowAutoClose {
122151 private fun apply (s : Settings ) {
123152 trigger.arming = s.toArming()
124153 trigger.delayMs = s.delayS * 1000L
154+ beepOn = s.beep
155+ beepVolume = s.beepVolume
156+ if (! beepOn) releaseTone()
125157 }
126158
127159 private fun describe (s : Settings ): String {
@@ -131,7 +163,8 @@ object WindowAutoClose {
131163 )
132164 val arming = if (parts.isEmpty()) " aucune condition (jamais armée)"
133165 else parts.joinToString(if (s.requireBoth) " ET " else " OU " )
134- return " armement $arming , délai ${s.delayS} s"
166+ val bip = if (s.beep) " oui à ${s.beepVolume} %" else " non"
167+ return " armement $arming , délai ${s.delayS} s, bip $bip "
135168 }
136169
137170 // ── Surveillance ────────────────────────────────────────────────────────
@@ -163,6 +196,7 @@ object WindowAutoClose {
163196 running = false
164197 main.removeCallbacks(tick)
165198 ReadyWatcher .remove(readyListener)
199+ releaseTone()
166200 if (trigger.cancelPending()) {
167201 setResult(Result .CANCELLED )
168202 AppLogger .i(TAG , " fermeture auto : attente abandonnée (option désactivée)" )
@@ -210,16 +244,67 @@ object WindowAutoClose {
210244
211245 private fun handle (outcome : WindowAutoCloseTrigger .Outcome ) {
212246 when (outcome.action) {
213- Action .SCHEDULE -> setResult(Result .PENDING )
247+ Action .SCHEDULE -> {
248+ setResult(Result .PENDING )
249+ beep()
250+ }
214251 Action .CANCEL -> {
215252 setResult(Result .CANCELLED )
253+ releaseTone()
216254 AppLogger .i(TAG , " fermeture auto : annulée (${outcome.reason} )" )
217255 }
218- Action .CLOSE -> closeAll(outcome.reason)
219- Action .NONE -> Unit
256+ Action .CLOSE -> {
257+ releaseTone()
258+ closeAll(outcome.reason)
259+ }
260+ // Rien à faire, sauf le bip de chaque seconde d'attente.
261+ Action .NONE -> if (trigger.isPending) beep()
262+ }
263+ }
264+
265+ /* *
266+ * Un bip par seconde pendant le délai, comme winclose : le seul avertissement possible pour
267+ * qui est à côté de la voiture, l'écran ne se voyant pas de l'extérieur. Décoché par défaut.
268+ * Un échec (piste audio occupée, flux refusé) ne doit jamais empêcher la fermeture.
269+ */
270+ private fun beep () {
271+ if (beepOn) playBeep(beepVolume)
272+ }
273+
274+ /* *
275+ * Bip d'essai pendant que l'utilisateur déplace le curseur : régler un volume à l'aveugle
276+ * n'aurait pas de sens. Muet si l'avertissement est décoché.
277+ */
278+ fun previewBeep (context : Context ) {
279+ val s = settings(context)
280+ if (! s.beep) return
281+ playBeep(s.beepVolume)
282+ // Hors surveillance, personne ne viendra rendre la piste audio : on s'en charge.
283+ if (! running) {
284+ main.removeCallbacks(releasePreview)
285+ main.postDelayed(releasePreview, PREVIEW_RELEASE_MS )
220286 }
221287 }
222288
289+ private val releasePreview = Runnable { if (! running) releaseTone() }
290+
291+ private fun playBeep (volume : Int ) {
292+ runCatching {
293+ // Le volume est figé à la construction : un réglage déplacé impose un nouveau générateur.
294+ if (toneVolume != volume) releaseTone()
295+ val t = tone ? : ToneGenerator (AudioManager .STREAM_NOTIFICATION , volume)
296+ .also { tone = it; toneVolume = volume }
297+ t.startTone(ToneGenerator .TONE_PROP_BEEP , BEEP_MS )
298+ }.onFailure { AppLogger .w(TAG , " fermeture auto : bip impossible (${it.javaClass.simpleName} : ${it.message} )" ) }
299+ }
300+
301+ /* * Le générateur tient une piste audio : on le rend dès que l'attente est finie. */
302+ private fun releaseTone () {
303+ tone?.release()
304+ tone = null
305+ toneVolume = - 1
306+ }
307+
223308 private fun closeAll (reason : String ) {
224309 val context = MG4Hardware .appContext()
225310 if (context != null && GarageMode .isOn(context)) {
0 commit comments