@@ -3702,6 +3702,125 @@ object MG4Hardware {
37023702 */
37033703 fun mediaPlayPause (): Boolean = envoyerToucheMedia(KeyEvent .KEYCODE_MEDIA_PLAY_PAUSE )
37043704
3705+ /* *
3706+ * Monte ou descend le volume média d'un cran.
3707+ *
3708+ * Deux voies, dans cet ordre :
3709+ * 1. la voie **SAIC** ([getMediaVolume] / [setMediaVolume]) — la même que la baisse à
3710+ * l'ouverture de porte, déjà validée sur véhicule. C'est la seule qui donne un niveau
3711+ * ABSOLU, donc un log exploitable et un vrai respect des bornes ;
3712+ * 2. à défaut, `AudioManager.adjustStreamVolume`, la voie Android standard. Elle ne dit pas
3713+ * d'où l'on part, mais elle répond là où les managers SAIC ne sont pas liés — et elle
3714+ * affiche l'indicateur système, ce qui donne un retour visuel.
3715+ *
3716+ * Contrairement aux touches média, aucune des deux ne dépend d'une application : le volume
3717+ * est un réglage du véhicule. C'est pourquoi ces raccourcis fonctionnent quelle que soit la
3718+ * source, y compris là où « piste suivante » reste sans effet.
3719+ */
3720+ fun mediaVolumeStep (delta : Int ): Boolean {
3721+ val actuel = getMediaVolume()
3722+ val max = getMediaVolumeMax()
3723+ if (actuel >= 0 && max > 0 ) {
3724+ val cible = (actuel + delta).coerceIn(0 , max)
3725+ if (cible == actuel) {
3726+ AppLogger .i(VOL_TAG , " volume déjà à la borne ($actuel /$max ) — aucune écriture" )
3727+ return true
3728+ }
3729+ AppLogger .i(VOL_TAG , " volume $actuel → $cible (max $max , voie SAIC)" )
3730+ return setMediaVolume(cible)
3731+ }
3732+ val am = sAppContext?.getSystemService(Context .AUDIO_SERVICE ) as ? AudioManager
3733+ if (am == null ) {
3734+ AppLogger .w(VOL_TAG , " volume : ni voie SAIC ni AudioManager — aucune action" )
3735+ return false
3736+ }
3737+ return try {
3738+ am.adjustStreamVolume(
3739+ AudioManager .STREAM_MUSIC ,
3740+ if (delta > 0 ) AudioManager .ADJUST_RAISE else AudioManager .ADJUST_LOWER ,
3741+ AudioManager .FLAG_SHOW_UI
3742+ )
3743+ AppLogger .i(VOL_TAG , " volume ${if (delta > 0 ) " +1" else " -1" } " +
3744+ " (voie Android — niveau SAIC illisible, actuel=$actuel max=$max )" )
3745+ true
3746+ } catch (e: Exception ) {
3747+ AppLogger .w(VOL_TAG , " volume : ${(e.cause ? : e).message} " )
3748+ false
3749+ }
3750+ }
3751+
3752+ /* *
3753+ * Sonde média, LECTURE SEULE — elle n'envoie aucune commande.
3754+ *
3755+ * Elle existe pour trancher la seule question qui compte quand une touche média reste sans
3756+ * effet : **la commande n'est-elle pas partie, ou est-elle partie sans destinataire ?** Vu
3757+ * de l'utilisateur les deux cas sont identiques, et `dispatchMediaKeyEvent` ne rend rien.
3758+ *
3759+ * Trois informations, de la plus sûre à la plus révélatrice :
3760+ * • `isMusicActive` — quelque chose joue-t-il ;
3761+ * • les flux audio actifs et, si le système le laisse voir, l'application qui les produit ;
3762+ * • les **sessions média**. C'est le point décisif : `dispatchMediaKeyEvent` ne peut
3763+ * atteindre qu'une session. S'il n'y en a aucune, aucune touche média n'aboutira jamais,
3764+ * quel que soit le code envoyé — il faudra alors une autre voie.
3765+ */
3766+ fun runMediaDiag () {
3767+ val ctx = sAppContext ? : return
3768+ val am = ctx.getSystemService(Context .AUDIO_SERVICE ) as ? AudioManager
3769+ if (am == null ) {
3770+ AppLogger .w(MEDIA_TAG , " DIAG : AudioManager indisponible" )
3771+ return
3772+ }
3773+ AppLogger .i(MEDIA_TAG , " ── DIAG média ────────────────────────────────" )
3774+ AppLogger .i(MEDIA_TAG , " lecture en cours (isMusicActive) = ${am.isMusicActive} " )
3775+
3776+ // Qui produit du son. La liste est publique ; l'identité du client, elle, est masquée aux
3777+ // applications ordinaires — on la tente par réflexion, en app système.
3778+ try {
3779+ val configs = am.activePlaybackConfigurations
3780+ AppLogger .i(MEDIA_TAG , " flux audio actifs : ${configs.size} " )
3781+ configs.forEachIndexed { i, c ->
3782+ val uid = runCatching {
3783+ c.javaClass.getMethod(" getClientUid" ).invoke(c) as ? Int
3784+ }.getOrNull()
3785+ val paquets = uid?.let {
3786+ runCatching { ctx.packageManager.getPackagesForUid(it)?.joinToString() }
3787+ .getOrNull()
3788+ }
3789+ AppLogger .i(MEDIA_TAG , " [$i ] usage=${c.audioAttributes.usage} " +
3790+ " contenu=${c.audioAttributes.contentType} uid=${uid ? : " ?" } " +
3791+ " paquet=${paquets ? : " inconnu" } " )
3792+ }
3793+ } catch (e: Exception ) {
3794+ AppLogger .w(MEDIA_TAG , " flux audio illisibles : ${(e.cause ? : e).message} " )
3795+ }
3796+
3797+ try {
3798+ val msm = ctx.getSystemService(" media_session" )
3799+ ? : throw IllegalStateException (" MediaSessionManager absent" )
3800+ val m = msm.javaClass.getMethod(" getActiveSessions" , ComponentName ::class .java)
3801+ @Suppress(" UNCHECKED_CAST" )
3802+ val sessions = m.invoke(msm, null ) as ? List <Any >
3803+ AppLogger .i(MEDIA_TAG , " sessions média actives : ${sessions?.size ? : 0 } " )
3804+ sessions?.forEach { session ->
3805+ val pkg = runCatching {
3806+ session.javaClass.getMethod(" getPackageName" ).invoke(session)
3807+ }.getOrNull()
3808+ AppLogger .i(MEDIA_TAG , " session : $pkg " )
3809+ }
3810+ if (sessions.isNullOrEmpty()) {
3811+ AppLogger .w(MEDIA_TAG , " AUCUNE session : les touches média ne peuvent aboutir " +
3812+ " nulle part, la source ne passe pas par MediaSession" )
3813+ }
3814+ } catch (e: Exception ) {
3815+ val cause = e.cause ? : e
3816+ AppLogger .w(MEDIA_TAG , " sessions média illisibles : " +
3817+ " ${cause.javaClass.simpleName} — ${cause.message} " +
3818+ " (SecurityException = il manque MEDIA_CONTENT_CONTROL au manifest, " +
3819+ " et l'entrée correspondante dans permission-allowlist.txt)" )
3820+ }
3821+ AppLogger .i(MEDIA_TAG , " ──────────────────────────────────────────────" )
3822+ }
3823+
37053824 /* *
37063825 * Envoie une touche média au système, qui la remet à la session active.
37073826 *
0 commit comments