API Multi-Display Communications

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

OU

  • Armazene em cache o Payload e envie-o quando o endpoint receptor esperado for registrado, para o qual MyReceiverService é notificado através onReceiverRegistered()

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 e FLAG_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 Os FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED se tornam requisitos adicionais. Para um 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 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 assinatura FLAG_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:

Enviar a carga útil

Figura 1. Envie o 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 receiverEndpointIds 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 em Payload. Quando receber o Payload, o AbstractReceiverService nas chamadas de receptor; forwardPayload("FragmentB", payload) para enviar o payload para FragmentB
  • O remetente especifica data_type:VOLUME_CONTROL em Payload. Quando receber o Payload, o AbstractReceiverService no receptor saberá que esse tipo de Payload precisa ser enviado para FragmentB, então 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, 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.

Fluxo de conexão

Figura 2. Fluxo de conexão.

Solução de problemas

Verifique os registros

Para verificar os registros correspondentes:

  1. 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"
    
  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 as possíveis causas:

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

  2. CarRemoteDeviceService ou CarOccupantConnectionService não é 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 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 um StateCallback.
  • unregisterStateCallback() Nenhum StateCallback foi registrado por isso instância CarRemoteDeviceManager.
  • 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.