API Multi-Display Communications

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

OU

  • Armazene em cache o Payload e envie-o 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 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 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 da conexão, FLAG_OCCUPANT_ZONE_POWER_ON e FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED vão se tornar requisitos adicionais. Para uma melhor experiência do usuário, FLAG_CLIENT_RUNNING e FLAG_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:

Enviar o payload

Figura 1. Envie o 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 receiverEndpointIds 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 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 a Payload, o AbstractReceiverService no receptor sabe que esse tipo de Payload precisa ser enviado para FragmentB, então ele chama forwardPayload("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.

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 gerar registros:

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

  2. CarRemoteDeviceService ou CarOccupantConnectionService 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 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á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 um StateCallback.
  • unregisterStateCallback() Nenhum StateCallback foi registrado por esta instância CarRemoteDeviceManager.
  • 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.