@@ -4031,12 +4031,17 @@ object MG4Hardware {
40314031 * voie a réellement agi.
40324032 */
40334033 private fun mediaTransact (action : String , descripteur : String , code : Int ,
4034- pkg : String = MEDIA_PKG , cls : String? = MEDIA_CLS ): Boolean {
4034+ pkg : String = MEDIA_PKG , cls : String? = MEDIA_CLS ,
4035+ arg : Int? = null): Boolean {
40354036 val binder = serviceBinder(action, pkg, cls) ? : return false
40364037 val data = Parcel .obtain()
40374038 val reply = Parcel .obtain()
40384039 return try {
40394040 data.writeInterfaceToken(descripteur)
4041+ // ⚠️ Toutes ces méthodes ne sont pas sans argument. Omettre celui qu'attend le
4042+ // service ne provoque aucune erreur : il lit un parcel vide et obtient 0 — une
4043+ // valeur parfaitement valide, mais qui n'est pas celle voulue.
4044+ if (arg != null ) data.writeInt(arg)
40404045 val ok = binder.transact(code, data, reply, 0 )
40414046 reply.readException()
40424047 AppLogger .i(MEDIA_TAG , " ${action.substringAfterLast(' .' )} tx=0x${Integer .toHexString(code)} → $ok " )
@@ -4076,20 +4081,25 @@ object MG4Hardware {
40764081 }
40774082 }
40784083
4084+ /* * Bande et état de la radio, lus en une seule interrogation. */
4085+ private data class InfoRadio (val type : Int , val enLecture : Boolean )
4086+
40794087 /* *
4080- * La radio joue-t-elle ? `null` si on ne peut pas conclure.
4088+ * Lit le `RadioBean` courant : bande écoutée et lecture en cours. `null` si illisible.
4089+ *
4090+ * Les deux informations viennent du même appel parce qu'elles sont dans le même bean, et
4091+ * qu'une seconde interrogation donnerait au mieux le même résultat, au pire un résultat
4092+ * décalé.
40814093 *
4082- * `isPlaying()` du SDK radio se résume à `getCurrentRadioInfo().getRadioState() == 1`. Il
4083- * faut donc lire le `RadioBean` renvoyé, dans l'ordre exact de son `writeToParcel` :
4084- * enable(byte), name(String), rds(byte), cover(String), fréquence(int), type(int),
4085- * **état(int)**. C'est le seul endroit du projet couplé à la sérialisation d'un bean SAIC —
4086- * tout est sous try/catch, et un échec vaut `null`, jamais une supposition.
4094+ * ⚠️ Lecture dans l'ordre EXACT de `writeToParcel` : enable(byte), nom(String), rds(byte),
4095+ * pochette(String), fréquence(int), **type(int)**, **état(int)**. C'est le seul endroit du
4096+ * projet couplé à la sérialisation d'un bean SAIC — tout est sous try/catch, et un échec
4097+ * vaut `null`, jamais une supposition.
40874098 *
4088- * Sans cette lecture, `isMusicActive` servait de repli : or il est **faux même quand la
4089- * radio joue**, son flux ne passant pas par le canal musique. On envoyait donc toujours
4090- * « lecture », jamais « pause » — d'où un raccourci sans effet.
4099+ * `isPlaying()` du SDK radio se résume à `getRadioState() == 1` ; `next()`/`previous()`, eux,
4100+ * reçoivent `getRadioType()`.
40914101 */
4092- private fun radioEnLecture (): Boolean ? {
4102+ private fun radioInfo (): InfoRadio ? {
40934103 val binder = serviceBinder(RADIO_ACT , RADIO_PKG , null ) ? : return null
40944104 val data = Parcel .obtain()
40954105 val reply = Parcel .obtain()
@@ -4100,10 +4110,11 @@ object MG4Hardware {
41004110 if (reply.readInt() == 0 ) return null // bean nul
41014111 reply.readByte(); reply.readString() // enable, nom
41024112 reply.readByte(); reply.readString() // rds, pochette
4103- reply.readInt(); reply.readInt() // fréquence, type
4113+ reply.readInt() // fréquence
4114+ val type = reply.readInt()
41044115 val etat = reply.readInt()
4105- AppLogger .i(MEDIA_TAG , " état radio = $etat (1 = en lecture)" )
4106- etat == 1
4116+ AppLogger .i(MEDIA_TAG , " radio : type= $type état= $etat (1 = en lecture)" )
4117+ InfoRadio (type, etat == 1 )
41074118 } catch (e: Exception ) {
41084119 AppLogger .w(MEDIA_TAG , " état radio illisible : ${(e.cause ? : e).message} " )
41094120 null
@@ -4113,43 +4124,6 @@ object MG4Hardware {
41134124 }
41144125 }
41154126
4116- /* *
4117- * CarPlay / Android Auto joue-t-il ? `null` si on ne peut pas conclure.
4118- *
4119- * `getLastCpAaAudioInfoBean` (tx 0x7) rend un `AudioInfoBean` dont le champ `mPlayState`
4120- * vaut 3 en lecture — la carte média du launcher le compare à cette valeur exacte. Il faut
4121- * dérouler le bean dans l'ordre de son `writeToParcel` : id(long), durée(long), puis sept
4122- * chaînes, deux longs, une chaîne, et enfin **l'état(int)**.
4123- *
4124- * Sans cette lecture, la projection retombait sur `isMusicActive`, qui reste vrai deux à
4125- * trois secondes après une pause : le raccourci renvoyait une pause au lieu d'une reprise,
4126- * et il fallait patienter avant que la bascule redevienne possible.
4127- */
4128- private fun cpAaEnLecture (): Boolean? {
4129- val binder = serviceBinder(ACT_CPAA , MEDIA_PKG , MEDIA_CLS ) ? : return null
4130- val data = Parcel .obtain()
4131- val reply = Parcel .obtain()
4132- return try {
4133- data.writeInterfaceToken(DESC_CPAA )
4134- binder.transact(0x7 , data, reply, 0 )
4135- reply.readException()
4136- if (reply.readInt() == 0 ) return null // bean nul : rien à conclure
4137- reply.readLong(); reply.readLong() // id, durée
4138- repeat(7 ) { reply.readString() } // nom, pochette, chemin, artiste, utilisateur, avatar, album
4139- reply.readLong(); reply.readLong() // ajout, dernière lecture
4140- reply.readString() // position lisible
4141- val etat = reply.readInt()
4142- AppLogger .i(MEDIA_TAG , " état projection = $etat ($PLAYER_STATUS_START = en lecture)" )
4143- etat == PLAYER_STATUS_START
4144- } catch (e: Exception ) {
4145- AppLogger .w(MEDIA_TAG , " état projection illisible : ${(e.cause ? : e).message} " )
4146- null
4147- } finally {
4148- data.recycle()
4149- reply.recycle()
4150- }
4151- }
4152-
41534127 /* * Source audio réellement active, ou -1 si le service SAIC ne répond pas. */
41544128 private fun mediaSourceCourante (): Int {
41554129 val v = mediaLireInt(ACT_STATUS , DESC_STATUS , 0x9 ) ? : return - 1 // getCurrentMediaSource
@@ -4345,16 +4319,29 @@ object MG4Hardware {
43454319 cible in SRC_RADIO_MIN .. SRC_RADIO_MAX -> {
43464320 // La radio a son propre service : next/previous y changent de station, et
43474321 // srcPlayRadio/srcPauseRadio sont les commandes que le launcher utilise.
4322+ val info = radioInfo()
43484323 // Un état illisible est traité comme « en lecture » : c'est l'état normal
43494324 // d'une radio qui est la source active, et une pause de trop se corrige d'un
43504325 // second appui — l'inverse laisserait le raccourci sans effet.
4351- val joue = radioEnLecture() != false
4326+ val joue = info?.enLecture != false
43524327 val code = when (cmd) {
43534328 CmdMedia .SUIVANT -> RADIO_NEXT
43544329 CmdMedia .PRECEDENT -> RADIO_PREV
43554330 CmdMedia .LECTURE_PAUSE -> if (joue) RADIO_PAUSE else RADIO_PLAY
43564331 }
4357- val ok = mediaTransact(RADIO_ACT , DESC_RADIO , code, RADIO_PKG , null )
4332+
4333+ // ⚠️ `next` et `previous` attendent la BANDE écoutée, contrairement à
4334+ // play/pause. Sans elle le service lit 0 et bascule en FM : mesuré sur SWI133,
4335+ // une station DAB retombait systématiquement en FM. Bande inconnue = on
4336+ // s'abstient, plutôt que de changer de bande dans le dos de l'utilisateur.
4337+ val bande = if (cmd == CmdMedia .LECTURE_PAUSE ) null else info?.type
4338+ if (cmd != CmdMedia .LECTURE_PAUSE && bande == null ) {
4339+ AppLogger .w(MEDIA_TAG , " radio : bande illisible — aucune action " +
4340+ " (l'envoyer sans la bande ferait basculer en FM)" )
4341+ return false
4342+ }
4343+
4344+ val ok = mediaTransact(RADIO_ACT , DESC_RADIO , code, RADIO_PKG , null , bande)
43584345 // Repli DANS la source uniquement : la façade générique s'adresse à la source
43594346 // courante, elle ne peut donc pas en réveiller une autre.
43604347 if (ok) true else mediaTransact(ACT_MEDIA , DESC_MEDIA , when (cmd) {
0 commit comments