The Multi-Display Communications API can be used by a system privileged app in AAOS to communicate with the same app (same package name) running in a different occupant zone in a car. This page describes how to integrate the API. To learn more, you can also see CarOccupantZoneManager.OccupantZoneInfo.
Occupant zone
The concept of an occupant zone maps a user to a set of displays. Each occupant zone has a display with the type DISPLAY_TYPE_MAIN. An occupant zone may also have additional displays, such as a cluster display. Each occupant zone is assigned an Android user. Each user has their own accounts and apps.
Hardware configuration
The Comms API supports only a single SoC. In the single SoC model, all occupant zones and users run on the same SoC. The Comms API consists of three components:
Power management API allows the client to manage the power of the displays in the occupant zones.
Discovery API allows the client to monitor the states of other occupant zones in the car, and to monitor peer clients in those occupant zones. Use the Discovery API before using the Connection API.
Connection API allows the client to connect to its peer client in another occupant zone and to send a payload to the peer client.
The Discovery API and the Connection API are required for connection. The Power management API is optional.
The Comms API doesn't support communication between different apps. Instead, it's designed only for communication between apps with the same package name and used only for communication between different visible users.
Integration guide
Implement AbstractReceiverService
To receive the Payload, the receiver app MUST implement the abstract methods
defined in AbstractReceiverService. For example:
public class MyReceiverService extends AbstractReceiverService {
@Override
public void onConnectionInitiated(@NonNull OccupantZoneInfo senderZone) {
}
@Override
public void onPayloadReceived(@NonNull OccupantZoneInfo senderZone,
@NonNull Payload payload) {
}
}
onConnectionInitiated() is invoked when the sender client requests a
connection to this receiver client. If user confirmation is needed to establish
the connection, MyReceiverService can override this method to launch a
permission activity, and call acceptConnection() or rejectConnection() based
on the result. Otherwise, MyReceiverService can just call
acceptConnection().
onPayloadReceived() is invoked when MyReceiverService has received a
Payload from the sender client. MyReceiverService can override this
method to:
- Forward the
Payloadto the corresponding receiver endpoint(s), if any. To get the registered receiver endpoints, callgetAllReceiverEndpoints(). To forward thePayloadto a given receiver endpoint, callforwardPayload()
OR,
- Cache the
Payload, and send it when the expected receiver endpoint is registered, for which theMyReceiverServiceis notified throughonReceiverRegistered()
Declare AbstractReceiverService
The receiver app MUST declare the implemented AbstractReceiverService in its
manifest file, add an intent filter with action
android.car.intent.action.RECEIVER_SERVICE for this service, and require the
android.car.occupantconnection.permission.BIND_RECEIVER_SERVICE permission:
<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>
The android.car.occupantconnection.permission.BIND_RECEIVER_SERVICEpermission
ensures that only the framework can bind to this service. If this service
doesn't require the permission, a different app might be able to bind to this
service and send a Payload to it directly.
Declare permission
The client app MUST declare the permissions in its manifest file.
<!-- 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"/>
Each of the three permissions above are privileged permissions, which MUST be
pre-granted by allowlist files. For example, here is the allowlist file of
MultiDisplayTest app:
// 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>
Get Car managers
To use the API, the client app MUST register a CarServiceLifecycleListener to
get the associated Car managers:
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);
(Sender) Discover
Before connecting to the receiver client, the sender client SHOULD discover the
receiver client by registering a 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);
}
Before requesting a connection to the receiver, the sender SHOULD make sure all the flags of the receiver occupant zone and receiver app are set. Otherwise, errors can occur. For example:
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;
}
We recommend the sender request a connection to the receiver only when all the flags of the receiver are set. That said, there are exceptions:
FLAG_OCCUPANT_ZONE_CONNECTION_READYandFLAG_CLIENT_INSTALLEDare the minimum requirements needed to establish a connection.If the receiver app needs to displays a UI to get user approval of the connection,
FLAG_OCCUPANT_ZONE_POWER_ONandFLAG_OCCUPANT_ZONE_SCREEN_UNLOCKEDbecome additional requirements. For a better user experience,FLAG_CLIENT_RUNNINGandFLAG_CLIENT_IN_FOREGROUNDare also recommended, otherwise the user might be surprised.For now (Android 15),
FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKEDisn't implemented. The client app can just ignore it.For now (Android 15), the Comms API only supports multiple users on the same Android instance so that peer apps can have the same long version code (
FLAG_CLIENT_SAME_LONG_VERSION) and signature (FLAG_CLIENT_SAME_SIGNATURE). As a result, apps needn't verify that the two values agree.
For a better user experience, the sender client CAN show a UI if a flag is not
set. For example, if FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED is not set, the sender
can show a toast or a dialog to prompt the user to unlock the screen of the
receiver occupant zone.
When the sender no longer needs to discover the receivers (for example, when it finds all receivers and established connections or becomes inactive), it CAN stop the discovery.
if (mRemoteDeviceManager != null) {
mRemoteDeviceManager.unregisterStateCallback();
}
When discovery is stopped, existing connections aren't affected. The sender can
continue to send Payload to the connected receivers.
(Sender) Request connection
When all flags of the receiver are set, the sender CAN request a connection to the receiver:
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);
}
(Receiver service) Accept the connection
Once the sender requests a connection to the receiver, the
AbstractReceiverService in the receiver app will be bound by the car service,
and AbstractReceiverService.onConnectionInitiated() will be invoked. As
explained in the (Sender) Request Connection,
onConnectionInitiated() is an abstracted method and MUST be implemented by the
client app.
When the receiver accepts the connection request, the sender's
ConnectionRequestCallback.onConnected() will be invoked, then the connection
is established.
(Sender) Send the payload
Once the connection is established, the sender CAN send Payload to the
receiver:
if (mOccupantConnectionManager != null) {
Payload payload = ...;
try {
mOccupantConnectionManager.sendPayload(receiverZone, payload);
} catch (CarOccupantConnectionManager.PayloadTransferException e) {
Log.e(TAG, "Failed to send Payload to " + receiverZone);
}
}
The sender can put a Binder object, or a byte array in the Payload. If the
sender needs to send other data types, it MUST serialize the data into a byte
array, use the byte array to construct a Payload object, and send the
Payload. Then the receiver client gets the byte array from the received
Payload, and deserializes the byte array into the expected data object.
For example, if the sender wants to send a String hello to the receiver
endpoint with ID FragmentB, it can use Proto Buffers to define a data type
like this:
message MyData {
required string receiver_endpoint_id = 1;
required string data = 2;
}
Figure 1 illustrates the Payload flow:
(Receiver service) Receive and dispatch the payload
Once the receiver app receives the Payload, its
AbstractReceiverService.onPayloadReceived() will be invoked. As explained in
the Send the payload, onPayloadReceived() is an
abstracted method and MUST be implemented by the client app. In this method, the
client CAN forward the Payload to the corresponding receiver endpoint(s), or
cache the Payload then send it once the expected receiver endpoint is
registered.
(Receiver endpoint) Register and unregister
The receiver app SHOULD call registerReceiver() to register the receiver
endpoints. A typical use case is that a Fragment needs to receiver Payload, so
it registers a receiver endpoint:
private final PayloadCallback mPayloadCallback = (senderZone, payload) -> {
…
};
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.registerReceiver("FragmentB",
getActivity().getMainExecutor(), mPayloadCallback);
}
Once the AbstractReceiverService in the receiver client dispatches the
Payload to the receiver endpoint, the associated PayloadCallback will be
invoked.
The client app CAN register multiple receiver endpoints as long as their
receiverEndpointIds are unique among the client app. The receiverEndpointId
will be used by the AbstractReceiverService to decide which receiver
endpoint(s) to dispatch the Payload to. For example:
- The sender specifies
receiver_endpoint_id:FragmentBin thePayload. When receiving thePayload, theAbstractReceiverServicein the receiver callsforwardPayload("FragmentB", payload)to dispatch the Payload toFragmentB - The sender specifies
data_type:VOLUME_CONTROLin thePayload. When receiving thePayload, theAbstractReceiverServicein the receiver knows that this type ofPayloadshould be dispatched toFragmentB, so it callsforwardPayload("FragmentB", payload)
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.unregisterReceiver("FragmentB");
}
(Sender) Terminate the connection
Once the sender no longer needs to send Payload to the receiver (for example,
it becomes inactive), it SHOULD terminate the connection.
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.disconnect(receiverZone);
}
Once disconnected, the sender can no longer send Payload to the receiver.
Connection flow
A connection flow is illustrated in Figure 2.
Troubleshooting
Check the logs
To check the corresponding logs:
Run this command for logging:
adb shell setprop log.tag.CarRemoteDeviceService VERBOSE && adb shell setprop log.tag.CarOccupantConnectionService VERBOSE && adb logcat -s "AbstractReceiverService","CarOccupantConnectionManager","CarRemoteDeviceManager","CarRemoteDeviceService","CarOccupantConnectionService"To dump the internal state of
CarRemoteDeviceServiceandCarOccupantConnectionService:adb shell dumpsys car_service --services CarRemoteDeviceService && adb shell dumpsys car_service --services CarOccupantConnectionService
Null CarRemoteDeviceManager and CarOccupantConnectionManager
Check out these possible root causes:
The car service crashed. As illustrated earlier, the two managers are intentionally reset to be
nullwhen car service crashes. When car service is restarted, the two managers are set to non-null values.Either
CarRemoteDeviceServiceorCarOccupantConnectionServiceis not enabled. To determine if one or the other is enabled, run:adb shell dumpsys car_service --services CarFeatureControllerLook for
mDefaultEnabledFeaturesFromConfig, which should containcar_remote_device_serviceandcar_occupant_connection_service. For example: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]By default, these two services are disabled. When a device supports multi-display, you MUST overlay this configuration file. You can enable the two services in a configuration file:
// 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 when calling the API
If the client app isn't using the API as intended, an exception can occur. In this case, the client app can check the message in the exception and the crash stack to resolve the issue. Examples of misusing the API are:
registerStateCallback()This client already registered aStateCallback.unregisterStateCallback()NoStateCallbackwas registered by thisCarRemoteDeviceManagerinstance.registerReceiver()receiverEndpointIdis already registered.unregisterReceiver()receiverEndpointIdis not registered.requestConnection()A pending or established connection already exists.cancelConnection()No pending connection to cancel.sendPayload()No established connection.disconnect()No established connection.
Client1 can send Payload to client2, but not the other way around
The connection is one way by design. To establish two-way connection, both
client1 and client2 MUST request a connection to each other and then
get approval.