L'API Multi-Display Communications peut être utilisée par une application privilégiée du système AAOS pour communiquer avec la même application (même nom de package) exécutée dans un dans une voiture. Cette page explique comment intégrer l'API. Pour apprendre plus, vous pouvez également voir CarOccupantZoneManager.OccupantZoneInfo :
Zone de l'occupant
Le concept de zone de l'occupant permet de mapper un utilisateur à un ensemble d'écrans. Chaque la zone de l'occupant est dotée d'un écran dont le type DISPLAY_TYPE_MAIN Une zone d'occupation peut également disposer d'écrans supplémentaires, tels qu'un écran de cluster. Chaque zone de l'occupant se voit attribuer un utilisateur Android. Chaque utilisateur possède son propre compte et des applications.
Configuration matérielle
L'API Comms n'accepte qu'un seul SoC. Dans le modèle SoC unique, tous les occupants et les utilisateurs s'exécutent sur le même SoC. L'API Comms comprend trois composants:
L'API de gestion de l'alimentation permet au client de gérer la puissance du s'affiche dans les zones de l'occupant.
L'API Discovery permet au client de surveiller les états des autres occupants. dans la voiture et surveiller les clients pairs dans ces zones de l'occupant. Utilisez l'API Discovery avant d'utiliser l'API Connection.
L'API Connection permet au client de se connecter à son client pair dans une autre zone d'occupant et d'envoyer une charge utile au client pair.
Les API Discovery et Connection sont requises pour la connexion. Le pouvoir est facultative.
L'API Comms ne permet pas la communication entre différentes applications. À la place, il est conçu uniquement pour la communication entre des 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 Payload
, l'application réceptrice DOIT implémenter les méthodes abstraites
défini 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 expéditeur demande une
à ce client récepteur. Si la confirmation de l'utilisateur
est nécessaire pour établir
la connexion, MyReceiverService
peut remplacer cette méthode pour lancer un
l'activité d'autorisation, et appelez acceptConnection()
ou rejectConnection()
en fonction
sur le résultat. Sinon, MyReceiverService
peut simplement appeler
acceptConnection()
.
onPayloadReceived()is invoked when
MyReceiverServicehas received a
Payloadfrom the sender client.
MyReceiverService` peut remplacer cela
pour:
- Transférez le
Payload
au(x) point(s) de terminaison de réception correspondant, le cas échéant. À obtenez les points de terminaison du récepteur enregistrés, appelezgetAllReceiverEndpoints()
. À transférer lePayload
à un point de terminaison de récepteur donné, appelerforwardPayload()
OU
- Mettez en cache le
Payload
et envoyez-le lorsque le point de terminaison du récepteur attendu est enregistré, pour lequelMyReceiverService
est notifié viaonReceiverRegistered()
Déclarer AbstractReceiverService
L'application réceptrice DOIT déclarer le AbstractReceiverService
implémenté dans son
fichier manifeste, ajoutez un filtre d'intent avec action
android.car.intent.action.RECEIVER_SERVICE
pour ce service et nécessitent
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 pourrait s'y associer
service et 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 permissions ci-dessus est des autorisations privilégiées, qui DOIVENT être
pré-accordé par les fichiers de la liste d'autorisation. Par exemple, voici le fichier d'autorisation
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);
Discover (Expéditeur)
Avant de se connecter au client récepteur, le client émetteur DOIT découvrir
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 destinataire, l'expéditeur DOIT s'assurer que tous les indicateurs de la zone d'occupation 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 toutes les indicateurs du récepteur sont définis. Il existe toutefois quelques exceptions:
FLAG_OCCUPANT_ZONE_CONNECTION_READY
etFLAG_CLIENT_INSTALLED
sont les la configuration minimale requise pour établir une connexion.Si l'application réceptrice doit afficher une interface utilisateur pour obtenir l'approbation de l'utilisateur connexion,
FLAG_OCCUPANT_ZONE_POWER_ON
etFLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED
deviennent des exigences supplémentaires. Pour une une meilleure expérience utilisateur,FLAG_CLIENT_RUNNING
etFLAG_CLIENT_IN_FOREGROUND
sont également recommandées, sinon l'utilisateur pourrait être surpris.Pour l'instant (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 ne prend en charge que plusieurs utilisateurs sur le même Instance Android afin que les applications similaires puissent avoir le même code de version long (
FLAG_CLIENT_SAME_LONG_VERSION
) et signature (FLAG_CLIENT_SAME_SIGNATURE
). Par conséquent, les applications n'ont pas besoin de vérifier que deux valeurs s'accordent.
Pour une meilleure expérience utilisateur, le client expéditeur 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 l'occupant du récepteur.
Lorsque l'expéditeur n'a plus besoin de découvrir les destinataires (par exemple, lorsqu'il trouve tous les récepteurs et les connexions établies ou devient inactif), il PEUT d'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 destinataire sont définis, l'expéditeur PEUT demander une connexion au destinataire:
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 du récepteur) Accepter la connexion
Une fois que l'expéditeur demande à se connecter au destinataire, le
AbstractReceiverService
dans l'application réceptrice sera lié au service de voiture,
et AbstractReceiverService.onConnectionInitiated()
est appelé. En tant que
expliqué dans l'article (Sender) Request Connection (Connexion de requête de l'expéditeur),
onConnectionInitiated()
est une méthode abstraite qui DOIT être implémentée par la
l'application cliente.
Lorsque le destinataire accepte la demande de connexion,
ConnectionRequestCallback.onConnected()
est appelée, 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 le
l'émetteur doit envoyer d'autres types de données, il DOIT sérialiser les données dans un octet
utilisez le tableau d'octets pour construire un objet Payload
, puis envoyez le
Payload
Ensuite, le client récepteur obtient le tableau d'octets à partir de l'objet
Payload
, et désérialise le tableau d'octets dans l'objet de données attendu.
Par exemple, si l'expéditeur souhaite envoyer une chaîne hello
au destinataire
point de terminaison avec l'ID FragmentB
, il peut utiliser Proto Buffers pour définir un type de données
comme ceci:
message MyData {
required string receiver_endpoint_id = 1;
required string data = 2;
}
La Figure 1 illustre le flux Payload
:
(Service du récepteur) Recevoir et distribuer la charge utile
Une fois que l'application réceptrice reçoit l'Payload
, son
AbstractReceiverService.onPayloadReceived()
sera appelé. Comme expliqué dans
l'option Send the payload (Envoyer la charge utile), onPayloadReceived()
est un
méthode abstraite et DOIT être implémentée par l'application cliente. Dans cette méthode,
le client PEUT transférer le Payload
au(x) point(s) de terminaison de réception correspondant ; ou
mettre en cache le Payload
, puis l'envoyer une fois que le point de terminaison du récepteur attendu est
enregistré.
(Point de terminaison du destinataire) Enregistrer et annuler l'enregistrement
L'application du récepteur DOIT appeler registerReceiver()
pour l'enregistrer
les points de terminaison. Un cas d'utilisation typique est qu'un fragment doit recevoir Payload
.
il enregistre un point de terminaison récepteur:
private final PayloadCallback mPayloadCallback = (senderZone, payload) -> {
…
};
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.registerReceiver("FragmentB",
getActivity().getMainExecutor(), mPayloadCallback);
}
Une fois que AbstractReceiverService
dans le client récepteur envoie l'événement
Payload
au point de terminaison du récepteur, le PayloadCallback
associé sera
invoquée.
L'application cliente peut enregistrer plusieurs points de terminaison récepteurs à condition que leurs
Les éléments receiverEndpointId
sont uniques dans l'application cliente. receiverEndpointId
sera utilisé par AbstractReceiverService
pour déterminer le destinataire
vers lesquels envoyer la charge utile. Exemple :
- L'expéditeur indique
receiver_endpoint_id:FragmentB
dans le champPayload
. Quand ? recevoir lePayload
, leAbstractReceiverService
dans les appels du destinataireforwardPayload("FragmentB", payload)
pour envoyer la charge utileFragmentB
- L'expéditeur indique
data_type:VOLUME_CONTROL
dans le champPayload
. Quand ? reçoit lePayload
, leAbstractReceiverService
du récepteur connaît que ce type dePayload
doit être envoyé àFragmentB
. Il appelle doncforwardPayload("FragmentB", payload)
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.unregisterReceiver("FragmentB");
}
(Expéditeur) Arrêter la connexion
Une fois que l'expéditeur n'a plus besoin d'envoyer de 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 la commande suivante 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
CarRemoteDeviceManager et CarOccupantConnectionManager Null
Vérifiez les causes possibles suivantes:
L'entretien automobile a planté. Comme illustré précédemment, les deux gestionnaires intentionnellement réinitialisés sur
null
lorsque le service automobile plante. Quand l'entretien automobile redémarre, les deux gestionnaires sont définis avec des valeurs non nulles.CarRemoteDeviceService
ouCarOccupantConnectionService
n'est pas est activé. Pour déterminer si l'une ou l'autre est activée, 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 multi-écran, vous DEVEZ superposer ce fichier de configuration. Vous pouvez activer les deux services d'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 de 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 cetCarRemoteDeviceManager
.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 la charge utile au client2, mais pas l'inverse
La connexion est un moyen par nature. Pour établir une connexion bidirectionnelle, les deux
client1
et client2
DOIVENT demander une connexion, puis
obtenir l'approbation.