L'API Multi-Display Communications peut être utilisée par une application système privilégiée dans AAOS pour communiquer avec la même application (même nom de package) s'exécutant dans une autre zone de l'occupant d'une voiture. Cette page explique comment intégrer l'API. Pour en savoir plus, vous pouvez également consulter CarOccupantZoneManager.OccupantZoneInfo.
Zone des occupants
Le concept de zone d'occupant permet de mapper 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 possè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 et tous les utilisateurs s'exécutent sur le même SoC. L'API Comms se compose de trois éléments :
L'API de gestion de l'alimentation permet au client de gérer l'alimentation des écrans dans les zones des occupants.
L'API Discovery permet au client de surveiller l'état des autres zones de l'occupant dans la voiture, ainsi que 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'occupation et d'envoyer une charge utile au client homologue.
Les API Discovery et Connection sont requises pour la connexion. L'API de gestion de l'alimentation est facultative.
L'API Comms n'est pas compatible avec la communication entre différentes applications. Au lieu de cela, 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 émetteur demande une connexion à ce client récepteur. 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()
est appelé lorsque MyReceiverService
a reçu un Payload
du client émetteur. MyReceiverService
can remplacer cette méthode pour :
- Transmettez le
Payload
au ou aux points de terminaison du 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
- Mettre en cache le
Payload
et l'envoyer lorsque le point de terminaison du destinataire attendu est enregistré, pour lequel leMyReceiverService
est averti viaonReceiverRegistered()
Déclarer AbstractReceiverService
L'application réceptrice DOIT déclarer l'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 être lié à ce service. Si ce service ne nécessite pas l'autorisation, une autre application peut être en mesure de se lier à ce service 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. Voici par exemple 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 voiture
Pour utiliser l'API, l'application cliente DOIT enregistrer un CarServiceLifecycleListener
pour obtenir les gestionnaires de voiture 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) Découverte
Avant de se connecter au client récepteur, le client émetteur DOIT découvrir le client récepteur 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'émetteur DOIT s'assurer que tous les indicateurs de la zone d'occupation 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'émetteur de demander une connexion au récepteur uniquement lorsque tous les indicateurs du récepteur sont définis. Cela dit, il existe des exceptions :
FLAG_OCCUPANT_ZONE_CONNECTION_READY
etFLAG_CLIENT_INSTALLED
sont les exigences minimales requises pour établir une connexion.Si l'application réceptrice doit afficher une UI pour obtenir l'approbation de l'utilisateur pour la connexion,
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 l'instant (Android 15), l'API Comms n'est compatible qu'avec plusieurs utilisateurs sur la même instance Android. Les applications homologues peuvent ainsi avoir le même code de version longue (
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 correspondent.
Pour une meilleure expérience utilisateur, le client de l'expéditeur PEUT afficher une UI si aucun indicateur n'est 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 récepteur.
Lorsque l'expéditeur n'a plus besoin de découvrir les récepteurs (par exemple, lorsqu'il trouve tous les récepteurs et établit des connexions 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 Payload
aux récepteurs connectés.
(Expéditeur) Demander une connexion
Lorsque tous les indicateurs du récepteur sont définis, l'émetteur 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 récepteur) Accepter la connexion
Une fois que l'expéditeur a demandé une connexion au destinataire, le AbstractReceiverService
dans l'application du destinataire sera lié au service automobile, et AbstractReceiverService.onConnectionInitiated()
sera appelé. Comme expliqué dans (Émetteur) Demande de connexion, onConnectionInitiated()
est une méthode abstraite qui DOIT être implémentée par l'application cliente.
Lorsque le destinataire accepte la demande de connexion, le 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 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 construire un objet Payload
et envoyer le Payload
. Le client récepteur obtient ensuite le tableau d'octets à partir du 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 des tampons de protocole 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 réceptrice reçoit le Payload
, son AbstractReceiverService.onPayloadReceived()
est appelé. Comme expliqué dans la section Envoyer la charge utile, onPayloadReceived()
est une méthode abstraite qui DOIT être implémentée par l'application cliente. Dans cette méthode, le client PEUT transférer Payload
aux points de terminaison du récepteur correspondants ou mettre en cache Payload
, puis l'envoyer une fois que le point de terminaison du récepteur attendu est enregistré.
(Point de terminaison du récepteur) Enregistrer et annuler l'enregistrement
L'application réceptrice DOIT appeler registerReceiver()
pour enregistrer les points de terminaison du récepteur. Dans un cas d'utilisation typique, 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
dans le client récepteur distribue le Payload
au point de terminaison du récepteur, le PayloadCallback
associé est appelé.
L'application cliente PEUT enregistrer plusieurs points de terminaison de récepteur à condition que leurs receiverEndpointId
soient uniques dans l'application cliente. Le receiverEndpointId
sera utilisé par AbstractReceiverService
pour déterminer à quel(s) point(s) de terminaison de récepteur envoyer la charge utile. Exemple :
- L'expéditeur spécifie
receiver_endpoint_id:FragmentB
dansPayload
. Lors de la réception dePayload
, leAbstractReceiverService
du récepteur appelleforwardPayload("FragmentB", payload)
pour distribuer la charge utile àFragmentB
. - L'expéditeur spécifie
data_type:VOLUME_CONTROL
dansPayload
. Lors de la réception duPayload
, 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 la déconnexion effectuée, 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 quelques 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 automobile plante. Lorsque le service automobile 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'un ou l'autre est activé, exécutez la commande suivante :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 plusieurs écrans, 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
Une exception peut se produire si l'application cliente n'utilise pas l'API comme prévu. 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.
Client1 peut envoyer une charge utile à client2, mais pas l'inverse.
La connexion est unidirectionnelle par conception. Pour établir une connexion bidirectionnelle, client1
et client2
DOIVENT demander une connexion l'un à l'autre, puis obtenir l'approbation.