A API Multi-Display Communications pode ser usada por um app com privilégios do sistema no AAOS para se comunicar com o mesmo app (mesmo nome de pacote) executado em uma zona de ocupante diferente em um carro. Nesta página, descrevemos como integrar a API. Para saber mais, consulte CarOccupantZoneManager.OccupantZoneInfo.
Zona de ocupação
O conceito de uma zona de ocupante mapeia um usuário para um conjunto de telas. Cada zona de ocupante tem uma tela do tipo DISPLAY_TYPE_MAIN. Uma zona de ocupante também pode ter outras telas, como um display de cluster. Cada zona de ocupação é atribuída a um usuário do Android. Cada usuário tem as próprias contas e apps.
Configuração de hardware
A API Comms é compatível apenas com um SoC. No modelo de SoC único, todas as zonas e usuários ocupantes são executados no mesmo SoC. A API Comms consiste em três componentes:
A API de gerenciamento de energia permite que o cliente gerencie a energia das telas nas zonas de ocupação.
A API Discovery permite que o cliente monitore os estados de outras zonas de ocupantes no carro e de clientes semelhantes nessas zonas. Use a API Discovery antes da API Connection.
A API Connection permite que o cliente se conecte ao cliente pareado em outra zona de ocupação e envie uma carga útil para o cliente pareado.
As APIs Discovery e Connection são necessárias para a conexão. A API Power management é opcional.
A API Comms não oferece suporte à comunicação entre apps diferentes. Em vez disso, ele foi projetado apenas para comunicação entre apps com o mesmo nome de pacote e usado apenas para comunicação entre diferentes usuários visíveis.
Guia de integração
Implementar AbstractReceiverService
Para receber o Payload, o app receptor PRECISA implementar os métodos abstratos
definidos em AbstractReceiverService. Exemplo:
public class MyReceiverService extends AbstractReceiverService {
@Override
public void onConnectionInitiated(@NonNull OccupantZoneInfo senderZone) {
}
@Override
public void onPayloadReceived(@NonNull OccupantZoneInfo senderZone,
@NonNull Payload payload) {
}
}
O onConnectionInitiated() é invocado quando o cliente remetente solicita uma
conexão com o cliente receptor. Se for necessária a confirmação do usuário para estabelecer
a conexão, MyReceiverService can vai substituir esse método para iniciar uma
atividade de permissão e chamar acceptConnection() ou rejectConnection() com base
no resultado. Caso contrário, MyReceiverService pode apenas chamar
acceptConnection().
onPayloadReceived() é invocado quando MyReceiverService recebe um
Payload do cliente remetente. MyReceiverService pode substituir esse
método para:
- Encaminhe o
Payloadpara os endpoints do receptor correspondentes, se houver. Para receber os endpoints de receptor registrados, chamegetAllReceiverEndpoints(). Para encaminhar oPayloada um determinado endpoint de receptor, chameforwardPayload()
OU
- Armazene em cache o
Payloade envie quando o endpoint do destinatário esperado for registrado, para o qual oMyReceiverServiceé notificado poronReceiverRegistered().
Declarar AbstractReceiverService
O app receptor PRECISA declarar o AbstractReceiverService implementado no arquivo de manifesto, adicionar um filtro de intent com a ação android.car.intent.action.RECEIVER_SERVICE para esse serviço e exigir a permissão 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>
A permissão android.car.occupantconnection.permission.BIND_RECEIVER_SERVICE
garante que apenas o framework possa se vincular a esse serviço. Se o serviço não exigir a permissão, outro app poderá se vincular a ele e enviar um Payload diretamente.
Declarar permissão
O app cliente PRECISA declarar as permissões no arquivo de manifesto.
<!-- 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"/>
Cada uma das três permissões acima é privilegiada e PRECISA ser
pré-concedida por arquivos de lista de permissões. Por exemplo, confira o arquivo de lista de permissões do
app 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>
Receber administradores de carro
Para usar a API, o app cliente PRECISA registrar um CarServiceLifecycleListener para
receber os gerenciadores de carro associados:
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);
(Remetente) Discover
Antes de se conectar ao cliente receptor, o cliente remetente DEVE descobrir o
cliente receptor registrando um 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);
}
Antes de solicitar uma conexão com o receptor, o remetente DEVE verificar se todas as flags da zona de ocupação do receptor e do app receptor estão definidas. Caso contrário, erros podem ocorrer. Exemplo:
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;
}
Recomendamos que o remetente solicite uma conexão com o receptor somente quando todas as flags do receptor estiverem definidas. No entanto, há exceções:
FLAG_OCCUPANT_ZONE_CONNECTION_READYeFLAG_CLIENT_INSTALLEDsão os requisitos mínimos necessários para estabelecer uma conexão.Se o app receptor precisar mostrar uma interface para receber a aprovação do usuário para a conexão,
FLAG_OCCUPANT_ZONE_POWER_ONeFLAG_OCCUPANT_ZONE_SCREEN_UNLOCKEDserão requisitos adicionais. Para uma melhor experiência do usuário, também recomendamosFLAG_CLIENT_RUNNINGeFLAG_CLIENT_IN_FOREGROUND. Caso contrário, o usuário pode se surpreender.Por enquanto (Android 15),
FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKEDnão está implementado. O app cliente pode simplesmente ignorar.Por enquanto (Android 15), a API Comms só oferece suporte a vários usuários na mesma instância do Android para que apps semelhantes tenham o mesmo código de versão longa (
FLAG_CLIENT_SAME_LONG_VERSION) e assinatura (FLAG_CLIENT_SAME_SIGNATURE). Assim, os apps não precisam verificar se os dois valores são iguais.
Para uma melhor experiência do usuário, o cliente remetente PODE mostrar uma interface se uma flag não estiver definida. Por exemplo, se FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED não estiver definido, o remetente poderá mostrar um toast ou uma caixa de diálogo para pedir que o usuário desbloqueie a tela da zona de ocupante do receptor.
Quando o remetente não precisa mais descobrir os receptores (por exemplo, quando ele encontra todos os receptores e conexões estabelecidas ou fica inativo), ele PODE interromper a descoberta.
if (mRemoteDeviceManager != null) {
mRemoteDeviceManager.unregisterStateCallback();
}
Quando a descoberta é interrompida, as conexões atuais não são afetadas. O remetente pode continuar enviando Payload para os receptores conectados.
(Remetente) Solicitar conexão
Quando todas as flags do receptor são definidas, o remetente PODE solicitar uma conexão com o receptor:
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);
}
(Serviço receptor) Aceitar a conexão
Depois que o remetente solicitar uma conexão com o destinatário, o
AbstractReceiverService no app do destinatário será vinculado pelo serviço do carro,
e o AbstractReceiverService.onConnectionInitiated() será invocado. Conforme
explicado em (Transmissor) Solicitar conexão,
onConnectionInitiated() é um método abstraído e PRECISA ser implementado pelo
app cliente.
Quando o destinatário aceita a solicitação de conexão, o
ConnectionRequestCallback.onConnected() do remetente é invocado e a conexão
é estabelecida.
(Remetente) Enviar o payload
Depois que a conexão for estabelecida, o remetente poderá enviar Payload para o
destinatário:
if (mOccupantConnectionManager != null) {
Payload payload = ...;
try {
mOccupantConnectionManager.sendPayload(receiverZone, payload);
} catch (CarOccupantConnectionManager.PayloadTransferException e) {
Log.e(TAG, "Failed to send Payload to " + receiverZone);
}
}
O remetente pode colocar um objeto Binder ou uma matriz de bytes no Payload. Se o
remetente precisar enviar outros tipos de dados, ele DEVE serializar os dados em uma matriz de
bytes, usar a matriz de bytes para construir um objeto Payload e enviar o
Payload. Em seguida, o cliente receptor recebe a matriz de bytes do Payload recebido e desserializa a matriz de bytes no objeto de dados esperado.
Por exemplo, se o remetente quiser enviar uma string hello para o endpoint
do destinatário com ID FragmentB, ele poderá usar buffers de protocolo para definir um tipo de dados
como este:
message MyData {
required string receiver_endpoint_id = 1;
required string data = 2;
}
A Figura 1 ilustra o fluxo Payload:
(Serviço de recebimento) Receber e despachar o payload
Quando o app receptor recebe o Payload, o
AbstractReceiverService.onPayloadReceived() dele é invocado. Conforme explicado em
Enviar o payload, onPayloadReceived() é um
método abstraído e PRECISA ser implementado pelo app cliente. Nesse método, o
cliente PODE encaminhar o Payload para os endpoints de receptor correspondentes ou
armazenar em cache o Payload e enviá-lo quando o endpoint de receptor esperado for
registrado.
(Endpoint do receptor) Registrar e cancelar o registro
O app receptor PRECISA chamar registerReceiver() para registrar os endpoints
do receptor. Um caso de uso típico é quando um Fragment precisa receber Payload e, por isso,
registra um endpoint de receptor:
private final PayloadCallback mPayloadCallback = (senderZone, payload) -> {
…
};
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.registerReceiver("FragmentB",
getActivity().getMainExecutor(), mPayloadCallback);
}
Quando o AbstractReceiverService no cliente receptor despacha o
Payload para o endpoint receptor, o PayloadCallback associado é
invocado.
O app cliente PODE registrar vários endpoints de receptor, desde que os
receiverEndpointIds sejam exclusivos entre eles. O receiverEndpointId
será usado pelo AbstractReceiverService para decidir a quais endpoints
de receptor enviar o payload. Exemplo:
- O remetente especifica
receiver_endpoint_id:FragmentBnoPayload. Ao receber oPayload, oAbstractReceiverServiceno receptor chamaforwardPayload("FragmentB", payload)para enviar o payload paraFragmentB - O remetente especifica
data_type:VOLUME_CONTROLnoPayload. Ao receber oPayload, oAbstractReceiverServiceno receptor sabe que esse tipo dePayloadprecisa ser enviado paraFragmentB. Portanto, ele chamaforwardPayload("FragmentB", payload)
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.unregisterReceiver("FragmentB");
}
(Remetente) Encerrar a conexão
Quando o remetente não precisar mais enviar Payload ao destinatário (por exemplo,
se ele ficar inativo), a conexão DEVE ser encerrada.
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.disconnect(receiverZone);
}
Depois de desconectado, o remetente não pode mais enviar Payload para o destinatário.
Fluxo de conexão
Um fluxo de conexão é ilustrado na Figura 2.
Solução de problemas
Verificar os registros
Para verificar os registros correspondentes:
Execute este comando para fazer o registro:
adb shell setprop log.tag.CarRemoteDeviceService VERBOSE && adb shell setprop log.tag.CarOccupantConnectionService VERBOSE && adb logcat -s "AbstractReceiverService","CarOccupantConnectionManager","CarRemoteDeviceManager","CarRemoteDeviceService","CarOccupantConnectionService"Para despejar o estado interno de
CarRemoteDeviceServiceeCarOccupantConnectionService:adb shell dumpsys car_service --services CarRemoteDeviceService && adb shell dumpsys car_service --services CarOccupantConnectionService
CarRemoteDeviceManager e CarOccupantConnectionManager nulos
Confira estas possíveis causas raiz:
O serviço de carro falhou. Como ilustrado anteriormente, os dois gerenciadores são intencionalmente redefinidos para
nullquando o serviço do carro falha. Quando o serviço de carro é reiniciado, os dois gerenciadores são definidos como valores não nulos.CarRemoteDeviceServiceouCarOccupantConnectionServicenão está ativado. Para determinar se um ou outro está ativado, execute:adb shell dumpsys car_service --services CarFeatureControllerProcure
mDefaultEnabledFeaturesFromConfig, que deve contercar_remote_device_serviceecar_occupant_connection_service. Por exemplo: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]Por padrão, esses dois serviços estão desativados. Quando um dispositivo oferece suporte a vários monitores, você PRECISA sobrepor esse arquivo de configuração. É possível ativar os dois serviços em um arquivo de configuração:
// 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>
Exceção ao chamar a API
Se o app cliente não estiver usando a API da maneira pretendida, uma exceção poderá ocorrer. Nesse caso, o app cliente pode verificar a mensagem na exceção e a pilha de falhas para resolver o problema. Exemplos de uso indevido da API:
registerStateCallback()Este cliente já registrou umStateCallback.unregisterStateCallback()NenhumaStateCallbackfoi registrada por esta instância deCarRemoteDeviceManager.- O nome de domínio
registerReceiver()receiverEndpointIdjá está registrado. unregisterReceiver()receiverEndpointIdnão está registrado.requestConnection()Já existe uma conexão pendente ou estabelecida.cancelConnection()Não há conexões pendentes para cancelar.sendPayload()Nenhuma conexão estabelecida.disconnect()Nenhuma conexão estabelecida.
O cliente 1 pode enviar um payload para o cliente 2, mas não o contrário.
A conexão é unidirecional por design. Para estabelecer uma conexão bidirecional, client1 e client2 precisam solicitar uma conexão entre si e receber aprovação.