A API Multi-Display Communications pode ser usada por um app privilegiado do sistema no AAOS para se comunicar com o mesmo app (mesmo nome de pacote) em execução em uma zona de ocupantes diferente em um carro. Esta página descreve como integrar a API. Para saber mais, consulte CarOccupantZoneManager.OccupantZoneInfo.
Zona de ocupantes
O conceito de zona de ocupantes mapeia um usuário para um conjunto de telas. Cada zona de ocupantes tem uma tela com o tipo DISPLAY_TYPE_MAIN. Uma zona de ocupantes também pode ter outras telas, como uma tela de cluster. Cada zona de ocupantes recebe um usuário do Android. Cada usuário tem as próprias contas e apps.
Configuração de hardware
A API Comms oferece suporte apenas a um único SoC. No modelo de SoC único, todas as zonas de ocupantes e usuários são executados no mesmo SoC. A API Comms consiste em três componentes:
A API Power Management permite que o cliente gerencie a energia das telas nas zonas de ocupantes.
A API Discovery permite que o cliente monitore os estados de outras zonas de ocupantes no carro e os clientes semelhantes nessas zonas. Use a API Discovery antes de usar a API Connection.
A API Connection permite que o cliente se conecte ao cliente semelhante em outra zona de ocupantes e envie um payload para ele.
A API Discovery e a API 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, ela foi projetada apenas para comunicação entre apps com o mesmo nome de pacote e usada somente para comunicação entre usuários visíveis diferentes.
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) {
}
}
onConnectionInitiated() é invocado quando o cliente remetente solicita uma conexão com esse cliente receptor. Se a confirmação do usuário for necessária para estabelecer a conexão, MyReceiverService poderá modificar esse método para iniciar uma atividade de permissão e chamar acceptConnection() ou rejectConnection() com base no resultado. Caso contrário, MyReceiverService poderá apenas chamar acceptConnection().
onPayloadReceived() é invocado quando MyReceiverService recebe um
Payload do cliente remetente. MyReceiverService pode modificar esse método para:
- Encaminhar o
Payloadpara os endpoints do receptor correspondentes, se houver. Para receber os endpoints do receptor registrados, chamegetAllReceiverEndpoints(). Para encaminhar oPayloadpara um endpoint do receptor específico, chameforwardPayload()
OU
- Armazenar o
Payloadem cache e enviá-lo quando o endpoint do receptor 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 esse serviço não exigir a permissão, um app diferente 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 são privilegiadas, que PRECISAM ser pré-concedidas 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 gerenciadores de carros
Para usar a API, o app cliente PRECISA registrar um CarServiceLifecycleListener para receber os gerenciadores de carros 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) Descobrir
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 todos os flags da zona de ocupantes e do app receptor estão definidos. Caso contrário, erros poderão 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 todos os flags do receptor estiverem definidos. 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 da conexão,
FLAG_OCCUPANT_ZONE_POWER_ONeFLAG_OCCUPANT_ZONE_SCREEN_UNLOCKEDse tornam requisitos adicionais. Para uma melhor experiência do usuário,FLAG_CLIENT_RUNNINGeFLAG_CLIENT_IN_FOREGROUNDtambém são recomendados. Caso contrário, o usuário poderá se surpreender.Por enquanto (Android 15),
FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKEDnão está implementado. O app cliente pode ignorá-lo.Por enquanto (Android 15), a API Comms oferece suporte apenas a vários usuários na mesma instância do Android para que apps semelhantes possam ter o mesmo código de versão longa (
FLAG_CLIENT_SAME_LONG_VERSION) e assinatura (FLAG_CLIENT_SAME_SIGNATURE). Como resultado, os apps não precisam verificar se os dois valores concordam.
Para uma melhor experiência do usuário, o cliente remetente PODE mostrar uma interface se um flag não estiver definido. Por exemplo, se FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED não estiver definido, o remetente poderá mostrar um aviso ou uma caixa de diálogo para pedir ao usuário que desbloqueie a tela da zona de ocupantes do receptor.
Quando o remetente não precisar mais descobrir os receptores (por exemplo, quando encontrar todos os receptores e conexões estabelecidas ou ficar 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 todos os flags do receptor estiverem definidos, 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 receptor, o
AbstractReceiverService no app receptor será vinculado pelo serviço de carro,
e AbstractReceiverService.onConnectionInitiated() será invocado. Conforme
explicado em (Remetente) Solicitar conexão,
onConnectionInitiated() é um método abstrato e PRECISA ser implementado pelo
app cliente.
Quando o receptor 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 PODE enviar Payload para o receptor:
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 PRECISA 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 receptor com o 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 de Payload:
(Serviço receptor) Receber e enviar o payload
Depois que o app receptor receber o Payload, o
AbstractReceiverService.onPayloadReceived() será invocado. Conforme explicado em
Enviar o payload, onPayloadReceived() é um
método abstrato e PRECISA ser implementado pelo app cliente. Nesse método, o
cliente PODE encaminhar o Payload para os endpoints do receptor correspondentes ou
armazenar o Payload em cache e enviá-lo quando o endpoint do receptor esperado for
registrado.
(Endpoint do receptor) Registrar e cancelar o registro
O app receptor DEVE chamar registerReceiver() para registrar os endpoints do receptor. Um caso de uso típico é que um fragmento precisa receber Payload, então ele registra um endpoint do receptor:
private final PayloadCallback mPayloadCallback = (senderZone, payload) -> {
…
};
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.registerReceiver("FragmentB",
getActivity().getMainExecutor(), mPayloadCallback);
}
Depois que o AbstractReceiverService no cliente receptor enviar o
Payload para o endpoint do receptor, o PayloadCallback será
invocado.
O app cliente PODE registrar vários endpoints do receptor, desde que os receiverEndpointIds sejam exclusivos no app cliente. O receiverEndpointId será usado pelo AbstractReceiverService para decidir para quais endpoints do 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, então ele chamaforwardPayload("FragmentB", payload)
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.unregisterReceiver("FragmentB");
}
(Remetente) Encerrar a conexão
Quando o remetente não precisar mais enviar Payload para o receptor (por exemplo, ele fica inativo), ele DEVE encerrar a conexão.
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.disconnect(receiverZone);
}
Depois de desconectado, o remetente não poderá mais enviar Payload para o receptor.
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 login:
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. Conforme ilustrado anteriormente, os dois gerenciadores são redefinidos intencionalmente para serem
nullquando o serviço de 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 precisa contercar_remote_device_serviceecar_occupant_connection_service. 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árias telas, 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 conforme o esperado, 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 são:
registerStateCallback()Esse cliente já registrou umStateCallback.unregisterStateCallback()NenhumStateCallbackfoi registrado por essaCarRemoteDeviceManagerinstância.registerReceiver()receiverEndpointIdjá está registrado.unregisterReceiver()receiverEndpointIdnão está registrado.requestConnection()Já existe uma conexão pendente ou estabelecida.cancelConnection()Nenhuma conexão pendente para cancelar.sendPayload()Nenhuma conexão estabelecida.disconnect()Nenhuma conexão estabelecida.
O cliente 1 pode enviar o 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.