API Multi-Display Communications

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

OU

  • Mettez en cache le Payload et envoyez-le lorsque le point de terminaison du destinataire attendu est enregistré, pour lequel le MyReceiverService est informé via onReceiverRegistered().

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 et FLAG_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 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 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:

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 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 le Payload. Lors de la réception de Payload, AbstractReceiverService dans le récepteur appelle forwardPayload("FragmentB", payload) pour distribuer la charge utile à FragmentB.
  • L'expéditeur spécifie data_type:VOLUME_CONTROL dans le Payload. Lors de la réception de l'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 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.

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 les 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 de voiture plante. Lorsque le service de voiture 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'une ou l'autre est activée, exécutez:

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

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.