API Multi-Display Communications

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, appelez getAllReceiverEndpoints(). Pour transférer le Payload vers un point de terminaison de récepteur donné, appelez forwardPayload().

OU

  • Mettre en cache le Payload et l'envoyer lorsque le point de terminaison du destinataire attendu est enregistré, pour lequel le MyReceiverService est averti via onReceiverRegistered()

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 et FLAG_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 et FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED deviennent des exigences supplémentaires. Pour une meilleure expérience utilisateur, FLAG_CLIENT_RUNNING et FLAG_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 :

Envoyer la charge utile

Figure 1. Envoyez la charge utile.

(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 dans Payload. Lors de la réception de Payload, le AbstractReceiverService du récepteur appelle forwardPayload("FragmentB", payload) pour distribuer la charge utile à FragmentB.
  • L'expéditeur spécifie data_type:VOLUME_CONTROL dans Payload. Lors de la réception du Payload, le AbstractReceiverService du récepteur sait que ce type de Payload doit être distribué à FragmentB. Il appelle donc forwardPayload("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.

Flux de connexion

Figure 2. Flux de connexion.

Dépannage

Vérifier les journaux

Pour vérifier les journaux correspondants :

  1. 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"
  2. Pour vider l'état interne de CarRemoteDeviceService et CarOccupantConnectionService :

    adb shell dumpsys car_service --services CarRemoteDeviceService && adb shell dumpsys car_service --services CarOccupantConnectionService

Null CarRemoteDeviceManager et CarOccupantConnectionManager

Voici quelques causes possibles :

  1. 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.

  2. CarRemoteDeviceService ou CarOccupantConnectionService 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 contenir car_remote_device_service et car_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é un StateCallback.
  • unregisterStateCallback() Aucun StateCallback n'a été enregistré par cette instance CarRemoteDeviceManager.
  • 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.