Skip to content

Commit ce8524f

Browse files
committed
fix next/previous for all firmware (SWI133 OK)
1 parent 3b2fc7f commit ce8524f

1 file changed

Lines changed: 160 additions & 38 deletions

File tree

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

Lines changed: 160 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3777,13 +3777,102 @@ object MG4Hardware {
37773777
else -> "inconnue ($v)"
37783778
}
37793779

3780+
// ── Projection : le service allgo, hors SDK SAIC ────────────────────────
3781+
//
3782+
// CarPlay et Android Auto ne se pilotent PAS par le même chemin selon le firmware, et les
3783+
// deux voies sont exactement complémentaires — vérifié dans les 6 launchers :
3784+
// • SWI133 : `ICpAaBinderInterface` du SDK média SAIC ;
3785+
// • SWI68 / 165 / 69 / 131 / 132 : ce service-ci, celui de la pile de projection allgo,
3786+
// que le launcher appelle lui-même.
3787+
// Aucun firmware n'a les deux, aucun n'en est dépourvu.
3788+
private const val RUI_PKG = "com.allgo.rui"
3789+
private const val RUI_CLS = "com.allgo.rui.RemoteUIService"
3790+
private const val DESC_RUI = "com.allgo.rui.IRemoteUIService"
3791+
private const val TX_RUI_MEDIA_KEY = 0x12 // sendMediaPlayControlKey(int) : int
3792+
3793+
/**
3794+
* Valeurs de `sendMediaPlayControlKey`, relevées dans les méthodes du launcher SWI68 :
3795+
* `playRemoteUIyMusicResource`, `pauseRemoteUIyMusicResource`, `touchNextRemoteUiMusic`,
3796+
* `touchPreviousRemoteUiMusic`. Ce ne sont PAS des keycodes Android — c'est une énumération
3797+
* propre à allgo, qu'il aurait été impossible de deviner.
3798+
*
3799+
* Piste suivante et précédente se donnent en DEUX temps, appui puis relâchement, comme le
3800+
* fait le launcher.
3801+
*/
3802+
private const val RUI_PLAY = 1
3803+
private const val RUI_PAUSE = 2
3804+
private const val RUI_NEXT_DOWN = 7
3805+
private const val RUI_PREV_DOWN = 8
3806+
private const val RUI_NEXT_UP = 11
3807+
private const val RUI_PREV_UP = 12
3808+
3809+
/** Envoie une commande à la pile de projection. Rend faux si le service ne répond pas. */
3810+
private fun ruiEnvoyer(valeur: Int): Boolean {
3811+
val binder = serviceBinder("", RUI_PKG, RUI_CLS) ?: return false
3812+
val data = Parcel.obtain()
3813+
val reply = Parcel.obtain()
3814+
return try {
3815+
data.writeInterfaceToken(DESC_RUI)
3816+
data.writeInt(valeur)
3817+
binder.transact(TX_RUI_MEDIA_KEY, data, reply, 0)
3818+
reply.readException()
3819+
// La méthode rend un entier : on le journalise sans l'interpréter. Sa signification
3820+
// est inconnue, mais il distinguera un refus d'un acquittement le jour où il faudra.
3821+
val retour = if (reply.dataAvail() > 0) reply.readInt() else 0
3822+
AppLogger.i(MEDIA_TAG, "projection : sendMediaPlayControlKey($valeur) → $retour")
3823+
true
3824+
} catch (e: Exception) {
3825+
AppLogger.w(MEDIA_TAG, "projection : commande $valeur refusée — ${(e.cause ?: e).message}")
3826+
false
3827+
} finally {
3828+
data.recycle()
3829+
reply.recycle()
3830+
}
3831+
}
3832+
3833+
/**
3834+
* Commande média de la projection par la voie allgo.
3835+
*
3836+
* [joue] décide du sens de la bascule lecture/pause : ce service n'expose pas d'état, c'est
3837+
* l'appelant qui l'a déjà déterminé (session, ou `isMusicActive` à défaut).
3838+
*/
3839+
private fun ruiMedia(cmd: CmdMedia, joue: Boolean): Boolean = when (cmd) {
3840+
// Appui PUIS relâchement : un appui laissé « enfoncé » serait interprété comme une
3841+
// avance rapide, c'est le rôle des deux codes distincts.
3842+
CmdMedia.SUIVANT ->
3843+
ruiEnvoyer(RUI_NEXT_DOWN) && ruiEnvoyer(RUI_NEXT_UP)
3844+
CmdMedia.PRECEDENT ->
3845+
ruiEnvoyer(RUI_PREV_DOWN) && ruiEnvoyer(RUI_PREV_UP)
3846+
CmdMedia.LECTURE_PAUSE ->
3847+
ruiEnvoyer(if (joue) RUI_PAUSE else RUI_PLAY)
3848+
}
3849+
37803850
private enum class CmdMedia { SUIVANT, PRECEDENT, LECTURE_PAUSE }
37813851

37823852
// Accès croisé : onServiceConnected arrive sur le thread principal, la lecture se fait
37833853
// depuis le contexte IO du raccourci. Des HashMap simples se corrompraient silencieusement.
37843854
private val sMediaBinders = ConcurrentHashMap<String, IBinder>()
37853855
private val sMediaConns = ConcurrentHashMap<String, ServiceConnection>()
37863856

3857+
/**
3858+
* Actions dont la liaison n'aboutit pas, avec l'instant du constat.
3859+
*
3860+
* ⚠️ Sans cette mémoire, un service absent du firmware coûtait [MEDIA_BIND_MS] à CHAQUE
3861+
* appui : mesuré sur SWI68, où `CPAA_PLAYER_ACTION` n'existe pas, 1,2 s d'attente avant
3862+
* même d'essayer la voie suivante. Un raccourci de volant qui répond en une seconde et
3863+
* demie passe pour cassé, même quand il finit par agir.
3864+
*
3865+
* Le constat est daté plutôt que définitif : un service peut être simplement lent à
3866+
* démarrer, et l'exclure pour toujours sur un seul échec serait excessif.
3867+
*/
3868+
private val sMediaSansReponse = ConcurrentHashMap<String, Long>()
3869+
3870+
/** Délai d'attente d'une liaison, en ms. */
3871+
private const val MEDIA_BIND_MS = 1200L
3872+
3873+
/** Durée pendant laquelle on ne retente pas une action qui n'a pas répondu. */
3874+
private const val MEDIA_ABSENT_MS = 300_000L
3875+
37873876
/**
37883877
* Binder du service média pour une action donnée, ou null.
37893878
*
@@ -3804,45 +3893,75 @@ object MG4Hardware {
38043893
* c'est ainsi que `RadioOptionManager` procède, et une liaison par classe échouerait.
38053894
*/
38063895
private fun serviceBinder(action: String, pkg: String, cls: String?,
3807-
attenteMs: Long = 1200): IBinder? {
3808-
sMediaBinders[action]?.let { if (it.isBinderAlive) return it else sMediaBinders.remove(action) }
3896+
attenteMs: Long = MEDIA_BIND_MS): IBinder? {
3897+
// Clé de cache : l'action quand il y en a une, sinon le composant visé. Le service de
3898+
// projection allgo se lie en effet par COMPOSANT seul, sans action — deux services
3899+
// différents ne doivent pas se partager une entrée de cache.
3900+
val cle = if (action.isEmpty()) "$pkg/$cls" else action
3901+
sMediaBinders[cle]?.let { if (it.isBinderAlive) return it else sMediaBinders.remove(cle) }
38093902
val ctx = sAppContext ?: return null
38103903

3811-
if (!sMediaConns.containsKey(action)) {
3904+
// Cible déjà constatée sans réponse récemment : on rend la main tout de suite plutôt
3905+
// que de refaire attendre l'utilisateur pour le même constat.
3906+
sMediaSansReponse[cle]?.let { instant ->
3907+
if (SystemClock.uptimeMillis() - instant < MEDIA_ABSENT_MS) return null
3908+
sMediaSansReponse.remove(cle)
3909+
}
3910+
3911+
if (!sMediaConns.containsKey(cle)) {
38123912
val conn = object : ServiceConnection {
38133913
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
3814-
if (service != null) sMediaBinders[action] = service
3815-
AppLogger.i(MEDIA_TAG, "lié à ${action.substringAfterLast('.')}")
3914+
if (service != null) sMediaBinders[cle] = service
3915+
sMediaSansReponse.remove(cle)
3916+
AppLogger.i(MEDIA_TAG, "lié à ${cle.substringAfterLast('.')}")
38163917
}
38173918
override fun onServiceDisconnected(name: ComponentName?) {
3818-
sMediaBinders.remove(action)
3819-
AppLogger.w(MEDIA_TAG, "liaison perdue : ${action.substringAfterLast('.')}")
3919+
sMediaBinders.remove(cle)
3920+
AppLogger.w(MEDIA_TAG, "liaison perdue : ${cle.substringAfterLast('.')}")
3921+
}
3922+
// Le service existe mais refuse CETTE action : son onBind rend null. Le système
3923+
// nous le dit ici, immédiatement — inutile d'attendre le délai pour le découvrir.
3924+
override fun onNullBinding(name: ComponentName?) {
3925+
sMediaSansReponse[cle] = SystemClock.uptimeMillis()
3926+
AppLogger.i(MEDIA_TAG, "${cle.substringAfterLast('.')} : " +
3927+
"le service ne fournit pas cette interface sur ce firmware")
38203928
}
38213929
}
3822-
sMediaConns[action] = conn
3930+
sMediaConns[cle] = conn
38233931
val ok = try {
3824-
val intent = Intent(action)
3932+
val intent = if (action.isEmpty()) Intent() else Intent(action)
38253933
if (cls != null) intent.setClassName(pkg, cls) else intent.setPackage(pkg)
38263934
ctx.bindService(intent, conn, Context.BIND_AUTO_CREATE)
38273935
} catch (e: Exception) {
3828-
AppLogger.w(MEDIA_TAG, "bindService ${action.substringAfterLast('.')} : " +
3936+
AppLogger.w(MEDIA_TAG, "bindService ${cle.substringAfterLast('.')} : " +
38293937
"${(e.cause ?: e).message}")
38303938
false
38313939
}
38323940
if (!ok) {
3833-
sMediaConns.remove(action)
3834-
AppLogger.w(MEDIA_TAG, "service média indisponible pour " +
3835-
"${action.substringAfterLast('.')} (absent de ce firmware ?)")
3941+
sMediaConns.remove(cle)
3942+
sMediaSansReponse[cle] = SystemClock.uptimeMillis()
3943+
AppLogger.w(MEDIA_TAG, "service indisponible pour " +
3944+
"${cle.substringAfterLast('.')} (absent de ce firmware ?)")
38363945
return null
38373946
}
38383947
}
38393948

38403949
val limite = SystemClock.uptimeMillis() + attenteMs
38413950
while (SystemClock.uptimeMillis() < limite) {
3842-
sMediaBinders[action]?.let { return it }
3951+
sMediaBinders[cle]?.let { return it }
38433952
try { Thread.sleep(40) } catch (_: InterruptedException) {}
38443953
}
3845-
AppLogger.w(MEDIA_TAG, "liaison ${action.substringAfterLast('.')} non établie en ${attenteMs} ms")
3954+
// Seule une attente COMPLÈTE vaut constat. La sonde de diagnostic interroge avec un
3955+
// délai raccourci : en tirer une conclusion mettrait de côté un service simplement lent,
3956+
// et le raccourci suivant en pâtirait sans raison.
3957+
if (attenteMs >= MEDIA_BIND_MS) {
3958+
sMediaSansReponse[cle] = SystemClock.uptimeMillis()
3959+
AppLogger.w(MEDIA_TAG, "liaison ${cle.substringAfterLast('.')} non établie en " +
3960+
"${attenteMs} ms — mise de côté ${MEDIA_ABSENT_MS / 1000} s")
3961+
} else {
3962+
AppLogger.w(MEDIA_TAG, "liaison ${cle.substringAfterLast('.')} non établie en " +
3963+
"${attenteMs} ms (sondage court, aucun constat retenu)")
3964+
}
38463965
return null
38473966
}
38483967

@@ -4095,23 +4214,14 @@ object MG4Hardware {
40954214
else cible.transportControls.play()
40964215
}
40974216
} else {
4098-
// Repli : le bouton média, envoyé À CETTE SESSION précise. Beaucoup
4099-
// d'applications — les projections en particulier — traitent les touches média
4100-
// sans rien déclarer dans `actions`.
4217+
// Action non déclarée : on ne tente RIEN par cette voie et on rend la main.
41014218
//
4102-
// ⚠️ Ce n'est PAS le `dispatchMediaKeyEvent` global qui avait causé le
4103-
// changement de source : celui-ci vise le contrôleur qu'on a identifié comme
4104-
// étant celui qui joue. Aucune autre session ne peut l'intercepter.
4105-
val code = when (cmd) {
4106-
CmdMedia.SUIVANT -> KeyEvent.KEYCODE_MEDIA_NEXT
4107-
CmdMedia.PRECEDENT -> KeyEvent.KEYCODE_MEDIA_PREVIOUS
4108-
CmdMedia.LECTURE_PAUSE -> KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
4109-
}
4110-
val t = SystemClock.uptimeMillis()
4111-
cible.dispatchMediaButtonEvent(KeyEvent(t, t, KeyEvent.ACTION_DOWN, code, 0))
4112-
cible.dispatchMediaButtonEvent(KeyEvent(t, t, KeyEvent.ACTION_UP, code, 0))
4113-
AppLogger.i(MEDIA_TAG, "action non déclarée — bouton média ${
4114-
KeyEvent.keyCodeToString(code)} envoyé à ${cible.packageName}")
4219+
// Le bouton média envoyé à la session avait été essayé ici — sans effet, mesuré
4220+
// sur SWI68 et SWI131. La vraie réponse pour la projection est le service allgo,
4221+
// et rendre « vrai » à tort empêcherait l'appelant de l'essayer.
4222+
AppLogger.i(MEDIA_TAG, "${cmd.name} non déclarée par ${cible.packageName} — " +
4223+
"voie session abandonnée")
4224+
return false
41154225
}
41164226
true
41174227
} catch (e: Exception) {
@@ -4137,15 +4247,20 @@ object MG4Hardware {
41374247
private fun commandeMedia(cmd: CmdMedia): Boolean {
41384248
val src = mediaSourceCourante()
41394249

4140-
// Service SAIC muet : c'est le cas des firmwares A9, où il n'existe pas. La touche
4141-
// média d'Android devient alors la seule voie — elle atteindra au moins le Bluetooth.
4250+
// Service SAIC muet : c'est le cas des firmwares A9, où il n'existe pas. Trois voies
4251+
// s'y succèdent, de la plus précise à la plus grossière.
41424252
if (src < 0) {
41434253
// Firmwares A9 : le service média SAIC n'existe pas, et leur launcher n'en utilise
41444254
// pas non plus — il pilote les sessions média du framework. C'est donc la voie
41454255
// normale ici, pas un pis-aller.
41464256
AppLogger.i(MEDIA_TAG, "service média SAIC absent — pilotage par session")
41474257
if (sessionMedia(cmd)) return true
41484258

4259+
// La session couvre radio, Bluetooth et USB sur ces firmwares. Ce qu'elle ne couvre
4260+
// pas, c'est la projection : elle ne déclare ni piste suivante ni précédente. D'où
4261+
// le service allgo, qui est justement la voie que leur launcher emprunte.
4262+
if (ruiMedia(cmd, musiqueEnCours())) return true
4263+
41494264
// Ultime recours, si l'énumération des sessions est refusée : la touche média, mais
41504265
// UNIQUEMENT si quelque chose joue. Sans ce garde-fou, l'envoyer pendant que la
41514266
// radio joue (isMusicActive = faux, mesuré) réveillerait la session Bluetooth et
@@ -4214,11 +4329,18 @@ object MG4Hardware {
42144329
CmdMedia.LECTURE_PAUSE -> if (enLecture(cible)) 1 else 2
42154330
})
42164331

4217-
cible == SRC_CARPLAY || cible == SRC_AA -> mediaTransact(ACT_CPAA, DESC_CPAA, when (cmd) {
4218-
CmdMedia.SUIVANT -> 4
4219-
CmdMedia.PRECEDENT -> 3
4220-
CmdMedia.LECTURE_PAUSE -> if (enLecture(cible)) 2 else 1
4221-
})
4332+
cible == SRC_CARPLAY || cible == SRC_AA -> {
4333+
// L'état n'est lu que si la commande en dépend : inutile d'imposer une lecture
4334+
// binder à « piste suivante », qui n'en a que faire.
4335+
val joue = cmd == CmdMedia.LECTURE_PAUSE && enLecture(cible)
4336+
// SWI133 par le SDK SAIC, tous les autres par le service allgo. Les deux voies
4337+
// sont exclusives selon le firmware : celle qui est absente échoue sans effet.
4338+
mediaTransact(ACT_CPAA, DESC_CPAA, when (cmd) {
4339+
CmdMedia.SUIVANT -> 4
4340+
CmdMedia.PRECEDENT -> 3
4341+
CmdMedia.LECTURE_PAUSE -> if (joue) 2 else 1
4342+
}) || ruiMedia(cmd, joue)
4343+
}
42224344

42234345
else -> {
42244346
AppLogger.i(MEDIA_TAG, "source ${nomSource(cible)} sans lecteur identifié — " +

0 commit comments

Comments
 (0)