A API Multi-Display Communications pode ser usada por um app privilegiado no sistema em o AAOS precisa se comunicar com o mesmo app (mesmo nome de pacote) em execução em um local diferente. a zona de ocupantes em um carro. Nesta página, descrevemos como integrar a API. Para saber mais, também podemos conferir CarOccupantZoneManager.OccupantZoneInfo.
Zona de ocupantes
O conceito de uma zona de ocupante mapeia um usuário para um conjunto de telas. Cada da zona de ocupantes tem uma tela com o tipo DISPLAY_TYPE_MAIN. Uma zona de ocupante também pode ter outras telas, como uma de cluster. Cada zona de ocupante recebe um usuário Android. Cada usuário tem suas próprias contas e apps.
Configuração de hardware
A API Comms oferece suporte a apenas um único SoC. No modelo de SoC único, todos os ocupantes zonas 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 capacidade do nas zonas de ocupantes.
A API Discovery permite que o cliente monitore os estados de outros ocupantes. zonas de ocupação no carro e monitorar clientes de mesmo nível nessas zonas de ocupantes. Usar a API Discovery antes de usar a API Connection.
A API Connection permite que o cliente se conecte ao cliente de mesmo nível no outra zona de ocupantes e enviar um payload ao cliente de mesmo nível.
A API Discovery e a API Connection são necessárias para a conexão. O poder API de gerenciamento é opcional.
A API Comms não é compatível com a comunicação entre apps diferentes. Em vez disso, ele foi projetado apenas para comunicação entre apps com o mesmo nome de pacote. e usada apenas para comunicação entre diferentes usuários visíveis.
Guia de integração
Implementar PersistentVolumeReceiverService
Para receber o Payload
, o app receptor PRECISA implementar os métodos abstratos.
definido 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
para esse cliente receptor. Se a confirmação do usuário for necessária para estabelecer
a conexão, MyReceiverService
pode substituir esse método para iniciar uma
permissão ativa e chamar acceptConnection()
ou rejectConnection()
com base
no resultado. Caso contrário, MyReceiverService
pode apenas chamar
acceptConnection()
".
onPayloadReceived()is invoked when
MyReceiverServicehas received a
Payloadfrom the sender client.
MyReceiverService` pode substituir isso
para:
- Encaminhe o
Payload
para os endpoints receptor correspondentes, se houver. Para receber os endpoints de receptor registrados, chamegetAllReceiverEndpoints()
. Para encaminhar oPayload
para um determinado endpoint do receptor, chameforwardPayload()
OU
- Armazene em cache o
Payload
e envie-o quando o endpoint receptor esperado for registrado, para o qualMyReceiverService
é notificado atravésonReceiverRegistered()
Declarar {5/}ReceiverService
O app receptor PRECISA declarar o AbstractReceiverService
implementado na
de manifesto do aplicativo, adicione um filtro de intent com ação
android.car.intent.action.RECEIVER_SERVICE
para este serviço e exigem os
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, outro app poderá se vincular a ela
e enviar um Payload
diretamente a ele.
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 DEVEM ser
concedida previamente por arquivos da lista de permissões. Por exemplo, este é o arquivo da lista de permissões de
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 administradores do carro
Para usar a API, o app cliente PRECISA registrar um CarServiceLifecycleListener
para
acessar 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);
Discover (remetente)
Antes de se conectar ao cliente receptor, o cliente remetente DEVE descobrir a
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 destinatário, o remetente DEVE se certificar de que as flags da zona de ocupante 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 destinatário somente quando todos os do receptor estejam definidas. Dito isso, 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 exibir uma IU para receber a aprovação do usuário para
FLAG_OCCUPANT_ZONE_POWER_ON
, e OsFLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED
se tornam requisitos adicionais. Para um 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 poderá se surpreender.Por enquanto (Android 15), o
FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED
não foi implementado. O app cliente pode simplesmente ignorá-la.Por enquanto (Android 15), a API Comms só oferece suporte a vários usuários no mesmo Instância do Android para que os apps de peering possam ter o mesmo código de versão longo (
FLAG_CLIENT_SAME_LONG_VERSION
) e assinaturaFLAG_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 uma sinalização não for
definido. Por exemplo, se FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED
não estiver definido, o remetente
pode mostrar um aviso ou uma caixa de diálogo solicitando que o usuário desbloqueie a tela do
na zona de ocupantes do receptor.
Quando o remetente não precisar mais descobrir os destinatários (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 destinatário estão definidas, o remetente CAN pode solicitar uma conexão para o destinatário:
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 solicita uma conexão com o destinatário, a
A AbstractReceiverService
no app receptor vai ser vinculada pelo serviço do carro.
e AbstractReceiverService.onConnectionInitiated()
serão invocados. Conforme
explicado em Conexão de solicitação(remetente),
onConnectionInitiated()
é um método abstraído e PRECISA ser implementado pela
app cliente.
Quando o destinatário aceita a solicitação de conexão, o
ConnectionRequestCallback.onConnected()
será invocado, e então a conexão
é estabelecido.
(Remetente) Enviar o payload
Depois que a conexão é estabelecida, o remetente PODE 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 PRECISA serializar os dados em um byte
use a matriz de bytes para criar um objeto Payload
e envie o
Payload
: Em seguida, o cliente receptor recebe a matriz de bytes do
Payload
e desserializa a matriz de bytes no objeto de dados esperado.
Por exemplo, se o remetente quiser enviar uma string hello
para o destinatário
endpoint com ID FragmentB
, ele pode usar Proto Buffers para definir um tipo de dado
assim:
message MyData {
required string receiver_endpoint_id = 1;
required string data = 2;
}
A Figura 1 ilustra o fluxo de Payload
:
(Serviço do receptor) Receber e enviar o payload
Depois que o app receptor recebe o Payload
,
AbstractReceiverService.onPayloadReceived()
será invocado. Conforme explicado em
Enviar o payload, onPayloadReceived()
é uma
abstraído e PRECISA ser implementado pelo app cliente. Neste método, a
o cliente PODE encaminhar o Payload
para os endpoints receptor correspondentes; ou
armazenar em cache a Payload
e enviá-la quando o endpoint do receptor esperado for
registrados.
(Endpoint do destinatário) Registrar e cancelar o registro
O app receptor PRECISA chamar registerReceiver()
para fazer o registro dele.
endpoints. Um caso de uso típico é que um fragmento precisa receber Payload
. Portanto,
ele registra um endpoint receptor:
private final PayloadCallback mPayloadCallback = (senderZone, payload) -> {
…
};
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.registerReceiver("FragmentB",
getActivity().getMainExecutor(), mPayloadCallback);
}
Depois que o AbstractReceiverService
no cliente receptor envia o
Payload
ao endpoint do receptor, o PayloadCallback
associado será
invocado.
O aplicativo cliente PODE registrar vários endpoints de receptor, desde que seus
receiverEndpointId
s são exclusivos entre o app cliente. O receiverEndpointId
vai ser usado pelo AbstractReceiverService
para decidir qual receptor
endpoint(s) para onde enviar o payload. Exemplo:
- O remetente especifica
receiver_endpoint_id:FragmentB
emPayload
. Quando receber oPayload
, oAbstractReceiverService
nas chamadas de receptor;forwardPayload("FragmentB", payload)
para enviar o payload paraFragmentB
- O remetente especifica
data_type:VOLUME_CONTROL
emPayload
. Quando receber oPayload
, oAbstractReceiverService
no receptor saberá que esse tipo dePayload
precisa 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
ao destinatário (por exemplo,
ele ficar inativo), ele DEVE encerrar a conexão.
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.disconnect(receiverZone);
}
Após a desconexão, o remetente não poderá mais enviar Payload
ao destinatário.
Fluxo de conexão
Um fluxo de conexão é ilustrado na Figura 2.
Solução de problemas
Verifique os registros
Para verificar os registros correspondentes:
Execute este comando para geração de 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 as possíveis causas:
O serviço veicular falhou. Como ilustrado anteriormente, os dois gerentes são redefinido intencionalmente para ser
null
quando o serviço veicular falha. Ao manutenção do carro for reiniciado, os dois gerenciadores serão definidos como valores não nulos.CarRemoteDeviceService
ouCarOccupantConnectionService
não é 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 ficam desativados. Quando um dispositivo é compatível de 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 como pretendido, poderá ocorrer uma exceção. Nesse caso, o app cliente pode verificar a mensagem na exceção e no a pilha de falhas para resolver o problema. Exemplos de uso indevido da API:
registerStateCallback()
Esse cliente já registrou umStateCallback
.unregisterStateCallback()
NenhumStateCallback
foi registrado por isso instânciaCarRemoteDeviceManager
.registerReceiver()
receiverEndpointId
já está registrado.unregisterReceiver()
receiverEndpointId
nã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.
Client1 pode enviar payload para client2, mas não o contrário
Desde a concepção, a conexão é unidirecional. Para estabelecer uma conexão bidirecional,
client1
e client2
PRECISAM solicitar uma conexão entre si e, em seguida,
receber aprovação.