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
MyReceiverServicehas received a
Payloadfrom the sender client.
MyReceiverService` can override this
method to:
- Forward the
Payload
to the corresponding receiver endpoint(s), if any. To get the registered receiver endpoints, callgetAllReceiverEndpoints()
. To forward thePayload
to a given receiver endpoint, callforwardPayload()
OR,
- Cache the
Payload
, and send it when the expected receiver endpoint is registered, for which theMyReceiverService
is 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_SERVICE
permission
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_READY
andFLAG_CLIENT_INSTALLED
are 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_ON
andFLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED
become additional requirements. For a better user experience,FLAG_CLIENT_RUNNING
andFLAG_CLIENT_IN_FOREGROUND
are also recommended, otherwise the user might be surprised.For now (Android 15),
FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED
isn'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
receiverEndpointId
s 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:FragmentB
in thePayload
. When receiving thePayload
, theAbstractReceiverService
in the receiver callsforwardPayload("FragmentB", payload)
to dispatch the Payload toFragmentB
- The sender specifies
data_type:VOLUME_CONTROL
in thePayload
. When receiving thePayload
, theAbstractReceiverService
in the receiver knows that this type ofPayload
should 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
CarRemoteDeviceService
andCarOccupantConnectionService
: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
null
when car service crashes. When car service is restarted, the two managers are set to non-null values.Either
CarRemoteDeviceService
orCarOccupantConnectionService
is not enabled. To determine if one or the other is enabled, run:adb shell dumpsys car_service --services CarFeatureController
Look for
mDefaultEnabledFeaturesFromConfig
, which should containcar_remote_device_service
andcar_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()
NoStateCallback
was registered by thisCarRemoteDeviceManager
instance.registerReceiver()
receiverEndpointId
is already registered.unregisterReceiver()
receiverEndpointId
is 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.