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
Payload
para os endpoints do receptor correspondentes, se houver. Para receber os endpoints de receptor registrados, chamegetAllReceiverEndpoints()
. Para encaminhar oPayload
a um determinado endpoint de receptor, chameforwardPayload()
OU
- Armazene em cache o
Payload
e 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_READY
eFLAG_CLIENT_INSTALLED
sã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_ON
eFLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED
serão requisitos adicionais. Para uma melhor experiência do usuário, também recomendamosFLAG_CLIENT_RUNNING
eFLAG_CLIENT_IN_FOREGROUND
. Caso contrário, o usuário pode se surpreender.Por enquanto (Android 15),
FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED
nã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
receiverEndpointId
s 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:FragmentB
noPayload
. Ao receber oPayload
, oAbstractReceiverService
no receptor chamaforwardPayload("FragmentB", payload)
para enviar o payload paraFragmentB
- O remetente especifica
data_type:VOLUME_CONTROL
noPayload
. Ao receber oPayload
, oAbstractReceiverService
no receptor sabe que esse tipo dePayload
precisa 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
CarRemoteDeviceService
eCarOccupantConnectionService
: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
null
quando o serviço do carro falha. Quando o serviço de carro é reiniciado, os dois gerenciadores são definidos como valores não nulos.CarRemoteDeviceService
ouCarOccupantConnectionService
não está ativado. Para determinar se um ou outro está ativado, execute:adb shell dumpsys car_service --services CarFeatureController
Procure
mDefaultEnabledFeaturesFromConfig
, que deve contercar_remote_device_service
ecar_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()
NenhumaStateCallback
foi registrada por esta instância deCarRemoteDeviceManager
.- O nome de domínio
registerReceiver()
receiverEndpointId
já está registrado. unregisterReceiver()
receiverEndpointId
nã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.