A API de comunicações de várias telas 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 no carro. Esta página descreve como integrar a API. Para saber mais, consulte CarOccupantZoneManager.OccupantZoneInfo.
Zona do ocupante
O conceito de zona de ocupantes mapeia um usuário para um conjunto de telas. Cada zona de ocupante 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 ocupante é atribuída a um usuário do Android. Cada usuário tem suas 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 executadas 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 monitore clientes semelhantes nessas zonas de ocupantes. Use a API Discovery antes de usar a API Connection.
A API Connection permite que o cliente se conecte ao cliente peer em outra zona de ocupante 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 diferentes apps. Em vez disso, ele foi projetado apenas para a comunicação entre apps com o mesmo nome de pacote e usado apenas para a comunicação entre diferentes usuários visíveis.
Guia de integração
Implementar o AbstractReceiverService
Para receber a 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 de envio solicita uma
conexão a esse cliente de recebimento. Se a confirmação do usuário for necessária para estabelecer
a conexão, MyReceiverService
pode 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 chamar
acceptConnection()
.`
onPayloadReceived()is invoked when
MyReceiverServicehas received a
Payloadfrom the sender client.
MyReceiverService` pode substituir esse
método para:
- Encaminhar o
Payload
para os endpoints de receptor correspondentes, se houver. Para receber os endpoints de receptor registrados, chamegetAllReceiverEndpoints()
. Para encaminhar oPayload
para um determinado endpoint de receptor, chameforwardPayload()
OU
- Armazene em cache o
Payload
e envie-o 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 esse serviço
não exigir a permissão, um app diferente poderá se vincular a ele
e enviar uma 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 permissões privilegiadas, que precisam ser
concedidas previamente por arquivos de lista de permissões. 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>
Acessar os administradores do carro
Para usar a API, o app cliente PRECISA registrar um CarServiceLifecycleListener
para
receber os administradores 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);
(Sender) Discover
Antes de se conectar ao cliente receptor, o cliente remetente PRECISA 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 ao receptor, o remetente PRECISA garantir que todas as flags da zona de ocupantes e do app do receptor estejam definidas. 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 ao 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 da conexão,
FLAG_OCCUPANT_ZONE_POWER_ON
eFLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED
vão se tornar requisitos adicionais. Para uma melhor experiência do usuário,FLAG_CLIENT_RUNNING
eFLAG_CLIENT_IN_FOREGROUND
também são recomendados. Caso contrário, o usuário pode ficar surpreso.Por enquanto (Android 15),
FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED
não está implementado. O app cliente pode simplesmente ignorá-lo.Por enquanto (Android 15), a API Comms só oferece suporte a vários usuários na mesma instância do Android para que os apps peer 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 estão de acordo.
Para uma melhor experiência do usuário, o cliente do 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 uma mensagem flutuante ou uma caixa de diálogo para solicitar que o usuário desbloqueie a tela da
zona do ocupante do receptor.
Quando o remetente não precisa mais descobrir os receptores (por exemplo, quando 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
aos receptores conectados.
(Remetente) Solicitar conexão
Quando todas as flags do receptor estão definidas, o remetente PODE solicitar uma conexão ao 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 do receptor) Aceitar a conexão
Quando o remetente solicitar uma conexão ao receptor, o
AbstractReceiverService
no app receptor será vinculado pelo serviço do carro
e o AbstractReceiverService.onConnectionInitiated()
será invocado. Conforme
explicado em (Sender) Request Connection,
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 é estabelecida, o remetente PODE enviar Payload
ao
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 deserializa 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 o ID FragmentB
, ele poderá usar os ProtoBuffers 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 do receptor) Receber e enviar a carga útil
Quando o app receptor recebe o Payload
, o
AbstractReceiverService.onPayloadReceived()
dele é invocado. Como 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 de recebimento correspondentes ou
armazenar o Payload
em cache e enviá-lo assim que o endpoint de recebimento 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 fragmento precisa receber Payload
. Portanto, ele registra um endpoint do receptor:
private final PayloadCallback mPayloadCallback = (senderZone, payload) -> {
…
};
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.registerReceiver("FragmentB",
getActivity().getMainExecutor(), mPayloadCallback);
}
Quando o AbstractReceiverService
no cliente receptor envia o
Payload
para o endpoint do receptor, o PayloadCallback
associado é
invocado.
O app cliente PODE registrar vários endpoints de receptor, desde que os
receiverEndpointId
s sejam exclusivos entre o app cliente. O receiverEndpointId
será usado pelo AbstractReceiverService
para decidir para 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 aPayload
, oAbstractReceiverService
no receptor sabe que esse tipo dePayload
precisa ser enviado paraFragmentB
, então ele chamaforwardPayload("FragmentB", payload)
.
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.unregisterReceiver("FragmentB");
}
(Sender) Encerrar a conexão
Quando o remetente não precisar mais enviar Payload
ao receptor (por exemplo,
ele se torna inativo), ele PRECISA encerrar a conexão.
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.disconnect(receiverZone);
}
Depois de desconectada, o remetente não pode 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 gerar registros:
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 do carro falhou. Como ilustrado anteriormente, os dois gerenciadores são redefinidos intencionalmente para
null
quando o serviço de 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ão ativados. Para determinar se um ou outro está ativado, execute:adb shell dumpsys car_service --services CarFeatureController
Procure
mDefaultEnabledFeaturesFromConfig
, que precisa 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árias telas, é PRECISO 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 na pilha de falhas para resolver o problema. Exemplos de uso indevido da API são:
registerStateCallback()
Este cliente já registrou umStateCallback
.unregisterStateCallback()
NenhumStateCallback
foi registrado por esta instânciaCarRemoteDeviceManager
.registerReceiver()
receiverEndpointId
já está registrado.unregisterReceiver()
receiverEndpointId
não está registrado.requestConnection()
Uma conexão pendente ou estabelecida já existe.cancelConnection()
Não há conexão pendente para cancelar.sendPayload()
Nenhuma conexão estabelecida.disconnect()
Nenhuma conexão estabelecida.
O cliente 1 pode enviar 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.