API Multi-Display Communications

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

OU

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

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

Envoyer la charge utile

Figure 1. Envoyez la charge utile.

(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 champ Payload. Quand ? recevoir le Payload, le AbstractReceiverService dans les appels du destinataire forwardPayload("FragmentB", payload) pour envoyer la charge utile FragmentB
  • L'expéditeur indique data_type:VOLUME_CONTROL dans le champ Payload. Quand ? reçoit le Payload, le AbstractReceiverService du récepteur connaît que ce type de Payload doit être envoyé à FragmentB. Il appelle donc forwardPayload("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.

Flux de connexion

Figure 2. Flux de connexion.

Dépannage

Vérifier les journaux

Pour vérifier les journaux correspondants:

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

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

CarRemoteDeviceManager et CarOccupantConnectionManager Null

Vérifiez les causes possibles suivantes:

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

  2. CarRemoteDeviceService ou CarOccupantConnectionService 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 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 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é un StateCallback.
  • unregisterStateCallback() Aucun StateCallback n'a été enregistré par cet 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 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.