L'API Multi-Display Communications peut être utilisée par une application privilégiée du système dans AAOS pour communiquer avec la même application (même nom de package) exécutée dans une autre zone d'occupant dans une voiture. Cette page explique comment intégrer l'API. Pour en savoir plus, consultez également CarOccupantZoneManager.OccupantZoneInfo.
Zone des occupants
Le concept de zone d'occupant permet de faire correspondre un utilisateur à un ensemble d'écrans. Chaque zone d'occupant dispose d'un écran de type DISPLAY_TYPE_MAIN. Une zone d'occupant peut également comporter des écrans supplémentaires, comme un écran de cluster. Chaque zone d'occupant est associée à un utilisateur Android. Chaque utilisateur dispose de ses propres comptes et applications.
Configuration matérielle
L'API Comms n'est compatible qu'avec un seul SoC. Dans le modèle à SoC unique, toutes les zones d'occupants et tous les utilisateurs s'exécutent sur le même SoC. L'API Comms se compose de trois composants:
L'API de gestion de l'alimentation permet au client de gérer l'alimentation des écrans dans les zones d'occupation.
L'API Discovery permet au client de surveiller les états des autres zones d'occupant dans la voiture et de surveiller les clients pairs dans ces zones. Utilisez l'API Discovery avant d'utiliser l'API Connection.
L'API Connection permet au client de se connecter à son client homologue dans une autre zone d'occupant et d'envoyer une charge utile à ce client.
L'API Discovery et l'API Connection sont requises pour la connexion. L'API Power Management est facultative.
L'API Comms n'est pas compatible avec la communication entre différentes applications. Il est conçu uniquement pour la communication entre les applications portant le même nom de package et utilisé uniquement pour la communication entre différents utilisateurs visibles.
Guide d'intégration
Implémenter AbstractReceiverService
Pour recevoir le Payload
, l'application réceptrice DOIT implémenter les méthodes abstraites définies dans AbstractReceiverService
. Exemple :
public class MyReceiverService extends AbstractReceiverService {
@Override
public void onConnectionInitiated(@NonNull OccupantZoneInfo senderZone) {
}
@Override
public void onPayloadReceived(@NonNull OccupantZoneInfo senderZone,
@NonNull Payload payload) {
}
}
onConnectionInitiated()
est appelé lorsque le client d'envoi demande une connexion à ce client destinataire. Si une confirmation de l'utilisateur est nécessaire pour établir la connexion, MyReceiverService
peut remplacer cette méthode pour lancer une activité d'autorisation et appeler acceptConnection()
ou rejectConnection()
en fonction du résultat. Sinon, MyReceiverService
peut simplement appeler acceptConnection()
.
onPayloadReceived()is invoked when
MyReceiverServicehas received a
Payloadfrom the sender client.
MyReceiverService` peut remplacer cette méthode pour:
- Transmettez le
Payload
au ou aux points de terminaison de récepteur correspondants, le cas échéant. Pour obtenir les points de terminaison du récepteur enregistrés, appelezgetAllReceiverEndpoints()
. Pour transférer lePayload
vers un point de terminaison de récepteur donné, appelezforwardPayload()
.
OU
- Mettez en cache le
Payload
et envoyez-le lorsque le point de terminaison du destinataire attendu est enregistré, pour lequel leMyReceiverService
est informé viaonReceiverRegistered()
.
Déclarer AbstractReceiverService
L'application du récepteur DOIT déclarer le AbstractReceiverService
implémenté dans son fichier manifeste, ajouter un filtre d'intent avec l'action android.car.intent.action.RECEIVER_SERVICE
pour ce service et exiger l'autorisation android.car.occupantconnection.permission.BIND_RECEIVER_SERVICE
:
<service android:name=".MyReceiverService"
android:permission="android.car.occupantconnection.permission.BIND_RECEIVER_SERVICE"
android:exported="true">
<intent-filter>
<action android:name="android.car.intent.action.RECEIVER_SERVICE" />
</intent-filter>
</service>
L'autorisation android.car.occupantconnection.permission.BIND_RECEIVER_SERVICE
garantit que seul le framework peut se lier à ce service. Si ce service n'a pas besoin de cette autorisation, une autre application peut être en mesure de s'y associer et de lui envoyer directement un Payload
.
Déclarer une autorisation
L'application cliente DOIT déclarer les autorisations dans son fichier manifeste.
<!-- This permission is needed for connection API -->
<uses-permission android:name="android.car.permission.MANAGE_OCCUPANT_CONNECTION"/>
<!-- This permission is needed for discovery API -->
<uses-permission android:name="android.car.permission.MANAGE_REMOTE_DEVICE"/>
<!-- This permission is needed if the client app calls CarRemoteDeviceManager#setOccupantZonePower() -->
<uses-permission android:name="android.car.permission.CAR_POWER"/>
Chacune des trois autorisations ci-dessus est une autorisation privilégiée, qui DOIT être accordée au préalable par des fichiers de liste d'autorisation. Par exemple, voici le fichier de liste d'autorisation de l'application MultiDisplayTest
:
// packages/services/Car/data/etc/com.google.android.car.multidisplaytest.xml
<permissions>
<privapp-permissions package="com.google.android.car.multidisplaytest">
… …
<permission name="android.car.permission.MANAGE_OCCUPANT_CONNECTION"/>
<permission name="android.car.permission.MANAGE_REMOTE_DEVICE"/>
<permission name="android.car.permission.CAR_POWER"/>
</privapp-permissions>
</permissions>
Obtenir des gestionnaires de voitures
Pour utiliser l'API, l'application cliente DOIT enregistrer un CarServiceLifecycleListener
pour obtenir les gestionnaires de voitures associés:
private CarRemoteDeviceManager mRemoteDeviceManager;
private CarOccupantConnectionManager mOccupantConnectionManager;
private final Car.CarServiceLifecycleListener mCarServiceLifecycleListener = (car, ready) -> {
if (!ready) {
Log.w(TAG, "Car service crashed");
mRemoteDeviceManager = null;
mOccupantConnectionManager = null;
return;
}
mRemoteDeviceManager = car.getCarManager(CarRemoteDeviceManager.class);
mOccupantConnectionManager = car.getCarManager(CarOccupantConnectionManager.class);
};
Car.createCar(getContext(), /* handler= */ null, Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER,
mCarServiceLifecycleListener);
(Expéditeur) Discover
Avant de se connecter au client destinataire, le client expéditeur DOIT découvrir le client destinataire en enregistrant un CarRemoteDeviceManager.StateCallback
:
// The maps are accessed by the main thread only, so there is no multi-thread issue.
private final ArrayMap<OccupantZoneInfo, Integer> mOccupantZoneStateMap = new ArrayMap<>();
private final ArrayMap<OccupantZoneInfo, Integer> mAppStateMap = new ArrayMap<>();
private final StateCallback mStateCallback = new StateCallback() {
@Override
public void onOccupantZoneStateChanged(
@androidx.annotation.NonNull OccupantZoneInfo occupantZone,
int occupantZoneStates) {
mOccupantZoneStateMap.put(occupantZone, occupantZoneStates);
}
@Override
public void onAppStateChanged(
@androidx.annotation.NonNull OccupantZoneInfo occupantZone,
int appStates) {
mAppStateMap.put(occupantZone, appStates);
}
};
if (mRemoteDeviceManager != null) {
mRemoteDeviceManager.registerStateCallback(getActivity().getMainExecutor(),
mStateCallback);
}
Avant de demander une connexion au récepteur, l'expéditeur DOIT s'assurer que tous les indicateurs de la zone d'occupant du récepteur et de l'application du récepteur sont définis. Sinon, des erreurs peuvent se produire. Exemple :
private boolean canRequestConnectionToReceiver(OccupantZoneInfo receiverZone) {
Integer zoneState = mOccupantZoneStateMap.get(receiverZone);
if ((zoneState == null) || (zoneState.intValue() & (FLAG_OCCUPANT_ZONE_POWER_ON
// FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED is not implemented yet. Right now
// just ignore this flag.
// | FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED
| FLAG_OCCUPANT_ZONE_CONNECTION_READY)) == 0) {
return false;
}
Integer appState = mAppStateMap.get(receiverZone);
if ((appState == null) ||
(appState.intValue() & (FLAG_CLIENT_INSTALLED
| FLAG_CLIENT_SAME_LONG_VERSION | FLAG_CLIENT_SAME_SIGNATURE
| FLAG_CLIENT_RUNNING | FLAG_CLIENT_IN_FOREGROUND)) == 0) {
return false;
}
return true;
}
Nous recommandons à l'expéditeur de ne demander une connexion au destinataire que lorsque tous les indicateurs du destinataire sont définis. Toutefois, il existe des exceptions:
FLAG_OCCUPANT_ZONE_CONNECTION_READY
etFLAG_CLIENT_INSTALLED
sont les conditions minimales requises pour établir une connexion.Si l'application destinataire doit afficher une UI pour obtenir l'approbation de la connexion par l'utilisateur,
FLAG_OCCUPANT_ZONE_POWER_ON
etFLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED
deviennent des exigences supplémentaires. Pour une meilleure expérience utilisateur,FLAG_CLIENT_RUNNING
etFLAG_CLIENT_IN_FOREGROUND
sont également recommandés, sinon l'utilisateur pourrait être surpris.Pour le moment (Android 15),
FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED
n'est pas implémenté. L'application cliente peut simplement l'ignorer.Pour le moment (Android 15), l'API Comms n'est compatible qu'avec plusieurs utilisateurs sur la même instance Android afin que les applications homologues puissent avoir le même code de version long (
FLAG_CLIENT_SAME_LONG_VERSION
) et la même signature (FLAG_CLIENT_SAME_SIGNATURE
). Par conséquent, les applications n'ont pas besoin de vérifier que les deux valeurs sont identiques.
Pour une meilleure expérience utilisateur, le client d'envoi peut afficher une UI si un indicateur n'est pas défini. Par exemple, si FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED
n'est pas défini, l'expéditeur peut afficher un toast ou une boîte de dialogue pour inviter l'utilisateur à déverrouiller l'écran de la zone d'occupant du destinataire.
Lorsque l'expéditeur n'a plus besoin de découvrir les récepteurs (par exemple, lorsqu'il trouve tous les récepteurs et les connexions établies ou devient inactif), il PEUT arrêter la découverte.
if (mRemoteDeviceManager != null) {
mRemoteDeviceManager.unregisterStateCallback();
}
Lorsque la découverte est arrêtée, les connexions existantes ne sont pas affectées. L'expéditeur peut continuer à envoyer des Payload
aux récepteurs connectés.
(Expéditeur) Demander une connexion
Lorsque tous les indicateurs du récepteur sont définis, l'expéditeur PEUT demander une connexion au récepteur:
private final ConnectionRequestCallback mRequestCallback = new ConnectionRequestCallback() {
@Override
public void onConnected(OccupantZoneInfo receiverZone) {
}
@Override
public void onFailed(OccupantZoneInfo receiverZone, int connectionError) {
}
@Override
public void onDisconnected(OccupantZoneInfo receiverZone) {
}
};
if (mOccupantConnectionManager != null && canRequestConnectionToReceiver(receiverZone)) {
mOccupantConnectionManager.requestConnection(receiverZone,
getActivity().getMainExecutor(), mRequestCallback);
}
(Service destinataire) Accepter la connexion
Une fois que l'expéditeur demande une connexion au destinataire, AbstractReceiverService
dans l'application du destinataire est lié par le service de voiture, et AbstractReceiverService.onConnectionInitiated()
est appelé. Comme expliqué dans la section (Sender) Request Connection (Demande de connexion (émetteur)), onConnectionInitiated()
est une méthode abstraite et DOIT être implémentée par l'application cliente.
Lorsque le destinataire accepte la requête de connexion, l'ConnectionRequestCallback.onConnected()
de l'expéditeur est appelé, puis la connexion est établie.
(Expéditeur) Envoyer la charge utile
Une fois la connexion établie, l'expéditeur PEUT envoyer Payload
au destinataire:
if (mOccupantConnectionManager != null) {
Payload payload = ...;
try {
mOccupantConnectionManager.sendPayload(receiverZone, payload);
} catch (CarOccupantConnectionManager.PayloadTransferException e) {
Log.e(TAG, "Failed to send Payload to " + receiverZone);
}
}
L'expéditeur peut placer un objet Binder
ou un tableau d'octets dans le Payload
. Si l'expéditeur doit envoyer d'autres types de données, il DOIT sérialiser les données dans un tableau d'octets, utiliser le tableau d'octets pour créer un objet Payload
et envoyer l'Payload
. Le client destinataire obtient ensuite le tableau d'octets de l'Payload
reçu et le désérialise dans l'objet de données attendu.
Par exemple, si l'expéditeur souhaite envoyer une chaîne hello
au point de terminaison du destinataire avec l'ID FragmentB
, il peut utiliser Proto Buffers pour définir un type de données comme suit:
message MyData {
required string receiver_endpoint_id = 1;
required string data = 2;
}
La figure 1 illustre le flux Payload
:
(Service de réception) Recevoir et distribuer la charge utile
Une fois que l'application destinataire reçoit le Payload
, son AbstractReceiverService.onPayloadReceived()
est appelé. Comme expliqué dans la section Envoyer la charge utile, onPayloadReceived()
est une méthode abstraite et DOIT être implémentée par l'application cliente. Dans cette méthode, le client PEUT transférer le Payload
vers le ou les points de terminaison de récepteur correspondants, ou mettre en cache le Payload
, puis l'envoyer une fois le point de terminaison de récepteur attendu enregistré.
(Point de terminaison du récepteur) Enregistrement et désenregistrement
L'application du récepteur DOIT appeler registerReceiver()
pour enregistrer les points de terminaison du récepteur. Un cas d'utilisation typique est qu'un fragment doit recevoir Payload
. Il enregistre donc un point de terminaison du récepteur:
private final PayloadCallback mPayloadCallback = (senderZone, payload) -> {
…
};
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.registerReceiver("FragmentB",
getActivity().getMainExecutor(), mPayloadCallback);
}
Une fois que le AbstractReceiverService
du client destinataire a distribué le Payload
au point de terminaison du destinataire, le PayloadCallback
associé est appelé.
L'application cliente PEUT enregistrer plusieurs points de terminaison de récepteur tant que leurs receiverEndpointId
sont uniques dans l'application cliente. Le receiverEndpointId
sera utilisé par le AbstractReceiverService
pour déterminer le ou les points de terminaison de récepteur auxquels distribuer la charge utile. Exemple :
- L'expéditeur spécifie
receiver_endpoint_id:FragmentB
dans lePayload
. Lors de la réception dePayload
,AbstractReceiverService
dans le récepteur appelleforwardPayload("FragmentB", payload)
pour distribuer la charge utile àFragmentB
. - L'expéditeur spécifie
data_type:VOLUME_CONTROL
dans lePayload
. Lors de la réception de l'Payload
, leAbstractReceiverService
du récepteur sait que ce type dePayload
doit être distribué àFragmentB
. Il appelle doncforwardPayload("FragmentB", payload)
.
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.unregisterReceiver("FragmentB");
}
(Expéditeur) Mettre fin à la connexion
Une fois que l'expéditeur n'a plus besoin d'envoyer Payload
au destinataire (par exemple, s'il devient inactif), il DOIT mettre fin à la connexion.
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.disconnect(receiverZone);
}
Une fois déconnecté, l'expéditeur ne peut plus envoyer de Payload
au destinataire.
Flux de connexion
Un flux de connexion est illustré dans la figure 2.
Dépannage
Vérifier les journaux
Pour vérifier les journaux correspondants:
Exécutez cette commande pour la journalisation:
adb shell setprop log.tag.CarRemoteDeviceService VERBOSE && adb shell setprop log.tag.CarOccupantConnectionService VERBOSE && adb logcat -s "AbstractReceiverService","CarOccupantConnectionManager","CarRemoteDeviceManager","CarRemoteDeviceService","CarOccupantConnectionService"
Pour vider l'état interne de
CarRemoteDeviceService
etCarOccupantConnectionService
:adb shell dumpsys car_service --services CarRemoteDeviceService && adb shell dumpsys car_service --services CarOccupantConnectionService
Null CarRemoteDeviceManager et CarOccupantConnectionManager
Voici les causes possibles:
Le service de voiture a planté. Comme illustré précédemment, les deux gestionnaires sont intentionnellement réinitialisés sur
null
lorsque le service de voiture plante. Lorsque le service de voiture est redémarré, les deux gestionnaires sont définis sur des valeurs non nulles.CarRemoteDeviceService
ouCarOccupantConnectionService
n'est pas activé. Pour déterminer si l'une ou l'autre est activée, exécutez:adb shell dumpsys car_service --services CarFeatureController
Recherchez
mDefaultEnabledFeaturesFromConfig
, qui doit contenircar_remote_device_service
etcar_occupant_connection_service
. Exemple :mDefaultEnabledFeaturesFromConfig:[car_evs_service, car_navigation_service, car_occupant_connection_service, car_remote_device_service, car_telemetry_service, cluster_home_service, com.android.car.user.CarUserNoticeService, diagnostic, storage_monitoring, vehicle_map_service]
Par défaut, ces deux services sont désactivés. Lorsqu'un appareil est compatible avec l'affichage multiple, vous DEVEZ superposer ce fichier de configuration. Vous pouvez activer les deux services dans un fichier de configuration:
// packages/services/Car/service/res/values/config.xml <string-array translatable="false" name="config_allowed_optional_car_features"> <item>car_occupant_connection_service</item> <item>car_remote_device_service</item> … … </string-array>
Exception lors de l'appel de l'API
Si l'application cliente n'utilise pas l'API comme prévu, une exception peut se produire. Dans ce cas, l'application cliente peut vérifier le message dans l'exception et la pile de plantage pour résoudre le problème. Voici quelques exemples d'utilisation abusive de l'API:
registerStateCallback()
Ce client a déjà enregistré unStateCallback
.unregisterStateCallback()
AucunStateCallback
n'a été enregistré par cette instanceCarRemoteDeviceManager
.registerReceiver()
receiverEndpointId
est déjà enregistré.unregisterReceiver()
receiverEndpointId
n'est pas enregistré.requestConnection()
Une connexion en attente ou établie existe déjà.cancelConnection()
Aucune connexion en attente à annuler.sendPayload()
Aucune connexion établie.disconnect()
Aucune connexion établie.
Le client 1 peut envoyer la charge utile au client 2, mais pas l'inverse
La connexion est à sens unique par conception. Pour établir une connexion bidirectionnelle, client1
et client2
DOIVENT demander une connexion l'un à l'autre, puis obtenir l'approbation.