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 é 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 (o seu) 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.

Sessão

A Session é como uma instância de um app cliente em execução na tela do veículo. Assim 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.

Tela

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 aprender tudo sobre essas classes ao escrever um CarAppService, Session e Screen na seção Escrever o CarAppService deste codelab.

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 libs.version.toml, adicione entradas para o artefato androidx.car.app:app.

libs.version.toml

[versions]
...
carApp = "1.7.0-rc01"

[libraries]
...
androidx-car-app = { group = "androidx.car.app", name = "app", version.ref = "carApp" }
  1. Em seguida, adicione duas dependências ao arquivo build.gradle.kts 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.kts (módulo :common:car-app-service)

dependencies {
    ...
    implementation(libs.androidx.car.app)
    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

import androidx.car.app.CarAppService
import androidx.car.app.Session
import androidx.car.app.SessionInfo
import androidx.car.app.validation.HostValidator

...

class PlacesCarAppService : CarAppService() {

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

   override fun onCreateSession(sessionInfo: SessionInfo): 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 app (ponto de interesse, neste caso), que determina quais critérios de qualidade ele precisa satisfazer (veja detalhes sobre isso mais adiante). Outros valores possíveis estão detalhados em Categorias de apps compatíveis.

Criar a classe PlacesSession

  • Em PlacesCarAppService.kt, adicione o seguinte código:

PlacesCarAppService.kt

import android.content.Intent
import androidx.car.app.Screen

...

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, uma simples mensagem Hello, world! é mostrada usando o PaneTemplate.

MainScreen.kt

import androidx.car.app.CarContext
import androidx.car.app.Screen
import androidx.car.app.model.Action
import androidx.car.app.model.Header
import androidx.car.app.model.Pane
import androidx.car.app.model.PaneTemplate
import androidx.car.app.model.Row
import androidx.car.app.model.Template

...

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)
            .setHeader(
                Header.Builder()
                    .setStartHeaderAction(Action.APP_ICON)
                    .build()
            ).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.kts do módulo :app, adicione o seguinte:

build.gradle.kts (módulo :app)

dependencies {
    ...
    implementation(project(":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.kts (módulo :app)

dependencies {
    ...
    implementation(libs.androidx.car.app)
    ...
}
  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

import androidx.car.app.connection.CarConnection

...

@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

import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState

...

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 saber por que o app travou, verifique o Logcat no Android Studio. Talvez seja necessário remover o filtro padrão do Logcat para package:mine e substituí-lo por is:error.
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): todos os veículos Android Automotive OS compatíveis com apps 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. Primeiro, adicione uma entrada para o artefato androidx.car.app:app-automotive em libs.versions.toml.

libs.version.toml

[libraries]
...
androidx-car-app-automotive = { group = "androidx.car.app", name = "app-automotive", version.ref = "carApp"}
  1. Para adicionar dependências, abra o arquivo build.gradle.kts e insira o seguinte código:

build.gradle.kts (módulo :automotive)

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

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. Para este codelab, essas mudanças são suficientes. Ao criar seu próprio app, verifique se ele atende a todos os requisitos de recursos do Google Play para o Android Automotive OS.

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 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. Além disso, o elemento <meta-data> está dentro do <application>, então 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

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 (1408p landscape) with Google Play 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 a imagem do sistema da API 34 (ela será salva, caso ainda não tenha sido) e crie o AVD clicando em Finish.

c9631de84fc6e835.png

Executar o app

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

As

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

import androidx.car.app.model.CarLocation
import androidx.car.app.model.Distance
import androidx.car.app.model.DistanceSpan
import androidx.car.app.model.ItemList
import androidx.car.app.model.Metadata
import androidx.car.app.model.Place
import androidx.car.app.model.PlaceListMapTemplate
import androidx.car.app.model.PlaceMarker
import com.example.places.data.PlacesRepository

...

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

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

import android.graphics.Color
import androidx.car.app.CarContext
import androidx.car.app.Screen
import androidx.car.app.model.Action
import androidx.car.app.model.CarColor
import androidx.car.app.model.CarIcon
import androidx.car.app.model.Header
import androidx.car.app.model.MessageTemplate
import androidx.car.app.model.Pane
import androidx.car.app.model.PaneTemplate
import androidx.car.app.model.Row
import androidx.car.app.model.Template
import androidx.core.graphics.drawable.IconCompat
import com.example.android.cars.carappservice.R
import com.example.places.data.PlacesRepository
import com.example.places.data.model.toIntent

class DetailScreen(carContext: CarContext, private val placeId: Int) : Screen(carContext) {
    private var isBookmarked = false

    override fun onGetTemplate(): Template {
        val place = PlacesRepository().getPlace(placeId)
            ?: return MessageTemplate.Builder("Place not found")
                .setHeader(
                    Header.Builder()
                        .setStartHeaderAction(Action.BACK)
                        .build()
                )
                .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()
        ).setHeader(
            Header.Builder()
                .setStartHeaderAction(Action.BACK)
                .setTitle(place.name)
                .build()
        ).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 app 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 adicionar ou remover um lugar dos favoritos na DetailScreen.

  1. Primeiro, adicione uma variável local, isBookmarked, 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 isBookmarked = 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 dois ícones de favoritos usando a seguinte configuração:
  • Tipo de recurso: Clip art
  • Nome: outline_bookmark_add_24, outline_bookmark_added_24
  • Clip art: bookmark, bookmark_added (do conjunto de origem de símbolos do Material Design com contorno)
  • Tamanho: 24 dp por 24 dp
  • Cor: #000000
  • Opacidade: 100%
  1. Em seguida, em DetailsScreen.kt, crie uma Action para a funcionalidade de inclusão nos favoritos.

DetailScreen.kt

val navigateAction = ...

val bookmarkAction = Action.Builder()
    .setIcon(
        CarIcon.Builder(
            IconCompat.createWithResource(
                carContext,
                if (isBookmarked) R.drawable.outline_bookmark_added_24 else R.drawable.outline_bookmark_add_24
            )
        ).build()
    )
    .setOnClickListener {
        isBookmarked = !isBookmarked
    }.build()

...

Há dois pontos importantes aqui:

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

DetailScreen.kt

Header.Builder()
    ...
    .addEndHeaderAction(bookmarkAction)
    .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.

Os cliques estão sendo recebidos, mas o ícone não está mudando.

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 para o bookmarkAction da seguinte maneira:

DetailScreen.kt

.setOnClickListener {
    isBookmarked = !isBookmarked
    // Request that `onGetTemplate` be called again so that updates to the
    // screen's state can be picked up
    invalidate()
}
  1. Execute o app de novo para conferir se o ícone é atualizado quando clicado.

96da7958632706bb.gif

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. Se o app estiver em uma das categorias compatíveis, você já pode começar a criar.

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

Leia mais

Documentos de referência