Skip to content

Commit 6d1104e

Browse files
committed
fix play/previous/next
1 parent 3c935f8 commit 6d1104e

1 file changed

Lines changed: 148 additions & 52 deletions

File tree

‎app/src/main/java/com/mg4/control/hardware/MG4Hardware.kt‎

Lines changed: 148 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)