@@ -3701,39 +3701,46 @@ object MG4Hardware {
37013701 private const val DESC_STATUS = " com.saicmotor.sdk.media.IPlayStatusBinderInterface"
37023702
37033703 /* *
3704- * Sources rendues par `getCurrentMediaSource` — table `MediaConstants` du SDK launcher,
3705- * relevée telle quelle.
3704+ * Valeurs rendues par `getCurrentMediaSource`.
37063705 *
3707- * ⚠️ C'est `getCurrentMediaSource` qu'il faut interroger, PAS `getCurrentPlayer`. Ce dernier
3708- * ne connaît que les trois lecteurs de musique : passé sur la radio, il continue de rendre
3709- * le lecteur précédent. On commandait alors le Bluetooth alors que la radio jouait — et
3710- * comme lui dire « piste suivante » le réveille, le raccourci **changeait de source**.
3706+ * ⚠️ L'espace de valeurs est HYBRIDE, et ça ne se devine pas : le SDK y mélange ses
3707+ * `MEDIA_TYPE_*` (petits entiers) et ses `*_SOURCE_CODE` (0x12, 0x32, 0x46). Seules quatre
3708+ * valeurs sont CERTAINES — les trois constantes du SDK, et le 3 relevé sur véhicule en
3709+ * radio. Les autres restent des candidats, d'où l'identification de secours plus bas :
3710+ * quand le code est inconnu, on demande aux lecteurs lequel joue au lieu de parier.
37113711 */
3712- private const val SRC_NONE = 0
3713- private const val SRC_RADIO = 1
3714- private const val SRC_RADIO_FM = 2
3715- private const val SRC_RADIO_AM = 3
3716- private const val SRC_RADIO_DAB = 4
3717- private const val SRC_BT = 5
3718- private const val SRC_ONLINE = 6
3719- private const val SRC_USB = 7
3720- private const val SRC_CP = 8
3721- private const val SRC_AA = 9
3722- private const val SRC_USB_VIDEO = 10
3712+ private const val SRC_RADIO_MIN = 1 // 1 radio, 2 FM, 3 AM, 4 DAB
3713+ private const val SRC_RADIO_MAX = 4
3714+ private const val SRC_BT = 5 // candidat
3715+ private const val SRC_ONLINE = 6 // candidat
3716+ private const val SRC_USB = 7 // candidat
3717+ private const val SRC_USB_VIDEO = 0x12 // certain (USB_VIDEO_SOURCE_CODE)
3718+ private const val SRC_CARPLAY = 0x32 // certain (CP_MEDIA_SOURCE_CODE)
3719+ private const val SRC_AA = 0x46 // certain (AA_MEDIA_SOURCE_CODE)
37233720
37243721 /* * États de lecture (`MediaConstants`) : seul START vaut « en train de jouer ». */
37253722 private const val PLAYER_STATUS_START = 3
37263723
3727- private fun nomSource (v : Int ): String = when (v) {
3728- SRC_NONE -> " aucune"
3729- SRC_RADIO , SRC_RADIO_FM , SRC_RADIO_AM , SRC_RADIO_DAB -> " radio"
3730- SRC_BT -> " Bluetooth"
3731- SRC_ONLINE -> " en ligne"
3732- SRC_USB -> " USB"
3733- SRC_CP -> " CarPlay"
3734- SRC_AA -> " Android Auto"
3735- SRC_USB_VIDEO -> " vidéo USB"
3736- else -> " inconnue — à identifier"
3724+ // ── Service radio : un service à part, avec sa propre interface ──
3725+ private const val RADIO_PKG = " com.saicmotor.service.radio"
3726+ private const val RADIO_ACT = " com.saicmotor.service.radio.radioservice"
3727+ private const val DESC_RADIO = " com.saicmotor.sdk.radio.IRadioAppService"
3728+ private const val RADIO_NEXT = 0xd
3729+ private const val RADIO_PREV = 0xe
3730+ private const val RADIO_INFO = 0x13 // getCurrentRadioInfo → RadioBean
3731+ private const val RADIO_PLAY = 0x1b // srcPlayRadio
3732+ private const val RADIO_PAUSE = 0x1c // srcPauseRadio
3733+
3734+ private fun nomSource (v : Int ): String = when {
3735+ v == 0 -> " aucune"
3736+ v in SRC_RADIO_MIN .. SRC_RADIO_MAX -> " radio"
3737+ v == SRC_BT -> " Bluetooth"
3738+ v == SRC_ONLINE -> " en ligne"
3739+ v == SRC_USB -> " USB"
3740+ v == SRC_USB_VIDEO -> " vidéo USB"
3741+ v == SRC_CARPLAY -> " CarPlay"
3742+ v == SRC_AA -> " Android Auto"
3743+ else -> " inconnue ($v )"
37373744 }
37383745
37393746 private enum class CmdMedia { SUIVANT , PRECEDENT , LECTURE_PAUSE }
@@ -3753,7 +3760,17 @@ object MG4Hardware {
37533760 * Une action inconnue du firmware (SWI68/165 n'ont ni façade générique ni CarPlay/AA) fait
37543761 * simplement échouer `bindService` : on rend null, et l'appelant passe au repli suivant.
37553762 */
3756- private fun mediaBinder (action : String , attenteMs : Long = 1200): IBinder ? {
3763+ private fun mediaBinder (action : String , attenteMs : Long = 1200): IBinder ? =
3764+ serviceBinder(action, MEDIA_PKG , MEDIA_CLS , attenteMs)
3765+
3766+ /* *
3767+ * Binder d'un service SAIC, lié à la demande et mis en cache.
3768+ *
3769+ * [cls] est nul pour le service radio : il se lie par paquet + action, pas par classe —
3770+ * c'est ainsi que `RadioOptionManager` procède, et une liaison par classe échouerait.
3771+ */
3772+ private fun serviceBinder (action : String , pkg : String , cls : String? ,
3773+ attenteMs : Long = 1200): IBinder ? {
37573774 sMediaBinders[action]?.let { if (it.isBinderAlive) return it else sMediaBinders.remove(action) }
37583775 val ctx = sAppContext ? : return null
37593776
@@ -3770,8 +3787,9 @@ object MG4Hardware {
37703787 }
37713788 sMediaConns[action] = conn
37723789 val ok = try {
3773- ctx.bindService(Intent (action).setClassName(MEDIA_PKG , MEDIA_CLS ),
3774- conn, Context .BIND_AUTO_CREATE )
3790+ val intent = Intent (action)
3791+ if (cls != null ) intent.setClassName(pkg, cls) else intent.setPackage(pkg)
3792+ ctx.bindService(intent, conn, Context .BIND_AUTO_CREATE )
37753793 } catch (e: Exception ) {
37763794 AppLogger .w(MEDIA_TAG , " bindService ${action.substringAfterLast(' .' )} : " +
37773795 " ${(e.cause ? : e).message} " )
@@ -3806,8 +3824,9 @@ object MG4Hardware {
38063824 * journal à chaque étape : c'est lui, pas la valeur de retour, qui dira au volant quelle
38073825 * voie a réellement agi.
38083826 */
3809- private fun mediaTransact (action : String , descripteur : String , code : Int ): Boolean {
3810- val binder = mediaBinder(action) ? : return false
3827+ private fun mediaTransact (action : String , descripteur : String , code : Int ,
3828+ pkg : String = MEDIA_PKG , cls : String? = MEDIA_CLS ): Boolean {
3829+ val binder = serviceBinder(action, pkg, cls) ? : return false
38113830 val data = Parcel .obtain()
38123831 val reply = Parcel .obtain()
38133832 return try {
@@ -3851,6 +3870,43 @@ object MG4Hardware {
38513870 }
38523871 }
38533872
3873+ /* *
3874+ * La radio joue-t-elle ? `null` si on ne peut pas conclure.
3875+ *
3876+ * `isPlaying()` du SDK radio se résume à `getCurrentRadioInfo().getRadioState() == 1`. Il
3877+ * faut donc lire le `RadioBean` renvoyé, dans l'ordre exact de son `writeToParcel` :
3878+ * enable(byte), name(String), rds(byte), cover(String), fréquence(int), type(int),
3879+ * **état(int)**. C'est le seul endroit du projet couplé à la sérialisation d'un bean SAIC —
3880+ * tout est sous try/catch, et un échec vaut `null`, jamais une supposition.
3881+ *
3882+ * Sans cette lecture, `isMusicActive` servait de repli : or il est **faux même quand la
3883+ * radio joue**, son flux ne passant pas par le canal musique. On envoyait donc toujours
3884+ * « lecture », jamais « pause » — d'où un raccourci sans effet.
3885+ */
3886+ private fun radioEnLecture (): Boolean? {
3887+ val binder = serviceBinder(RADIO_ACT , RADIO_PKG , null ) ? : return null
3888+ val data = Parcel .obtain()
3889+ val reply = Parcel .obtain()
3890+ return try {
3891+ data.writeInterfaceToken(DESC_RADIO )
3892+ binder.transact(RADIO_INFO , data, reply, 0 )
3893+ reply.readException()
3894+ if (reply.readInt() == 0 ) return null // bean nul
3895+ reply.readByte(); reply.readString() // enable, nom
3896+ reply.readByte(); reply.readString() // rds, pochette
3897+ reply.readInt(); reply.readInt() // fréquence, type
3898+ val etat = reply.readInt()
3899+ AppLogger .i(MEDIA_TAG , " état radio = $etat (1 = en lecture)" )
3900+ etat == 1
3901+ } catch (e: Exception ) {
3902+ AppLogger .w(MEDIA_TAG , " état radio illisible : ${(e.cause ? : e).message} " )
3903+ null
3904+ } finally {
3905+ data.recycle()
3906+ reply.recycle()
3907+ }
3908+ }
3909+
38543910 /* * Source audio réellement active, ou -1 si le service SAIC ne répond pas. */
38553911 private fun mediaSourceCourante (): Int {
38563912 val v = mediaLireInt(ACT_STATUS , DESC_STATUS , 0x9 ) ? : return - 1 // getCurrentMediaSource
@@ -3898,6 +3954,20 @@ object MG4Hardware {
38983954 * 4. **La touche média Android**, dernier recours : c'est la seule voie sur les firmwares A9,
38993955 * où ce service SAIC n'existe pas, et la seule qui atteigne une session Bluetooth.
39003956 */
3957+ /* *
3958+ * Quand le code de source est inconnu, on DEMANDE aux lecteurs lequel joue.
3959+ *
3960+ * C'est ce qui rend le pilotage robuste malgré une table de sources incomplète : on ne
3961+ * commande que celui qui se déclare en lecture, donc jamais une source endormie — c'est
3962+ * exactement l'erreur qui faisait changer de source auparavant.
3963+ */
3964+ private fun sourceQuiJoue (): Int? {
3965+ if (mediaLireInt(ACT_BT , DESC_BT , 0x9 )?.let { it != 0 } == true ) return SRC_BT
3966+ if (mediaLireInt(ACT_USB , DESC_USB , 0x1e )?.let { it != 0 } == true ) return SRC_USB
3967+ if (mediaLireInt(ACT_ONLINE , DESC_ONLINE , 0x6 ) == PLAYER_STATUS_START ) return SRC_ONLINE
3968+ return null
3969+ }
3970+
39013971 private fun commandeMedia (cmd : CmdMedia ): Boolean {
39023972 val src = mediaSourceCourante()
39033973
@@ -3912,48 +3982,74 @@ object MG4Hardware {
39123982 })
39133983 }
39143984
3915- // ⚠️ AUCUNE CASCADE ICI, et c'est le cœur du correctif. Essayer les binders l'un après
3916- // l'autre revenait à commander une source qui ne jouait pas — le Bluetooth répondait
3917- // « oui » et se remettait à jouer, ce qui CHANGEAIT la source sous les doigts de
3918- // l'utilisateur. On ne parle qu'à la source réellement active, ou à personne.
3919- return when (src) {
3920- SRC_BT -> mediaTransact(ACT_BT , DESC_BT , when (cmd) {
3985+ // ⚠️ AUCUNE CASCADE ENTRE SOURCES. Essayer les binders l'un après l'autre revenait à
3986+ // commander une source qui ne jouait pas : elle répondait « oui » et se remettait à
3987+ // jouer, ce qui CHANGEAIT la source sous les doigts de l'utilisateur.
3988+ val cible = if (src in SRC_RADIO_MIN .. SRC_RADIO_MAX || src == SRC_BT ||
3989+ src == SRC_ONLINE || src == SRC_USB || src == SRC_CARPLAY ||
3990+ src == SRC_AA ) src
3991+ else sourceQuiJoue()?.also {
3992+ AppLogger .i(MEDIA_TAG , " code $src inconnu — lecteur identifié : ${nomSource(it)} " )
3993+ } ? : src
3994+
3995+ return when {
3996+ cible in SRC_RADIO_MIN .. SRC_RADIO_MAX -> {
3997+ // La radio a son propre service : next/previous y changent de station, et
3998+ // srcPlayRadio/srcPauseRadio sont les commandes que le launcher utilise.
3999+ // Un état illisible est traité comme « en lecture » : c'est l'état normal
4000+ // d'une radio qui est la source active, et une pause de trop se corrige d'un
4001+ // second appui — l'inverse laisserait le raccourci sans effet.
4002+ val joue = radioEnLecture() != false
4003+ val code = when (cmd) {
4004+ CmdMedia .SUIVANT -> RADIO_NEXT
4005+ CmdMedia .PRECEDENT -> RADIO_PREV
4006+ CmdMedia .LECTURE_PAUSE -> if (joue) RADIO_PAUSE else RADIO_PLAY
4007+ }
4008+ val ok = mediaTransact(RADIO_ACT , DESC_RADIO , code, RADIO_PKG , null )
4009+ // Repli DANS la source uniquement : la façade générique s'adresse à la source
4010+ // courante, elle ne peut donc pas en réveiller une autre.
4011+ if (ok) true else mediaTransact(ACT_MEDIA , DESC_MEDIA , when (cmd) {
4012+ CmdMedia .SUIVANT -> 5
4013+ CmdMedia .PRECEDENT -> 4
4014+ CmdMedia .LECTURE_PAUSE -> if (joue) 3 else 2
4015+ })
4016+ }
4017+
4018+ cible == SRC_BT -> mediaTransact(ACT_BT , DESC_BT , when (cmd) {
39214019 CmdMedia .SUIVANT -> 5
39224020 CmdMedia .PRECEDENT -> 4
3923- CmdMedia .LECTURE_PAUSE -> if (enLecture(src )) 1 else 2
4021+ CmdMedia .LECTURE_PAUSE -> if (enLecture(cible )) 1 else 2
39244022 })
39254023
3926- SRC_USB -> mediaTransact(ACT_USB , DESC_USB , when (cmd) {
4024+ cible == SRC_USB -> mediaTransact(ACT_USB , DESC_USB , when (cmd) {
39274025 CmdMedia .SUIVANT -> 0x1a // playNextMusic
39284026 CmdMedia .PRECEDENT -> 0x19 // playLastMusic
3929- CmdMedia .LECTURE_PAUSE -> 0xc // playOrPause : vraie bascule, aucun état à lire
4027+ CmdMedia .LECTURE_PAUSE -> 0xc // playOrPause : vraie bascule
39304028 })
39314029
3932- SRC_ONLINE -> mediaTransact(ACT_ONLINE , DESC_ONLINE , when (cmd) {
4030+ cible == SRC_ONLINE -> mediaTransact(ACT_ONLINE , DESC_ONLINE , when (cmd) {
39334031 CmdMedia .SUIVANT -> 4
39344032 CmdMedia .PRECEDENT -> 3
3935- CmdMedia .LECTURE_PAUSE -> if (enLecture(src )) 1 else 2
4033+ CmdMedia .LECTURE_PAUSE -> if (enLecture(cible )) 1 else 2
39364034 })
39374035
3938- SRC_CP , SRC_AA -> mediaTransact(ACT_CPAA , DESC_CPAA , when (cmd) {
4036+ cible == SRC_CARPLAY || cible == SRC_AA -> mediaTransact(ACT_CPAA , DESC_CPAA , when (cmd) {
39394037 CmdMedia .SUIVANT -> 4
39404038 CmdMedia .PRECEDENT -> 3
3941- CmdMedia .LECTURE_PAUSE -> if (enLecture(src)) 2 else 1
4039+ // La projection n'expose pas son état : isMusicActive est le seul indice, et il
4040+ // suffit ici — il retombe à faux quand la lecture s'arrête vraiment.
4041+ CmdMedia .LECTURE_PAUSE -> if (enLecture(cible)) 2 else 1
39424042 })
39434043
39444044 else -> {
3945- // Radio, vidéo, aucune source : le SDK n'expose pas de binder de pilotage pour
3946- // elles. La façade générique s'adresse à la source courante, elle est donc la
3947- // seule tentative acceptable — et si elle ne répond pas, on ne fait RIEN plutôt
3948- // que de réveiller une autre source.
3949- AppLogger .i(MEDIA_TAG , " source ${nomSource(src)} sans binder dédié — " +
4045+ AppLogger .i(MEDIA_TAG , " source ${nomSource(cible)} sans lecteur identifié — " +
39504046 " façade générique seule" )
39514047 val ok = mediaTransact(ACT_MEDIA , DESC_MEDIA , when (cmd) {
39524048 CmdMedia .SUIVANT -> 5
39534049 CmdMedia .PRECEDENT -> 4
3954- CmdMedia .LECTURE_PAUSE -> if (enLecture(src )) 3 else 2
4050+ CmdMedia .LECTURE_PAUSE -> if (enLecture(cible )) 3 else 2
39554051 })
3956- if (! ok) AppLogger .i(MEDIA_TAG , " aucune action possible sur ${nomSource(src )} " )
4052+ if (! ok) AppLogger .i(MEDIA_TAG , " aucune action possible sur ${nomSource(cible )} " )
39574053 ok
39584054 }
39594055 }
0 commit comments