API Multi-Display Communications

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, chame getAllReceiverEndpoints(). Para encaminhar o Payload a um determinado endpoint de receptor, chame forwardPayload()

OU

  • Armazene em cache o Payload e envie quando o endpoint do destinatário esperado for registrado, para o qual o MyReceiverService é notificado por onReceiverRegistered().

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 e FLAG_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 e FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED serão requisitos adicionais. Para uma melhor experiência do usuário, também recomendamos FLAG_CLIENT_RUNNING e FLAG_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:

Enviar o payload

Figura 1. Envie o 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:FragmentB no Payload. Ao receber o Payload, o AbstractReceiverService no receptor chama forwardPayload("FragmentB", payload) para enviar o payload para FragmentB
  • O remetente especifica data_type:VOLUME_CONTROL no Payload. Ao receber o Payload, o AbstractReceiverService no receptor sabe que esse tipo de Payload precisa ser enviado para FragmentB. Portanto, ele chama forwardPayload("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.

Fluxo de conexão

Figura 2. Fluxo de conexão.

Solução de problemas

Verificar os registros

Para verificar os registros correspondentes:

  1. 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"
  2. Para despejar o estado interno de CarRemoteDeviceService e CarOccupantConnectionService:

    adb shell dumpsys car_service --services CarRemoteDeviceService && adb shell dumpsys car_service --services CarOccupantConnectionService

CarRemoteDeviceManager e CarOccupantConnectionManager nulos

Confira estas possíveis causas raiz:

  1. 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.

  2. CarRemoteDeviceService ou CarOccupantConnectionService não está ativado. Para determinar se um ou outro está ativado, execute:

    adb shell dumpsys car_service --services CarFeatureController
    • Procure mDefaultEnabledFeaturesFromConfig, que deve conter car_remote_device_service e car_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 um StateCallback.
  • unregisterStateCallback() Nenhuma StateCallback foi registrada por esta instância de CarRemoteDeviceManager.
  • 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.