Fundamentos da biblioteca Car App

1. Antes de começar

Neste codelab, você aprenderá a criar apps otimizados contra distrações para Android Auto e Android Automotive OS usando a biblioteca Android for Cars App. Primeiro, você vai adicionar suporte para o Android Auto e, em seguida, com o mínimo de trabalho adicional, vai criar uma variante do app que pode ser executada no Android Automotive OS. Depois de executar o aplicativo nas duas plataformas, você criará outra tela e interatividade básica.

O que este codelab não é

O que é necessário

O que você vai criar

Android Auto

Android Automotive OS

Uma gravação de tela mostrando o aplicativo em execução no Android Auto usando a unidade principal do computador.

Uma gravação de tela mostrando o aplicativo em execução em um emulador do Android Automotive OS.

O que você vai aprender

  • Como funciona a arquitetura cliente-host da biblioteca Car App.
  • Como gravar suas próprias classes CarAppService, Session e Screen.
  • Como compartilhar sua implementação no Android Auto e no Android Automotive OS.
  • Como executar o Android Auto em sua máquina de desenvolvimento usando a unidade principal do computador.
  • Como executar o emulador do Android Automotive OS.

2. Começar a configuração

Acessar o código

  1. O código deste codelab pode ser encontrado no diretório car-app-library-fundamentals do repositório car-codelabs do GitHub. Para clonar, execute o seguinte comando:
git clone https://github.com/android/car-codelabs.git
  1. Se preferir, baixe o repositório como um arquivo ZIP:

Abrir o projeto

  • Depois de iniciar o Android Studio, importe o projeto, selecionando apenas o diretório car-app-library-fundamentals/start. O diretório car-app-library-fundamentals/end contém o código da solução, que você pode consultar a qualquer momento se tiver dificuldades ou apenas quiser conferir o projeto completo.

Conhecer o código

  • Depois de abrir o projeto no Android Studio, analise o código inicial.

Esse código do aplicativo é dividido em dois módulos, :app e :common:data.

O módulo :app depende do módulo :common:data.

O módulo :app contém a interface e a lógica do app para dispositivos móveis, e o módulo :common:data contém a classe de dados do modelo Place e o repositório usado para ler modelos Place. Para simplificar, o repositório faz a leitura em uma lista codificada, mas poderia facilmente ler em um banco de dados ou servidor de back-end em um aplicativo real.

O módulo :app inclui uma dependência do módulo :common:data para que ele possa ler e apresentar a lista de modelos Place.

3. Informações sobre a biblioteca Android for Cars App

A biblioteca Android for Cars App é um conjunto de bibliotecas do Jetpack que permite aos desenvolvedores criar aplicativos para uso em veículos. Ela fornece uma estrutura modelada com interfaces de usuário otimizadas para a direção e, ao mesmo tempo, se adapta às várias configurações de hardware presentes nos carros (por exemplo, métodos de entrada, tamanhos de tela e proporções). Em conjunto, isso torna mais fácil para os desenvolvedores criar um aplicativo e ter a confiança de que ele terá um bom desempenho em uma variedade de veículos que executam o Android Auto e o Android Automotive OS.

Saiba como funciona

Os apps criados com a biblioteca Car App não são executados diretamente no Android Auto ou no Android Automotive OS. Em vez disso, eles contam com um aplicativo host que se comunica com apps clientes e renderiza as interfaces de usuário do cliente em nome deles. O próprio Android Auto é um host, e o Google Automotive App Host é o host usado em veículos com Android Automotive OS e o Google integrado. Veja a seguir as principais classes da biblioteca Car App que é preciso estender ao criar seu aplicativo:

CarAppService

CarAppService é uma subclasse da classe Service do Android e serve como ponto de entrada para aplicativos host se comunicarem com apps clientes (como o que você cria neste codelab). O principal objetivo dela é criar instâncias de Session que interagem com o aplicativo host.

Session

A Session é como uma instância de um app cliente em execução na tela do veículo. Como outros componentes do Android, ela tem um ciclo de vida próprio que pode ser usado para inicializar e desmontar recursos usados durante a existência da instância de Session. Existe uma relação um para muitos entre CarAppService e Session. Por exemplo, um CarAppService pode ter duas instâncias de Session, uma em exibição primária e outra em exibição de cluster para aplicativos de navegação compatíveis com telas de cluster.

Screen

As instâncias de Screen são responsáveis por gerar as interfaces do usuário renderizadas por aplicativos host. Essas interfaces são representadas por classes Template com um tipo específico de layout, como uma grade ou lista. Cada Session gerencia uma pilha de instâncias de Screen que lidam com fluxos do usuário nas diferentes partes do seu aplicativo. Assim como acontece com Session, uma Screen tem um ciclo de vida próprio ao qual você pode se conectar.

Um diagrama de como funciona a biblioteca Car App. No lado esquerdo, estão duas caixas intituladas Display. No centro, há uma caixa intitulada Host. À direita, há uma caixa intitulada CarAppService. Dentro da caixa CarAppService, há duas caixas, cada uma intitulada Session. Na primeira caixa, há três caixas Screen uma em cima da outra. Na segunda Session, há duas caixas Screen uma em cima da outra. Há setas entre cada Display e o host, bem como entre o host e as Sessions, para indicar como o host gerencia a comunicação entre todos os diferentes componentes.

Você vai escrever CarAppService, Session e Screen na seção Escrever o CarAppService deste codelab, então não se preocupe com o que ainda não funciona.

4. Definir a configuração inicial

Para começar, configure o módulo que contém o CarAppService e declare as dependências.

Criar o módulo car-app-service

  1. Com o módulo :common selecionado na janela Project, clique com o botão direito do mouse e escolha a opção New > Module.
  2. No assistente de módulo que é aberto, selecione o modelo Android Library (para que este módulo possa ser usado como uma dependência por outros módulos) na lista do lado esquerdo e use os seguintes valores:
  • Nome do módulo: :common:car-app-service
  • Nome do pacote: com.example.places.carappservice
  • SDK mínimo: API 23: Android 6.0 (Marshmallow)

Assistente para criar novo módulo com os valores definidos conforme descrito nesta etapa.

Configurar dependências

  1. No arquivo build.gradle do projeto, adicione uma declaração de variável para a versão da biblioteca Car App da seguinte maneira. Assim, é possível usar com facilidade a mesma versão em cada um dos módulos do aplicativo.

build.gradle (projeto: Places)

buildscript {
    ext {
        // All versions can be found at https://developer.android.com/jetpack/androidx/releases/car-app
        car_app_library_version = '1.3.0-rc01'
        ...
    }
}
  1. Em seguida, adicione duas dependências ao arquivo build.gradle do módulo :common:car-app-service.
  • androidx.car.app:app. Esse é o artefato principal da biblioteca Car App e inclui todas as classes principais usadas ao criar aplicativos. Há três outros artefatos que compõem a biblioteca, androidx.car.app:app-projected para a funcionalidade específica do Android Auto, androidx.car.app:app-automotive para o código de funcionalidade do Android Automotive OS e androidx.car.app:app-testing para alguns auxiliares úteis em testes de unidade. Você usará app-projected e app-automotive mais adiante no codelab.
  • :common:data. Esse é o mesmo módulo de dados usado pelo app para dispositivos móveis e permite o uso da mesma fonte de dados para todas as versões da experiência no app.

build.gradle (módulo :common:car-app-service)

dependencies {
    ...
    implementation "androidx.car.app:app:$car_app_library_version"
    implementation project(":common:data")
    ...
}

Com essa mudança, o gráfico de dependência dos próprios módulos do aplicativo é o seguinte:

Os módulos :app e :common:car-app-service dependem do módulo :common:data.

Agora que as dependências estão configuradas, é hora de escrever o CarAppService.

5. Escrever o CarAppService

  1. Comece criando um arquivo chamado PlacesCarAppService.kt no pacote carappservice dentro do módulo :common:car-app-service.
  2. Nesse arquivo, crie uma classe chamada PlacesCarAppService, que estende CarAppService.

PlacesCarAppService.kt

class PlacesCarAppService : CarAppService() {

    override fun createHostValidator(): HostValidator {
        return HostValidator.ALLOW_ALL_HOSTS_VALIDATOR
    }

    override fun onCreateSession(): Session {
        // PlacesSession will be an unresolved reference until the next step
        return PlacesSession()
    }
}

A classe abstrata CarAppService implementa métodos Service, como onBind e onUnbind, e evita substituições adicionais desses métodos para garantir a interoperabilidade adequada com aplicativos host. Tudo o que você precisa fazer é implementar createHostValidator e onCreateSession.

O HostValidator retornado de createHostValidator é referenciado quando o CarAppService está sendo associado, para que o host seja confiável e a vinculação falhe se o host não corresponder aos parâmetros definidos. Para os fins deste codelab (e dos testes em geral), o ALLOW_ALL_HOSTS_VALIDATOR ajuda a garantir que seu app se conecte, mas não seja usado na produção. Consulte a documentação do createHostValidator para mais informações sobre como configurar isso em um app de produção.

Para um aplicativo tão simples quanto este, onCreateSession pode retornar uma instância de Session. Em um app mais complicado, esse seria um bom ponto para inicializar recursos de longa duração, como métricas e clientes de registro usados enquanto seu aplicativo está em execução no veículo.

  1. Por fim, você precisa adicionar o elemento <service> que corresponde ao PlacesCarAppService no arquivo AndroidManifest.xml do módulo :common:car-app-service para permitir que o sistema operacional (e, por extensão, outros apps, como hosts) saiba que ele existe.

AndroidManifest.xml (:common:car-app-service)

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <!--
        This AndroidManifest.xml will contain all of the elements that should be shared across the
        Android Auto and Automotive OS versions of the app, such as the CarAppService <service> element
    -->

    <application>
        <service
            android:name="com.example.places.carappservice.PlacesCarAppService"
            android:exported="true">
            <intent-filter>
                <action android:name="androidx.car.app.CarAppService" />
                <category android:name="androidx.car.app.category.POI" />
            </intent-filter>
        </service>
    </application>
</manifest>

Há duas observações importantes aqui:

  • O elemento <action> permite que aplicativos host (e de tela de início) encontrem o app.
  • O elemento <category> declara a categoria do aplicativo, que determina quais critérios de qualidade ele precisa satisfazer (veja mais detalhes sobre isso posteriormente). Outros valores possíveis são androidx.car.app.category.NAVIGATION e androidx.car.app.category.IOT.

Criar a classe PlacesSession

  • Crie um arquivo PlacesCarAppService.kt e adicione o código abaixo:

PlacesCarAppService.kt

class PlacesSession : Session() {
    override fun onCreateScreen(intent: Intent): Screen {
        // MainScreen will be an unresolved reference until the next step
        return MainScreen(carContext)
    }
}

Para um aplicativo simples como este, você pode retornar a tela principal em onCreateScreen. No entanto, como esse método usa Intent como um parâmetro, um app com mais recursos também pode lê-lo e preencher uma backstack de telas ou usar alguma outra lógica condicional.

Criar a classe MainScreen

Em seguida, crie um novo pacote chamado screen.

  1. Clique com o botão direito do mouse no pacote com.example.places.carappservice e selecione New > Package (o nome completo do pacote será com.example.places.carappservice.screen). É aqui que você coloca todas as subclasses Screen do aplicativo.
  2. No pacote screen, crie um arquivo chamado MainScreen.kt para conter a classe MainScreen, que estende Screen. Por enquanto, é exibida uma simples mensagem Hello, world! usando o PaneTemplate.

MainScreen.kt

class MainScreen(carContext: CarContext) : Screen(carContext) {
    override fun onGetTemplate(): Template {
        val row = Row.Builder()
            .setTitle("Hello, world!")
            .build()
        
        val pane = Pane.Builder()
            .addRow(row)
            .build()

        return PaneTemplate.Builder(pane)
            .setHeaderAction(Action.APP_ICON)
            .build()
    }
}

6. Adicionar suporte ao Android Auto

Você já implementou toda a lógica necessária para colocar o app em funcionamento, mas ainda há outras duas configurações a serem definidas antes de executá-lo no Android Auto.

Adicionar uma dependência no módulo car-app-service

No arquivo build.gradle do módulo :app, adicione o seguinte:

build.gradle (Módulo :app)

dependencies {
    ...
    implementation project(path: ':common:car-app-service')
    ...
}

Com essa mudança, o gráfico de dependência dos próprios módulos do aplicativo é o seguinte:

Os módulos :app e :common:car-app-service dependem do módulo :common:data. O módulo :app também depende do módulo :common:car-app-service.

Isso agrupa o código que você acabou de escrever no módulo :common:car-app-service com outros componentes incluídos na biblioteca Car App, como a atividade de concessão de permissão fornecida.

Declarar os metadados com.google.android.gms.car.application

  1. Clique com o botão direito do mouse no módulo :common:car-app-service, selecione a opção New > Android Resource File e substitua os seguintes valores:
  • Nome do arquivo: automotive_app_desc.xml
  • Tipo de recurso: XML
  • Elemento raiz: automotiveApp

Assistente de novo arquivo de recursos com os valores definidos conforme descrito nesta etapa.

  1. Nesse arquivo, adicione o elemento <uses> a seguir para declarar que seu aplicativo usa os modelos fornecidos pela biblioteca Car App.

automotive_app_desc.xml

<?xml version="1.0" encoding="utf-8"?>
<automotiveApp>
    <uses name="template"/>
</automotiveApp>
  1. No arquivo AndroidManifest.xml do módulo :app, adicione o seguinte elemento <meta-data> que faz referência ao arquivo automotive_app_desc.xml recém-criado.

AndroidManifest.xml (:app)

<application ...>

    <meta-data
        android:name="com.google.android.gms.car.application"
        android:resource="@xml/automotive_app_desc" />

    ...

</application>

Esse arquivo é lido pelo Android Auto e informa com quais recursos seu aplicativo é compatível. Neste caso, mostra que ele usa o sistema de modelos da biblioteca Car App. Essas informações são usadas para lidar com comportamentos como adicionar o aplicativo à tela de início do Android Auto e abri-lo nas notificações.

Opcional: detectar as mudanças de projeção

Às vezes, você quer saber se o dispositivo de um usuário está ou não conectado a um carro. Para fazer isso, use a API CarConnection. Ela fornece LiveData que mostram o estado da conexão.

  1. Para usar a API CarConnection, primeiro adicione uma dependência ao módulo :app no artefato androidx.car.app:app.

build.gradle (módulo :app)

dependencies {
    ...
    implementation "androidx.car.app:app:$car_app_library_version"
    ...
}
  1. Para fins de demonstração, você pode criar um elemento combinável simples, como o seguinte, que exibe o estado atual da conexão. Em um aplicativo real, esse estado pode ser capturado em algum registro, usado para desabilitar uma funcionalidade na tela do smartphone durante a projeção ou qualquer outra atividade.

MainActivity.kt

@Composable
fun ProjectionState(carConnectionType: Int, modifier: Modifier = Modifier) {
    val text = when (carConnectionType) {
        CarConnection.CONNECTION_TYPE_NOT_CONNECTED -> "Not projecting"
        CarConnection.CONNECTION_TYPE_NATIVE -> "Running on Android Automotive OS"
        CarConnection.CONNECTION_TYPE_PROJECTION -> "Projecting"
        else -> "Unknown connection type"
    }

    Text(
        text = text,
        style = MaterialTheme.typography.bodyMedium,
        modifier = modifier
    )
}
  1. Agora que há uma maneira de exibir os dados, leia-os e transmita-os para o elemento combinável, conforme demonstrado no snippet a seguir.

MainActivity.kt

setContent {
    val carConnectionType by CarConnection(this).type.observeAsState(initial = -1)
    PlacesTheme {
        // A surface container using the 'background' color from the theme
        Surface(
            modifier = Modifier.fillMaxSize(),
            color = MaterialTheme.colorScheme.background
        ) {
            Column {
                Text(
                    text = "Places",
                    style = MaterialTheme.typography.displayLarge,
                    modifier = Modifier.padding(8.dp)
                )
                ProjectionState(
                    carConnectionType = carConnectionType,
                    modifier = Modifier.padding(8.dp)
                )
                PlaceList(places = PlacesRepository().getPlaces())
            }
        }
    }
}
  1. Se você executar o app, será exibida a mensagem Not projecting.

Agora há uma linha adicional de texto na tela para o estado de projeção &quot;Not projecting&quot;

7. Testar com a unidade principal da área de trabalho (DHU, na sigla em inglês)

Com o CarAppService implementado e a configuração do Android Auto em vigor, é hora de executar o app e ver como ele fica.

  1. Instale o aplicativo em seu smartphone e siga as instruções de como instalar e executar a DHU.

Com a DHU em funcionamento, deve aparecer o ícone do app na tela de início. Caso contrário, verifique se você seguiu todas as etapas da seção anterior, depois saia e reinicie a DHU no terminal.

  1. Abrir o app na tela de início

A tela de início do Android Auto mostrando a grade de apps, incluindo o aplicativo Places.

Ops, travou!

Há uma tela de erro com a mensagem &quot;O Android Auto encontrou um erro inesperado&quot;. Há um botão para ativar/desativar a depuração no canto superior direito da tela.

  1. Para ver por que o aplicativo travou, ative o ícone de depuração no canto superior direito (visível apenas ao executar na DHU) ou verifique o Logcat no Android Studio.

A mesma tela de erro da figura anterior, mas agora com o botão de alternância de depuração ativado. Um stack trace é exibido na tela.

Error: [type: null, cause: null, debug msg: java.lang.IllegalArgumentException: Min API level not declared in manifest (androidx.car.app.minCarApiLevel)
        at androidx.car.app.AppInfo.retrieveMinCarAppApiLevel(AppInfo.java:143)
        at androidx.car.app.AppInfo.create(AppInfo.java:91)
        at androidx.car.app.CarAppService.getAppInfo(CarAppService.java:380)
        at androidx.car.app.CarAppBinder.getAppInfo(CarAppBinder.java:255)
        at androidx.car.app.ICarApp$Stub.onTransact(ICarApp.java:182)
        at android.os.Binder.execTransactInternal(Binder.java:1285)
        at android.os.Binder.execTransact(Binder.java:1244)
]

No registro, observe que há uma declaração ausente no manifesto para o nível mínimo da API compatível com o aplicativo. Antes de adicionar essa entrada, é melhor entender por que ela é necessária.

Assim como o próprio Android, a biblioteca Car App tem um conceito de níveis de API, porque é necessário haver um contrato entre os aplicativos host e cliente para que eles se comuniquem. Os aplicativos host são compatíveis com um determinado nível de API e seus recursos associados (e com recursos de níveis anteriores também). Por exemplo, o SignInTemplate pode ser usado em hosts que executam a API de nível 2 ou superior. No entanto, se você tentasse usá-lo em um host compatível apenas com o nível 1 da API, esse host não conheceria o tipo de modelo e não conseguiria fazer nada significativo com ele.

Durante o processo de vinculação do host ao cliente, deve haver alguma sobreposição nos níveis de API compatíveis para que a vinculação seja bem-sucedida. Por exemplo, se um host for compatível apenas com a API de nível 1, mas um aplicativo cliente não puder ser executado sem recursos da API de nível 2 (conforme indicado por essa declaração de manifesto), os apps não deverão se conectar porque o cliente não poderá ser executado no host. Assim, o nível mínimo de API necessário deve ser declarado pelo cliente no manifesto para garantir que apenas um host compatível esteja vinculado a ele.

  1. Para definir o nível mínimo de API com suporte, adicione o seguinte elemento <meta-data> ao arquivo AndroidManfiest.xml do módulo :common:car-app-service:

AndroidManifest.xml (:common:car-app-service)

<application>
    <meta-data
        android:name="androidx.car.app.minCarApiLevel"
        android:value="1" />
    <service android:name="com.example.places.carappservice.PlacesCarAppService" ...>
        ...
    </service>
</application>
  1. Instale o app novamente e inicie-o na DHU. Em seguida, você verá o seguinte:

O aplicativo mostra uma tela básica &quot;Hello World&quot;

Por uma questão de integridade, você também pode tentar definir o minCarApiLevel como um valor grande (por exemplo, 100) para ver o que acontece quando você tenta iniciar o aplicativo se o host e o cliente não forem compatíveis (dica: ele trava, semelhante a quando nenhum valor é definido).

Também é importante observar que, assim como no próprio Android, você pode usar recursos de uma API superior ao mínimo declarado se verificar em tempo de execução se o host dá suporte ao nível necessário.

Opcional: detectar as mudanças de projeção

  • Se você adicionou o listener CarConnection na etapa anterior, deverá ver a atualização de estado em seu smartphone quando a DHU estiver em execução, conforme mostrado abaixo:

A linha de texto que exibe o estado de projeção agora mostra &quot;Projecting&quot;, porque o telefone está conectado à DHU.

8. Adicionar suporte ao Android Automotive OS

Com o Android Auto em funcionamento, é hora de ir além e oferecer suporte ao Android Automotive OS também.

Criar o módulo :automotive

  1. Para criar um módulo que contém o código específico da versão do app Android Automotive OS, abra File > New > New Module... no Android Studio, selecione a opção Automotive na lista de tipos de modelo à esquerda e use os seguintes valores:
  • Nome do aplicativo/biblioteca: Places (o mesmo que o aplicativo principal, mas você também pode escolher um nome diferente, se quiser)
  • Nome do módulo: automotive
  • Nome do pacote: com.example.places.automotive
  • Idioma: Kotlin
  • SDK mínimo: API 29: Android 10.0 (Q), conforme já mencionado ao criar o módulo :common:car-app-service, todos os veículos Android Automotive OS compatíveis com aplicativos da biblioteca Car App executam pelo menos a API 29.

O assistente de criação do módulo Android Automotive OS mostrando os valores listados nesta etapa.

  1. Clique em Next e selecione No Activity na próxima tela antes de clicar em Finish.

A segunda página do assistente para criar novo módulo. Três opções são mostradas, &quot;No Activity&quot;, &quot;Media Service&quot; e &quot;Messaging Service&quot;. A opção &quot;No Activity&quot; é selecionada.

Adicionar dependências

Assim como no Android Auto, é preciso declarar uma dependência do módulo :common:car-app-service. Dessa forma, você pode compartilhar sua implementação nas duas plataformas.

Além disso, adicione uma dependência ao artefato androidx.car.app:app-automotive. Ao contrário do artefato androidx.car.app:app-projected, que é opcional para o Android Auto, essa dependência é necessária no Android Automotive OS, porque inclui a CarAppActivity usada para executar seu app.

  1. Para adicionar dependências, abra o arquivo build.gradle e insira o seguinte código:

build.gradle (módulo :automotive)

dependencies {
    ...
    implementation project(':common:car-app-service')
    implementation "androidx.car.app:app-automotive:$car_app_library_version"
    ...
}

Com essa mudança, o gráfico de dependência dos próprios módulos do aplicativo é o seguinte:

Os módulos :app e :common:car-app-service dependem do módulo :common:data. Os módulos :app e :automotive dependem do módulo :common:car-app-service.

Configurar o manifesto

  1. Primeiro, você precisa declarar dois recursos, android.hardware.type.automotive e android.software.car.templates_host, como obrigatórios.

android.hardware.type.automotive é um recurso do sistema que indica que o dispositivo é um veículo (consulte FEATURE_AUTOMOTIVE para mais detalhes). Somente os apps que marcam esse recurso como obrigatório podem ser enviados para uma faixa do Automotive OS no Play Console (os apps enviados para outras faixas não podem exigir esse recurso). android.software.car.templates_host é um recurso do sistema presente apenas em veículos que têm o host necessário para executar aplicativos de modelo.

AndroidManifest.xml (:automotive)

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-feature
        android:name="android.hardware.type.automotive"
        android:required="true" />
    <uses-feature
        android:name="android.software.car.templates_host"
        android:required="true" />
    ...
</manifest>
  1. Em seguida, você precisa declarar alguns recursos como não obrigatórios.

Isso garante que seu app seja compatível com a variedade de hardware disponível em carros com o Google integrado. Por exemplo, se o aplicativo exigir o recurso android.hardware.screen.portrait, ele não será compatível com veículos em que a tela está no modo paisagem, porque a orientação é fixa na maioria dos carros. É por isso que o atributo android:required é definido como false para esses recursos.

AndroidManifest.xml (:automotive)

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    ...
    <uses-feature
        android:name="android.hardware.wifi"
        android:required="false" />
    <uses-feature
        android:name="android.hardware.screen.portrait"
        android:required="false" />
    <uses-feature
        android:name="android.hardware.screen.landscape"
        android:required="false" />
    <uses-feature
        android:name="android.hardware.camera"
        android:required="false" />
    ...
</manifest>
  1. Em seguida, você precisa adicionar uma referência ao arquivo automotive_app_desc.xml como fez para o Android Auto.

Agora, o atributo android:name é diferente: em vez de com.google.android.gms.car.application, é com.android.automotive. Assim como antes, isso faz referência ao arquivo automotive_app_desc.xml do módulo :common:car-app-service, o que significa que o mesmo recurso é usado no Android Auto e no Android Automotive OS. O elemento <meta-data> está dentro do elemento <application>, portanto, desative o fechamento automático da tag application.

AndroidManifest.xml (:automotive)

<application>
    ...
    <meta-data android:name="com.android.automotive"
        android:resource="@xml/automotive_app_desc"/>
    ...
</application>
  1. Por fim, você precisa adicionar um elemento <activity> para a CarAppActivity incluída na biblioteca.

AndroidManifest.xml (:automotive)

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    ...
    <application ...>
        ...
        <activity
            android:name="androidx.car.app.activity.CarAppActivity"
            android:exported="true"
            android:launchMode="singleTask"
            android:theme="@android:style/Theme.DeviceDefault.NoActionBar">

            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>

            <meta-data
                android:name="distractionOptimized"
                android:value="true" />
        </activity>
    </application>
</manifest>

Confira o que tudo isso faz:

  • android:name lista o nome da classe totalmente qualificada da classe CarAppActivity do pacote app-automotive.
  • android:exported está definido como true porque essa Activity precisa ser iniciada por um aplicativo diferente de si mesmo (app de tela de início).
  • android:launchMode está definido como singleTask para só haver uma instância da CarAppActivity por vez.
  • android:theme é definido como @android:style/Theme.DeviceDefault.NoActionBar para que o app ocupe todo o espaço disponível em tela cheia.
  • O filtro de intent indica que essa é a Activity de acesso rápido do app.
  • Há um elemento <meta-data> que informa ao sistema que o app pode ser usado enquanto as restrições de UX estão em vigor, como quando o veículo está em movimento.

Opcional: copiar os ícones na tela de início do módulo :app

Como você acabou de criar o módulo :automotive, ele tem os ícones verdes padrão do logotipo do Android.

  • Se quiser, copie e cole o diretório de recursos mipmap do módulo :app no módulo :automotive para usar os mesmos ícones na tela de início que o app para dispositivos móveis.

9. Testar com o emulador do Android Automotive OS

Instalar as imagens do sistema do Automotive com a Play Store

  1. Primeiro, abra o SDK Manager no Android Studio e selecione a guia SDK Platforms, se ela ainda não estiver selecionada. No canto inferior direito da janela do SDK Manager, verifique se a caixa Show package details está marcada.
  2. Instale uma ou mais das seguintes imagens do emulador. As imagens só podem ser executadas em máquinas com a mesma arquitetura (x86/ARM) que elas.
  • Android 12L > Automotive with Play Store Intel x86 Atom_64 System Image
  • Android 12L > Automotive with Play Store ARM 64 v8a System Image
  • Android 11 > Automotive with Play Store Intel x86 Atom_64 System Image
  • Android 10 > Automotive with Play Store Intel x86 Atom_64 System Image

Criar um Dispositivo Virtual Android para o Android Automotive OS

  1. Depois de abrir o Gerenciador de dispositivos, selecione Automotive na coluna Category no lado esquerdo da janela. Em seguida, selecione a definição de dispositivo Automotive (1024p landscape) na lista e clique em Next.

O assistente Virtual Device Configuration mostrando o perfil de hardware &quot;Automotive (1024p landscape)&quot; selecionado.

  1. Na próxima página, selecione uma imagem do sistema da etapa anterior. Se você escolheu a imagem do Android 11/API 30, ela pode estar na guia x86 Images, e não na guia Recommended padrão. Clique em Next e selecione qualquer opção avançada antes de clicar em Finish para criar o AVD.

Executar o app

  1. Execute o app no emulador recém-criado usando a configuração de execução do automotive.

As

Ao executar o aplicativo pela primeira vez, pode ser exibida uma tela como a seguinte:

O app exibe uma tela &quot;System update required&quot; com um botão &quot;Check for updates&quot; abaixo.

Se esse for o caso, clique no botão Check for updates, que leva você à página da Play Store do aplicativo Google Automotive App Host. Depois, clique no botão Instalar. Se você não tiver feito login antes de clicar no botão Check for updates, será exibido o fluxo de entrada. Após o login, você pode abrir o app novamente para clicar no botão e voltar à página da Play Store.

A página da Play Store do Google Automotive App Host, em que há um botão &quot;Instalar&quot; no canto superior direito.

  1. Por fim, com o host instalado, abra de novo o aplicativo na tela de início (o ícone de grade de nove pontos na linha inferior) e você verá o seguinte:

O aplicativo mostra uma tela básica &quot;Hello World&quot;

Na próxima etapa, você fará mudanças no módulo :common:car-app-service para exibir a lista de lugares e permitir que o usuário inicie a navegação para um local escolhido em outro app.

10. Adicionar um mapa e uma tela de detalhes

Adicionar um mapa à tela principal

  1. Para começar, substitua o código no método onGetTemplate da classe MainScreen pelo seguinte:

MainScreen.kt

override fun onGetTemplate(): Template {
    val placesRepository = PlacesRepository()
    val itemListBuilder = ItemList.Builder()
        .setNoItemsMessage("No places to show")

    placesRepository.getPlaces()
        .forEach {
            itemListBuilder.addItem(
                Row.Builder()
                    .setTitle(it.name)
                    // Each item in the list *must* have a DistanceSpan applied to either the title
                    // or one of the its lines of text (to help drivers make decisions)
                    .addText(SpannableString(" ").apply {
                        setSpan(
                            DistanceSpan.create(
                                Distance.create(Math.random() * 100, Distance.UNIT_KILOMETERS)
                            ), 0, 1, Spannable.SPAN_INCLUSIVE_INCLUSIVE
                        )
                    })
                    .setOnClickListener { TODO() }
                    // Setting Metadata is optional, but is required to automatically show the
                    // item's location on the provided map
                    .setMetadata(
                        Metadata.Builder()
                            .setPlace(Place.Builder(CarLocation.create(it.latitude, it.longitude))
                                // Using the default PlaceMarker indicates that the host should
                                // decide how to style the pins it shows on the map/in the list
                                .setMarker(PlaceMarker.Builder().build())
                                .build())
                            .build()
                    ).build()
            )
        }

    return PlaceListMapTemplate.Builder()
        .setTitle("Places")
        .setItemList(itemListBuilder.build())
        .build()
}

Esse código lê a lista de instâncias de Place no PlacesRepository e converte cada uma delas em uma Row a ser adicionada à ItemList exibida pelo PlaceListMapTemplate.

  1. Execute o aplicativo de novo (em uma ou ambas as plataformas) para ver o resultado.

Android Auto

Android Automotive OS

Outro stack trace é mostrado devido a um erro

O aplicativo trava e o usuário é levado de volta para a tela de início após abri-lo.

Ops, outro erro: parece que está faltando uma permissão.

java.lang.SecurityException: The car app does not have a required permission: androidx.car.app.MAP_TEMPLATES
        at android.os.Parcel.createExceptionOrNull(Parcel.java:2373)
        at android.os.Parcel.createException(Parcel.java:2357)
        at android.os.Parcel.readException(Parcel.java:2340)
        at android.os.Parcel.readException(Parcel.java:2282)
        ...
  1. Para corrigir o erro, adicione o seguinte elemento <uses-permission> no manifesto do módulo :common:car-app-service.

Essa permissão precisa ser declarada por qualquer aplicativo que use o PlaceListMapTemplate. Caso contrário, o aplicativo falha, conforme demonstrado. Apenas os apps que declaram sua categoria como androidx.car.app.category.POI podem usar esse modelo e, por sua vez, essa permissão.

AndroidManifest.xml (:common:car-app-service)

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-permission android:name="androidx.car.app.MAP_TEMPLATES" />
    ...
</manifest>

Se você executar o app depois de adicionar a permissão, ele deverá ter a seguinte aparência em cada plataforma:

Android Auto

Android Automotive OS

Uma lista de locais é mostrada no lado esquerdo da tela, e um mapa com pinos correspondentes aos locais é mostrado atrás dela, preenchendo o restante da tela.

Uma lista de locais é mostrada no lado esquerdo da tela, e um mapa com pinos correspondentes aos locais é mostrado atrás dela, preenchendo o restante da tela.

O host do aplicativo cuida da renderização do mapa quando você fornece os Metadata necessários.

Adicionar uma tela de detalhes

A seguir, vamos adicionar uma tela de detalhes para que os usuários vejam mais informações sobre um local específico e tenham a opção de navegar até esse local com o app de navegação preferido ou retornar à lista de outros lugares. Isso pode ser feito usando o PaneTemplate, que permite exibir até quatro linhas de informações ao lado de botões de ação opcionais.

  1. Primeiro, clique com o botão direito do mouse no diretório res do módulo :common:car-app-service, clique em New > Vector Asset e crie um ícone de navegação usando a seguinte configuração:
  • Tipo de recurso: Clip art
  • Clip art: navigation
  • Nome: baseline_navigation_24
  • Tamanho: 24 dp por 24 dp
  • Cor: #000000
  • Opacidade: 100%

Assistente do Asset Studio mostrando as entradas mencionadas nesta etapa

  1. Em seguida, no pacote screen, crie um arquivo chamado DetailScreen.kt (ao lado do arquivo MainScreen.kt) e adicione o seguinte código:

DetailScreen.kt

class DetailScreen(carContext: CarContext, private val placeId: Int) : Screen(carContext) {

    override fun onGetTemplate(): Template {
        val place = PlacesRepository().getPlace(placeId)
            ?: return MessageTemplate.Builder("Place not found")
                .setHeaderAction(Action.BACK)
                .build()

        val navigateAction = Action.Builder()
            .setTitle("Navigate")
            .setIcon(
                CarIcon.Builder(
                    IconCompat.createWithResource(
                        carContext,
                        R.drawable.baseline_navigation_24
                    )
                ).build()
            )
            // Only certain intent actions are supported by `startCarApp`. Check its documentation
            // for all of the details. To open another app that can handle navigating to a location
            // you must use the CarContext.ACTION_NAVIGATE action and not Intent.ACTION_VIEW like
            // you might on a phone.
            .setOnClickListener {  carContext.startCarApp(place.toIntent(CarContext.ACTION_NAVIGATE)) }
            .build()

        return PaneTemplate.Builder(
            Pane.Builder()
                .addAction(navigateAction)
                .addRow(
                    Row.Builder()
                        .setTitle("Coordinates")
                        .addText("${place.latitude}, ${place.longitude}")
                        .build()
                ).addRow(
                    Row.Builder()
                        .setTitle("Description")
                        .addText(place.description)
                        .build()
                ).build()
        )
            .setTitle(place.name)
            .setHeaderAction(Action.BACK)
            .build()
    }
}

Preste atenção especial em como a navigateAction é construída: a chamada para o startCarApp no OnClickListener é a chave para interagir com outros aplicativos no Android Auto e no Android Automotive OS.

Agora que existem dois tipos de telas, é hora de adicionar navegação entre elas. A navegação na biblioteca Car App usa um modelo de pilha de envio e destaque que é ideal para os fluxos de tarefas simples adequados para realização ao dirigir.

Uma representação de diagrama da maneira como a navegação no aplicativo funciona com a biblioteca Car App. À esquerda, há uma pilha com apenas uma MainScreen. Entre ela e a pilha central, há uma seta chamada &quot;Push DetailScreen&quot;. A pilha central tem uma DetailScreen na parte de cima da MainScreen. Entre a pilha central e a pilha direita, há uma seta chamada &quot;Pop&quot;. A pilha direita é a mesma que a esquerda, apenas uma MainScreen.

  1. Para navegar de um dos itens da lista na MainScreen até uma DetailScreen desse item, adicione o seguinte código:

MainScreen.kt

Row.Builder()
    ...
    .setOnClickListener { screenManager.push(DetailScreen(carContext, it.id)) }
    ...

A navegação de volta de uma DetailScreen para a MainScreen já foi tratada, porque setHeaderAction(Action.BACK) é chamado ao criar o PaneTemplate exibido na DetailScreen. Quando um usuário clica na ação de cabeçalho, o host lida com a retirada da tela atual da pilha, mas esse comportamento pode ser substituído pelo seu app, se desejado.

  1. Execute o aplicativo agora para ver a DetailScreen e a navegação no aplicativo em ação.

11. Atualizar o conteúdo em uma tela

Muitas vezes, você quer permitir que um usuário interaja com uma tela e mude o estado dos elementos. Para demonstrar como fazer isso, crie uma funcionalidade que permita aos usuários alternar entre adicionar e remover um lugar dos favoritos na DetailScreen.

  1. Primeiro, adicione uma variável local, isFavorite, que contém o estado. Em um aplicativo real, isso deve ser armazenado como parte da camada de dados, mas uma variável local é suficiente para fins de demonstração.

DetailScreen.kt

class DetailScreen(carContext: CarContext, private val placeId: Int) : Screen(carContext) {
    private var isFavorite = false
    ...
}
  1. Depois, clique com o botão direito do mouse no diretório res do módulo :common:car-app-service, clique em New > Vector Asset e crie um ícone de favorito usando a seguinte configuração:
  • Tipo de recurso: Clip art
  • Nome: baseline_favorite_24
  • Clip art: favorite
  • Tamanho: 24 dp por 24 dp
  • Cor: #000000
  • Opacidade: 100%

Assistente do Asset Studio mostrando as entradas mencionadas nesta etapa

  1. Depois, em DetailsScreen.kt, crie uma ActionStrip para o PaneTemplate.

Os componentes da UI de ActionStrip são colocados na linha de cabeçalho oposta ao título e são ideais para ações secundárias e terciárias. Como a navegação é a principal ação a ser realizada na DetailScreen, colocar a Action de adicionar ou remover dos favoritos em uma ActionStrip é uma ótima maneira de estruturar a tela.

DetailScreen.kt

val navigateAction = ...

val actionStrip = ActionStrip.Builder()
    .addAction(
        Action.Builder()
            .setIcon(
                CarIcon.Builder(
                    IconCompat.createWithResource(
                        carContext,
                        R.drawable.baseline_favorite_24
                    )
                ).setTint(
                    if (isFavorite) CarColor.RED else CarColor.createCustom(
                        Color.LTGRAY,
                        Color.DKGRAY
                    )
                ).build()
            )
            .setOnClickListener {
                isFavorite = !isFavorite
            }.build()
    )
    .build()

...

Há dois pontos importantes aqui:

  • A cor do CarIcon depende do estado do item.
  • setOnClickListener é usado para reagir às entradas do usuário e alternar o estado favorito.
  1. Não se esqueça de chamar setActionStrip no PaneTemplate.Builder para usá-la.

DetailScreen.kt

return PaneTemplate.Builder(...)
    ...
    .setActionStrip(actionStrip)
    .build()
  1. Agora, execute o app e veja o que acontece:

A DetailScreen é mostrada. O usuário está tocando no ícone de favorito, mas ele não muda de cor conforme o esperado.

Interessante... parece que os cliques estão acontecendo, mas a interface não é atualizada.

Isso porque a biblioteca Car App tem um conceito de atualizações. Para limitar a distração do motorista, a atualização do conteúdo na tela tem certas limitações, que variam de acordo com o modelo em exibição. Cada atualização precisa ser solicitada explicitamente pelo seu código chamando o método invalidate da classe Screen. Apenas atualizar algum estado referenciado em onGetTemplate não é suficiente para atualizar a UI.

  1. Para corrigir esse problema, atualize o OnClickListener da seguinte maneira:

DetailScreen.kt

.setOnClickListener {
    isFavorite = !isFavorite
    // Request that `onGetTemplate` be called again so that updates to the
    // screen's state can be picked up
    invalidate()
}
  1. Execute o app novamente para ver se a cor do ícone de coração é atualizada a cada clique.

A DetailScreen é mostrada. O usuário está tocando no ícone de favorito, que agora muda de cor conforme o esperado.

Agora, você tem um aplicativo básico bem integrado ao Android Auto e ao Android Automotive OS.

12. Parabéns

Você criou seu primeiro aplicativo da biblioteca Car App. Agora é hora de aplicar em seu próprio aplicativo o que você aprendeu.

Como mencionado anteriormente, apenas algumas categorias criadas usando os aplicativos da biblioteca Car App podem ser enviadas para a Play Store no momento. No caso de um app de navegação, de ponto de interesse (PDI) (como aquele com que você trabalhou neste codelab) ou de Internet das Coisas (IoT), você poderá começar a criar hoje mesmo e lançar o app até a produção nas duas plataformas.

Novas categorias de aplicativos são adicionadas todos os anos, portanto, mesmo que você não possa aplicar imediatamente o que aprendeu, volte depois e talvez seja a hora certa de estender seu app para o carro.

O que testar

  • Instale um emulador do OEM (por exemplo, o Polestar 2) e veja como a personalização do OEM pode mudar a aparência dos apps da biblioteca Car App no Android Automotive OS. Nem todos os emuladores do OEM são compatíveis com aplicativos dessa biblioteca.
  • Confira o aplicativo de exemplo Showcase, que demonstra a funcionalidade completa da biblioteca Car App.

Leia mais

Documentos de referência