Criar e testar um app do Android Automotive OS para carros estacionados

1. Antes de começar

O que este codelab não é

  • Um guia sobre como criar apps de música (áudio, por exemplo, músicas, rádios, podcasts) para o Android Auto e o Android Automotive OS. Consulte Criar apps de música para carros para saber como criar esses apps.

O que é necessário

O que você vai criar

Neste codelab, você vai aprender a migrar um app para dispositivos móveis de stream de vídeo, chamado Road Reels, para o Android Automotive OS.

A versão inicial do app em execução em um smartphone

A versão completa do app em execução em um emulador do Android Automotive OS com corte da tela.

A versão inicial do app em execução em um smartphone

A versão completa do app em execução em um emulador do Android Automotive OS com corte da tela.

O que você vai aprender

  • Como usar o emulador do Android Automotive OS
  • Como fazer as mudanças necessárias para criar um build do Android Automotive OS
  • Suposições comuns feitas ao desenvolver apps para dispositivos móveis que podem ser violadas quando um app é executado no Android Automotive OS
  • Os diferentes níveis de qualidade para apps em carros
  • Como usar a sessão de mídia para permitir que outros apps controlem a reprodução do seu app
  • Como a interface do sistema e os encartes de janela podem ser diferentes em dispositivos do Android Automotive OS em comparação com dispositivos móveis

2. Começar a configuração

Acessar o código

  1. O código deste codelab pode ser encontrado no diretório build-a-parked-app 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 build-a-parked-app/start. O diretório build-a-parked-app/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.

3. Aprender sobre apps do Android Automotive OS para carros estacionados

Os apps para carros estacionados são um subconjunto das categorias de apps compatíveis com o Android Automotive OS. No momento, eles consistem em apps de streaming de vídeo, navegadores da Web e jogos. Esses apps são ideais para uso em carros, considerando o hardware presente em veículos com o Google integrado e a crescente prevalência de veículos elétricos, em que o tempo de carregamento é uma ótima oportunidade para motoristas e passageiros interagirem com esses tipos de apps.

De muitas maneiras, os carros são semelhantes a outros dispositivos de tela grande, como tablets e dobráveis. Eles têm telas touch com tamanhos, resoluções e proporções semelhantes e que podem estar na orientação retrato ou paisagem (embora, diferentemente dos tablets, a orientação seja fixa). Também são dispositivos conectados que podem entrar e sair da conexão de rede. Com tudo isso em mente, não é de surpreender que os apps já otimizados para telas grandes geralmente exijam uma quantidade mínima de trabalho para oferecer uma ótima experiência do usuário em carros.

Assim como em telas grandes, também há níveis de qualidade de apps em carros:

  • Nível 3, pronto para carros: o app é compatível com telas grandes e pode ser usado enquanto o carro está estacionado. Embora o app não tenha recursos otimizados para carros, os usuários podem usar o app da mesma forma que faria em qualquer outro dispositivo Android de tela grande. Os apps para dispositivos móveis que satisfazem esses requisitos podem ser distribuídos para carros sem alterações pelo programa de apps para dispositivos móveis criados para carros.
  • Nível 2, otimizado para carros: o app oferece uma ótima experiência na tela central do carro. Para isso, o app precisa ter uma engenharia específica para incluir recursos que possam ser usados nos modos de direção ou estacionado, dependendo da categoria do app.
  • Nível 1, diferenciado para carros: o app é criado para funcionar com diversos tipos de hardware em carros e pode adaptar a experiência nos modos de direção e estacionado. Ele oferece a melhor experiência do usuário projetada para as diferentes telas dos carros, como o console central, o painel de instrumentos e outras telas, como telas panorâmicas encontradas em muitos carros premium.

4. Executar o app no 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 das imagens do emulador do Automotive com a Play Store listadas em Adicionar imagens genéricas do sistema. As imagens só podem ser executadas em máquinas com a mesma arquitetura (x86/ARM) que elas.

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 o perfil de hardware do pacote Automotive (1024p landscape) na lista e clique em Next.

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

  1. Na próxima página, selecione a imagem do sistema da etapa anterior. Clique em Next e selecione qualquer opção avançada que você queira antes de criar o AVD. Para isso, clique em Finish. Observação: se você escolheu a imagem da API 30, ela pode estar em uma guia diferente da Recommended.

Executar o app

Execute o app no emulador recém-criado usando a configuração de execução do app atual. Teste o app para conferir as diferentes telas e comparar o comportamento dele com a execução em um emulador de smartphone ou tablet.

301e6c0d3675e937.png

5. Criar um build do Android Automotive OS

Embora o app "simplesmente funcione", algumas pequenas mudanças precisam ser feitas para que ele funcione bem no Android Automotive OS e satisfaça os requisitos para publicação na Play Store. Nem todas essas mudanças fazem sentido incluir na versão para dispositivos móveis do app. Por isso, primeiro crie uma variante de build do Android Automotive OS.

Adicionar uma dimensão de variações de formato

Para começar, adicione uma dimensão de variações ao formato segmentado pelo build modificando as flavorDimensions no arquivo build.gradle.kts. Em seguida, adicione um bloco productFlavors e variações para cada formato (mobile e automotive).

Para saber mais, consulte Configurar variações de produtos.

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

android {
    ...
    flavorDimensions += "formFactor"
    productFlavors {
        create("mobile") {
            // Inform Android Studio to use this flavor as the default (e.g. in the Build Variants tool window)
            isDefault = true
            // Since there is only one flavor dimension, this is optional
            dimension = "formFactor"
        }
        create("automotive") {
            // Since there is only one flavor dimension, this is optional
            dimension = "formFactor"
            // Adding a suffix makes it easier to differentiate builds (e.g. in the Play Console)
            versionNameSuffix = "-automotive"
        }
    }
    ...
}

Depois de atualizar o arquivo build.gradle.kts, um banner vai aparecer na parte de cima do arquivo informando que "Os arquivos do Gradle mudaram desde a última sincronização do projeto. Uma sincronização de projeto pode ser necessária para que o ambiente de desenvolvimento integrado "funcione corretamente". Clique no botão Sync Now nesse banner para que o Android Studio possa importar essas mudanças na configuração do build.

8685bcde6b21901f.png

Em seguida, abra a janela de ferramentas Build Variants no item de menu Build > Select Build Variant… e selecione a variante automotiveDebug. Isso garante que você veja os arquivos do conjunto de origem automotive na janela Project e que essa variante de build seja usada ao executar o app no Android Studio.

19e4aa8135553f62.png

Criar um manifesto do Android Automotive OS

Em seguida, você vai criar um arquivo AndroidManifest.xml para o conjunto de origem automotive. Esse arquivo contém os elementos necessários dos apps do Android Automotive OS.

  1. Na janela Project, clique com o botão direito do mouse no módulo app. No menu suspenso exibido, selecione New > Other > Android Manifest File
  2. Na janela New Android Component que é aberta, selecione automotive como o Target Source Set do novo arquivo. Clique em Finish para criar o arquivo.

3fe290685a1026f5.png

  1. No arquivo AndroidManifest.xml que acabou de ser criado (no caminho app/src/automotive/AndroidManifest.xml), adicione o seguinte:

AndroidManifest.xml (automotivo)

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <!--  https://developer.android.com/training/cars/parked#required-features  -->
    <uses-feature
        android:name="android.hardware.type.automotive"
        android:required="true" />
    <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" />
</manifest>

A primeira declaração é necessária para fazer upload do artefato do build para a faixa do Android Automotive OS no Play Console. A presença desse recurso é usada pelo Google Play para distribuir o app apenas para dispositivos que tenham o recurso android.hardware.type.automotive, ou seja, carros.

As outras declarações são necessárias para garantir que o app possa ser instalado nas várias configurações de hardware presentes em carros. Para mais detalhes, consulte Recursos necessários do Android Automotive OS.

Marcar como um app de vídeo

O último metadado que precisa ser adicionado é o arquivo automotive_app_desc.xml. Ele é usado para declarar a categoria do app no contexto do Android para carros e é independente da categoria selecionada para o app no Play Console.

  1. Clique com o botão direito do mouse no módulo app, selecione a opção New > Android Resource File e insira os seguintes valores antes de clicar em OK:
  • Nome do arquivo: automotive_app_desc.xml
  • Tipo de recurso: XML
  • Elemento raiz: automotiveApp
  • Conjunto de origem: automotive
  • Nome do diretório: xml

47ac6bf76ef8ad45.png

  1. Nesse arquivo, adicione o seguinte elemento <uses> para declarar que o app é de vídeo.

automotive_app_desc.xml

<?xml version="1.0" encoding="utf-8"?>
<automotiveApp xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <uses
        name="video"
        tools:ignore="InvalidUsesTagAttribute" />
</automotiveApp>
  1. No arquivo AndroidManifest.xml do conjunto de origem automotive (em que você acabou de adicionar os elementos <uses-feature>), adicione um elemento <application> vazio. Nele, adicione o seguinte elemento <meta-data> que faz referência ao arquivo automotive_app_desc.xml que você acabou de criar.

AndroidManifest.xml (automotivo)

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    ...

    <application>
        <meta-data
            android:name="com.android.automotive"
            android:resource="@xml/automotive_app_desc" />
    </application>
</manifest>

Com isso, você fez todas as mudanças necessárias para criar um build do Android Automotive OS do app.

6. Satisfazer os requisitos de qualidade do Android Automotive OS: navegabilidade

Criar uma variante de build do Android Automotive OS faz parte do processo de levar seu app para carros. No entanto, ainda é necessário garantir que ele seja utilizável e seguro.

Adicionar affordances de navegação

Ao executar o app no emulador do Android Automotive OS, você pode ter notado que não era possível voltar da tela de detalhes para a tela principal ou da tela do player para a tela de detalhes. Diferente de outros formatos, que podem exigir um botão "Voltar" ou um gesto de toque para ativar a navegação de retorno, esse requisito não existe para dispositivos Android Automotive OS. Dessa forma, os apps precisam fornecer affordances de navegação na interface para garantir que os usuários possam navegar sem ficar presos em uma tela do app. Esse requisito é codificado como a diretriz de qualidade AN-1.

Para oferecer suporte à navegação de retorno da tela de detalhes para a tela principal, adicione outro parâmetro navigationIcon à CenterAlignedTopAppBar da tela de detalhes da seguinte maneira:

RoadReelsApp.kt

import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton

...

navigationIcon = {
    IconButton(onClick = { navController.popBackStack() }) {
        Icon(
            Icons.AutoMirrored.Filled.ArrowBack,
            contentDescription = null
        )
    }
}

Para oferecer suporte à navegação de retorno da tela do player para a tela principal:

  1. Atualize o elemento combinável TopControls para usar um parâmetro de callback chamado onClose e adicione um IconButton que o chame quando clicado.

PlayerControls.kt

@Composable
fun TopControls(
    title: String?,
    onClose: () -> Unit,
    modifier: Modifier = Modifier
) {
    Box(modifier) {
        IconButton(
            modifier = Modifier
                .align(Alignment.TopStart),
            onClick = onClose
        ) {
            Icon(
                Icons.TwoTone.Close,
                contentDescription = "Close player",
                tint = Color.White
            )
        }

        if (title != null) { ... }
    }
}
  1. Atualize o elemento combinável PlayerControls para também usar um parâmetro de callback onClose e transmiti-lo aos TopControls.

PlayerControls.kt

fun PlayerControls(
    visible: Boolean,
    playerState: PlayerState,
    onClose: () -> Unit,
    onPlayPause: () -> Unit,
    onSeek: (seekToMillis: Long) -> Unit,
    modifier: Modifier = Modifier,
) {
    AnimatedVisibility(
        visible = visible,
        enter = fadeIn(),
        exit = fadeOut()
    ) {
        Box(modifier = modifier.background(Color.Black.copy(alpha = .5f))) {
            TopControls(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(dimensionResource(R.dimen.screen_edge_padding))
                    .align(Alignment.TopCenter),
                title = playerState.mediaMetadata.title?.toString(),
                onClose = onClose
            )
            ...
        }
    }
}
  1. Em seguida, atualize o elemento combinável PlayerScreen para usar o mesmo parâmetro e transmita-o aos PlayerControls.

PlayerScreen.kt

@Compsable
fun PlayerScreen(
    onClose: () -> Unit,
    modifier: Modifier = Modifier,
) {
    ...

    PlayerControls(
        modifier = Modifier
            .fillMaxSize(),
        visible = isShowingControls,
        playerState = playerState,
        onClose = onClose,
        onPlayPause = { if (playerState.isPlaying) player.pause() else player.play() },
        onSeek = { player.seekTo(it) }
    )
}
  1. Por fim, no RoadReelsNavHost, forneça a implementação que é transmitida para a PlayerScreen:

RoadReelsNavHost.kt

composable(route = Screen.Player.name) {
    PlayerScreen(onClose = { navController.popBackStack() })
}

Ótimo! Agora o usuário pode navegar entre as telas sem encontrar nenhuma rua sem saída. A experiência do usuário pode ser ainda melhor em outros formatos. Por exemplo, em um smartphone alto, quando a mão do usuário já está perto da parte de cima da tela, ele pode navegar pelo app com mais facilidade sem precisar mover o dispositivo na mão.

43122e716eeeeb20.gif

Adaptar ao suporte à orientação da tela

Ao contrário da grande maioria dos dispositivos móveis, a maioria dos carros tem orientação fixa. Ou seja, eles oferecem suporte para o modo paisagem ou retrato, mas não para ambos, já que as telas não podem ser giradas. Por isso, os apps precisam evitar presumir que há suporte para as duas orientações.

Em Criar um manifesto do Android Automotive OS, você adicionou dois elementos <uses-feature> aos recursos android.hardware.screen.portrait e android.hardware.screen.landscape com o atributo required definido como false. Isso garante que nenhuma dependência implícita de recursos em qualquer orientação de tela possa impedir que o app seja distribuído para carros. No entanto, esses elementos do manifesto não mudam o comportamento do app, apenas a forma como ele é distribuído.

Atualmente, o app tem um recurso útil que define automaticamente a orientação da atividade como paisagem quando o player de vídeo é aberto. Assim, os usuários de smartphone não precisam mexer nele para mudar a orientação se ele ainda não estiver no modo paisagem.

Infelizmente, esse mesmo comportamento pode resultar em um loop de tremulação ou efeito letterbox em dispositivos com orientação retrato fixa, o que inclui muitos carros disponíveis hoje.

Para corrigir isso, adicione uma verificação com base nas orientações de tela compatíveis com o dispositivo atual.

  1. Para simplificar a implementação, adicione o seguinte em Extensions.kt:

Extensions.kt

import android.content.Context
import android.content.pm.PackageManager

...

enum class SupportedOrientation {
    Landscape,
    Portrait,
}

fun Context.supportedOrientations(): List<SupportedOrientation> {
    return when (Pair(
        packageManager.hasSystemFeature(PackageManager.FEATURE_SCREEN_LANDSCAPE),
        packageManager.hasSystemFeature(PackageManager.FEATURE_SCREEN_PORTRAIT)
    )) {
        Pair(true, false) -> listOf(SupportedOrientation.Landscape)
        Pair(false, true) -> listOf(SupportedOrientation.Portrait)
        // For backwards compat, if neither feature is declared, both can be assumed to be supported
        else -> listOf(SupportedOrientation.Landscape, SupportedOrientation.Portrait)
    }
}
  1. Em seguida, guarde a chamada para definir a orientação solicitada. Como os apps podem enfrentar problemas semelhantes no modo de várias janelas em dispositivos móveis, você também pode incluir uma verificação para não definir dinamicamente a orientação nesse caso.

PlayerScreen.kt

import com.example.android.cars.roadreels.SupportedOrientation
import com.example.android.cars.roadreels.supportedOrientations

...

LaunchedEffect(Unit) {
    ...

    // Only automatically set the orientation to landscape if the device supports landscape.
    // On devices that are portrait only, the activity may enter a compat mode and won't get to
    // use the full window available if so. The same applies if the app's window is portrait
    // in multi-window mode.
    if (context.supportedOrientations().contains(SupportedOrientation.Landscape)
        && !context.isInMultiWindowMode
    ) {
        context.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
    }

    ...
}

A tela do player entra em um loop de tremulação no emulador Polestar 2 antes de adicionar a verificação (quando a atividade não processa mudanças de configuração de orientação).

A tela do player tem efeito letterbox no emulador Polestar 2 antes da adição da verificação (quando a atividade processa as mudanças de configuração de orientação).

A tela do player não tem efeito letterbox no emulador Polestar 2 após a adição da verificação.

A tela do player entra em um loop de tremulação no emulador Polestar 2 antes de adicionar a verificação (quando a atividade não processa mudanças de configuração de orientation).

A tela do player tem efeito letterbox no emulador Polestar 2 antes da adição da verificação (quando a atividade processa as mudanças de configuração de orientation).

A tela do player não tem efeito letterbox no emulador Polestar 2 após a adição da verificação.

Como esse é o único local no app que define a orientação da tela, o app agora evita o efeito letterbox. No seu app, verifique se há atributos screenOrientation ou chamadas setRequestedOrientation apenas para orientações paisagem ou retrato (incluindo sensor, inversão e variantes do usuário de cada um) e os remova ou proteja conforme necessário para limitar o efeito letterbox. Para mais detalhes, consulte Modo de compatibilidade com telas grandes.

Adaptar à capacidade de controle da barra de sistema

Embora a mudança anterior garanta que o app não entre em um loop de tremulação ou com efeito letterbox, ela também expõe outra suposição que está corrompida, ou seja, que as barras de sistema podem ficar sempre ocultas. Como os usuários têm necessidades diferentes ao usar o carro (em comparação com o uso de smartphones ou tablets), os OEMs têm a opção de impedir que os apps ocultem as barras de sistema para garantir que os controles do veículo, como os de temperatura, estejam sempre acessíveis na tela.

Como resultado, é possível que os apps sejam renderizados atrás das barras de sistema quando estiverem no modo imersivo e presumam que as barras estejam ocultas. Você pode perceber isso na etapa anterior, já que os controles das partes de cima e de baixo do player não ficam mais visíveis quando o app não está com efeito letterbox. Nesse caso específico, o app não é mais navegável, porque o botão para fechar o player está oculto e a funcionalidade dele impedida, já que a barra de busca não pode ser usada.

A correção mais fácil seria aplicar o padding dos encartes de janela systemBars ao player da seguinte maneira:

PlayerScreen.kt

import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding

...

Box(
    modifier = Modifier
        .fillMaxSize()
        .windowInsetsPadding(WindowInsets.systemBars)
) {
    PlayerView(...)
    PlayerControls(...)
}

No entanto, essa solução não é ideal, porque faz com que os elementos da interface pulem à medida que as barras de sistema se movimentam.

9c51956e2093820a.gif

Para melhorar a experiência do usuário, atualize o app para acompanhar quais encartes podem ser controlados e aplique o padding apenas aos que não podem.

  1. Como outras telas no app podem ter interesse em controlar os encartes de janela, faz sentido transmitir os encartes controláveis como um CompositionLocal. Crie um novo arquivo, LocalControllableInsets.kt, no pacote com.example.android.cars.roadreels e adicione o código abaixo:

LocalControllableInsets.kt

import androidx.compose.runtime.compositionLocalOf

// Assume that no insets can be controlled by default
const val DEFAULT_CONTROLLABLE_INSETS = 0
val LocalControllableInsets = compositionLocalOf { DEFAULT_CONTROLLABLE_INSETS }
  1. Configure um OnControllableInsetsChangedListener para detectar mudanças.

MainActivity.kt

import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsControllerCompat.OnControllableInsetsChangedListener

...

class MainActivity : ComponentActivity() {
    private lateinit var onControllableInsetsChangedListener: OnControllableInsetsChangedListener

    @OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        enableEdgeToEdge()

        setContent {
            var controllableInsetsTypeMask by remember { mutableIntStateOf(DEFAULT_CONTROLLABLE_INSETS) }

            onControllableInsetsChangedListener =
                OnControllableInsetsChangedListener { _, typeMask ->
                    if (controllableInsetsTypeMask != typeMask) {
                        controllableInsetsTypeMask = typeMask
                    }
                }

            WindowCompat.getInsetsController(window, window.decorView)
                .addOnControllableInsetsChangedListener(onControllableInsetsChangedListener)

            RoadReelsTheme {
                RoadReelsApp(calculateWindowSizeClass(this))
            }
        }
    }

    override fun onDestroy() {
        super.onDestroy()

        WindowCompat.getInsetsController(window, window.decorView)
            .removeOnControllableInsetsChangedListener(onControllableInsetsChangedListener)
    }
}
  1. Adicione um CompositionLocalProvider de nível superior que contenha o tema e os elementos combináveis do app e que vincule valores a LocalControllableInsets.

MainActivity.kt

import androidx.compose.runtime.CompositionLocalProvider

...

CompositionLocalProvider(LocalControllableInsets provides controllableInsetsTypeMask) {
    RoadReelsTheme {
        RoadReelsApp(calculateWindowSizeClass(this))
    }
}
  1. No player, leia o valor atual e use-o para determinar os encartes que serão usados para o padding.

PlayerScreen.kt

import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.union
import androidx.compose.ui.unit.dp
import com.example.android.cars.roadreels.LocalControllableInsets

...

val controllableInsetsTypeMask = LocalControllableInsets.current

// When the system bars can be hidden, ignore them when applying padding to the player and
// controls so they don't jump around as the system bars disappear. If they can't be hidden
// include them so nothing renders behind the system bars
var windowInsetsForPadding = WindowInsets(0.dp)
if (controllableInsetsTypeMask.and(WindowInsetsCompat.Type.statusBars()) == 0) {
    windowInsetsForPadding = windowInsetsForPadding.union(WindowInsets.statusBars)
}
if (controllableInsetsTypeMask.and(WindowInsetsCompat.Type.navigationBars()) == 0) {
    windowInsetsForPadding = windowInsetsForPadding.union(WindowInsets.navigationBars)
}

Box(
    modifier = Modifier
        .fillMaxSize()
        .windowInsetsPadding(windowInsetsForPadding)
) {
    PlayerView(...)
    PlayerControls(...)
}

O conteúdo não pula quando as barras de sistema podem ser ocultadas

O conteúdo permanece visível quando as barras de sistema não podem ser ocultadas

O conteúdo não pula quando as barras de sistema podem ser ocultadas

O conteúdo permanece visível quando as barras de sistema não podem ser ocultadas

Muito melhor! O conteúdo não pula e, ao mesmo tempo, os controles ficam totalmente visíveis, mesmo em carros em que as barras de sistema não podem ser controladas.

7. Satisfazer os requisitos de qualidade do Android Automotive OS: distração do motorista

Por fim, há uma grande diferença entre carros e outros formatos: eles são usados para dirigir. Por isso, é muito importante limitar as distrações ao dirigir. Todos os apps para carros estacionados do Android Automotive OS precisam pausar a reprodução quando o veículo começa a ser dirigido. Uma sobreposição do sistema aparece quando a direção começa e, por sua vez, o evento de ciclo de vida onPause é chamado para o app que está sendo sobreposto. É durante essa chamada que os apps precisam pausar a reprodução.

Simular a direção

Navegue até a visualização do player no emulador e comece a reproduzir o conteúdo. Depois, siga as etapas para simular a direção e observe que, embora a interface do app esteja obscurecida pelo sistema, a reprodução não é pausada. Isso viola a diretriz de qualidade de apps para carros DD-2.

839af1382c1f10ca.png

Pausar a reprodução quando o carro começa a ser dirigido

  1. Adicione uma dependência ao artefato androidx.lifecycle:lifecycle-runtime-compose, que contém o LifecycleEventEffect, que ajuda a executar o código em eventos de ciclo de vida.

libs.version.toml

androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycle" }

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

implementation(libs.androidx.lifecycle.runtime.compose)
  1. Depois de sincronizar o projeto para baixar a dependência, adicione um LifecycleEventEffect que seja executado no evento ON_PAUSE para pausar a reprodução.

PlayerScreen.kt

import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LifecycleEventEffect

...

@Composable
fun PlayerScreen(...) {
    ...
    LifecycleEventEffect(Lifecycle.Event.ON_PAUSE) {
        player.pause()
    }

    LifecycleEventEffect(Lifecycle.Event.ON_RESUME) {
        player.play()
    }
    ...
}

Com a correção implementada, siga as mesmas etapas anteriores para simular a direção durante a reprodução ativa e observe que a reprodução para, satisfazendo o requisito DD-2.

8. Testar o app no emulador de tela distante

Uma nova configuração que está começando a aparecer nos carros é a de duas telas, com uma tela principal no console central e uma tela secundária no painel perto do para-brisa. Os apps podem ser movidos da tela central para a secundária e vice-versa para oferecer mais opções aos motoristas e passageiros.

Instalar a imagem da tela distante do Automotive

  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 a imagem do emulador Automotive Distant Display with Google APIs para a arquitetura do seu computador (x86/ARM).

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 o perfil de hardware do pacote Automotive Distant Display na lista e clique em Next.
  2. Na próxima página, selecione a imagem do sistema da etapa anterior. Clique em Next e selecione qualquer opção avançada que você queira antes de criar o AVD. Para isso, clique em Finish.

Executar o app

Execute o app no emulador recém-criado usando a configuração de execução do app atual. Siga as instruções em Usar o emulador de tela distante para mover o app de e para a tela distante. Tente mover o app quando ele estiver na tela principal/de detalhes e na tela do player, tentando interagir com o app nas duas telas.

b277bd18a94e9c1b.png

9. Melhorar a experiência no app na tela distante

Ao usar o app na tela distante, você pode ter notado duas coisas:

  1. A reprodução é reiniciada quando o app é movido para a tela distante e longe dela
  2. Não é possível interagir com o app enquanto ele está na tela distante, o que inclui mudar o estado de reprodução.

Melhorar a continuidade do app

O problema em que a reprodução é reiniciada é causado pela atividade recriada devido a uma mudança de configuração. Como o app é escrito usando o Compose e a configuração que está mudando está relacionada ao tamanho, é fácil permitir que o Compose processe as mudanças de configuração para você restringindo a recriação de atividades para mudanças de configuração baseadas em tamanho. Isso facilita a transição entre as telas, sem interrupção na reprodução ou atualização devido à recriação de atividades.

AndroidManifest.xml

<activity
    android:name="com.example.android.cars.roadreels.MainActivity"
    ...
    android:configChanges="screenSize|smallestScreenSize|orientation|screenLayout|density">
        ...
</activity>

Implementar controles de mídia

Para corrigir o problema em que o app não pode ser controlado enquanto está na tela distante, implemente MediaSession. As sessões de mídia oferecem uma maneira universal de interagir com um player de áudio ou vídeo. Para mais informações, consulte Controlar e anunciar a reprodução usando uma MediaSession.

  1. Adicione uma dependência ao artefato androidx.media3:media3-session.

libs.version.toml

androidx-media3-mediasession = { group = "androidx.media3", name = "media3-session", version.ref = "media3" }

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

implementation(libs.androidx.media3.mediasession)
  1. Crie uma MediaSession usando o builder correspondente.

PlayerScreen.kt

import androidx.media3.session.MediaSession

@Composable
fun PlayerScreen(...) {
    ...
    val mediaSession = remember(context, player) {
        MediaSession.Builder(context, player).build()
    }
    ...
}
  1. Em seguida, adicione outra linha ao bloco onDispose do DisposableEffect no elemento combinável Player para liberar a MediaSession quando o Player sair da árvore de composição.

PlayerScreen.kt

DisposableEffect(Unit) {
    onDispose {
        mediaSession.release()
        player.release()
        ...
    }
}
  1. Por fim, na tela do player, você pode testar os controles de mídia usando o comando adb shell cmd media_session dispatch.
# To play content
adb shell cmd media_session dispatch play

# To pause content
adb shell cmd media_session dispatch pause

# To toggle the playing state
adb shell cmd media_session dispatch play-pause

Com isso, o app funciona muito melhor em carros com telas distantes. Mais do que isso, ele também funciona melhor em outros formatos. Em dispositivos que permitem girar a tela ou que os usuários redimensionem a janela de um app, agora o app também se adapta perfeitamente a essas situações.

Além disso, graças à integração da sessão de mídia, a reprodução do app pode ser controlada não apenas por controles de hardware e software em carros, mas também por outras fontes, como uma consulta do Google Assistente ou um botão de pausa em fones de ouvido, dando aos usuários mais opções para controlar o app em vários formatos.

10. Testar o app em diferentes configurações do sistema

Com o app funcionando bem na tela principal e na tela distante, a última coisa a verificar é como ele processa diferentes configurações da barra de sistema e cortes da tela. Conforme descrito em Trabalhar com encartes de janela e cortes da tela, os dispositivos Android Automotive OS podem apresentar configurações que quebram as suposições geralmente aplicáveis a formatos de dispositivos móveis.

Nesta seção, você vai baixar um emulador que pode ser configurado no tempo de execução, configurar o emulador para ter uma barra de sistema à esquerda e testar o app nessa configuração.

Instalar a imagem do Android Automotive com APIs do Google

  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 a imagem do emulador do Android Automotive com APIs do Google na API 33 para a arquitetura do seu computador (x86/ARM).

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 o perfil de hardware do pacote Automotive (1080p landscape) na lista e clique em Next.
  2. Na próxima página, selecione a imagem do sistema da etapa anterior. Clique em Next e selecione qualquer opção avançada que você queira antes de criar o AVD. Para isso, clique em Finish.

Configurar uma barra de sistema lateral

Conforme detalhado em Testar usando o emulador configurável, há várias opções para emular diferentes configurações de sistema presentes nos carros.

Para os fins deste codelab, o com.android.systemui.rro.left pode ser usado para testar uma configuração diferente da barra de sistema. Para ativá-lo, use o seguinte comando:

adb shell cmd overlay enable --user 0 com.android.systemui.rro.left

b642703a7278b219.png

Como o app usa o modificador systemBars como contentWindowInsets no Scaffold, o conteúdo já está sendo renderizado em uma área segura das barras de sistema. Para saber o que aconteceria se o app presumisse que as barras de sistema só apareciam na parte de cima e de baixo da tela, mude esse parâmetro para o seguinte:

RoadReelsApp.kt

contentWindowInsets = if (route?.equals(Screen.Player.name) == true) WindowInsets(0.dp) else WindowInsets.systemBars.only(WindowInsetsSides.Vertical)

Opa! A tela de lista e detalhes é renderizada atrás da barra de sistema. Graças ao trabalho anterior, a tela do player estaria OK, mesmo que as barras de sistema não fossem controláveis.

9898f7298a7dfb4.gif

Antes de passar para a próxima seção, reverta a mudança que você acabou de fazer no parâmetro windowContentPadding.

11. Trabalhar com cortes da tela

Por fim, alguns carros têm telas com cortes muito diferentes dos encontrados em dispositivos móveis. Em vez dos entalhes ou recortes da câmera com orifício, alguns veículos com Android Automotive OS têm telas curvas que tornam a imagem não retangular.

Para conferir como o app se comporta quando esse recorte está presente, primeiro ative o corte da tela usando o seguinte comando:

adb shell cmd overlay enable --user 0 com.android.internal.display.cutout.emulation.free_form

Para testar o comportamento do app, ative também a barra de sistema esquerda usada na última seção, se ela ainda não estiver ativada:

adb shell cmd overlay enable --user 0 com.android.systemui.rro.left

No momento, o app não é renderizado no corte da tela. É difícil identificar o formato exato do corte, mas ele vai ficar claro na próxima etapa. Isso é perfeitamente aceitável e uma experiência melhor do que a de um app renderizado em cortes, mas que não se adapta a eles com cuidado.

212628db84981025.gif

Renderizar no corte da tela

Para oferecer aos usuários a experiência mais imersiva possível, você pode usar muito mais espaço na tela renderizando o corte da tela.

  1. Para renderizar no corte da tela, crie um arquivo integers.xml para armazenar a substituição específica para carros. Para fazer isso, use o qualificador UI mode com o valor Car Dock. O nome é uma referência de quando apenas o Android Auto existia, mas também é usado pelo Android Automotive OS. Além disso, como o LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS foi introduzido no Android R, adicione também o qualificador Version do Android com o valor 30. Consulte Usar recursos alternativos para saber mais detalhes.

22b7f17657cac3fd.png

  1. No arquivo que você acabou de criar (res/values-car-v30/integers.xml), adicione o seguinte:

integers.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <integer name="windowLayoutInDisplayCutoutMode">3</integer>
</resources>

O valor inteiro 3 corresponde a LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS e substitui o valor padrão de 0 de res/values/integers.xml, que corresponde a LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT. Esse valor inteiro já é referenciado em MainActivity.kt para substituir o modo definido por enableEdgeToEdge(). Para mais informações sobre esse atributo, consulte a documentação de referência.

Agora, ao executar o app, observe que o conteúdo se estende para o corte e parece muito imersivo. No entanto, a barra de apps na parte de cima e parte do conteúdo ficam parcialmente obscurecidos pelo corte da tela, causando um problema semelhante ao que acontecia quando o app presumia que as barras de sistema só apareciam na parte de cima e de baixo.

f0eefa42dee6f7c7.gif

Corrigir as barras de apps na parte de cima

Para corrigir as barras de apps na parte de cima, adicione o seguinte parâmetro windowInsets aos elementos combináveis CenterAlignedTopAppBar:

RoadReelsApp.kt

import androidx.compose.foundation.layout.safeDrawing

...

windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal + WindowInsetsSides.Top)

Como safeDrawing consiste nos encartes displayCutout e systemBars, isso melhora o parâmetro windowInsets padrão, que usa apenas systemBars ao posicionar a barra de apps na parte de cima.

Além disso, como a barra de apps superior está posicionada na parte de cima da janela, não inclua o componente de baixo dos encartes safeDrawing. Isso pode adicionar um padding desnecessário.

7d59ebb63ada5f71.gif

Corrigir a tela principal

Uma opção para corrigir o conteúdo nas telas principal e de detalhes é usar safeDrawing em vez de systemBars para os contentWindowInsets do Scaffold. No entanto, o app parece bem menos imersivo usando essa opção, com o conteúdo sendo cortado abruptamente onde o corte da tela começa - nada melhor do que se o app não fosse renderizado no corte da tela.

6b3824ca3214cbfa.gif

Para uma interface do usuário mais imersiva, é possível processar os encartes em cada componente na tela.

  1. Atualize os contentWindowInsets do Scaffold para que sejam constantemente 0 dp (em vez de apenas para a PlayerScreen). Isso permite que cada tela e/ou componente dentro de uma tela determine o comportamento em relação aos encartes.

RoadReelsApp.kt

Scaffold(
    ...,
    contentWindowInsets = WindowInsets(0.dp)
) { ... }
  1. Defina o windowInsetsPadding dos elementos combináveis Text do cabeçalho da linha para usar os componentes horizontais dos encartes safeDrawing. O componente superior desses encartes é processado pela barra de apps superior, e o componente inferior será processado mais tarde.

MainScreen.kt

import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.windowInsetsPadding

...

LazyColumn(
    contentPadding = PaddingValues(bottom = dimensionResource(R.dimen.screen_edge_padding))
) {
    items(NUM_ROWS) { rowIndex: Int ->
        Text(
            "Row $rowIndex",
            style = MaterialTheme.typography.headlineSmall,
            modifier = Modifier
                .padding(
                    horizontal = dimensionResource(R.dimen.screen_edge_padding),
                    vertical = dimensionResource(R.dimen.row_header_vertical_padding)
                )
                .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal))
        )
    ...
}
  1. Remova o parâmetro contentPadding da LazyRow. Em seguida, no início e no fim de cada LazyRow, adicione um Spacer com a largura do componente safeDrawing correspondente para garantir que todas as miniaturas possam ser visualizadas. Use o modificador widthIn para garantir que esses espaçadores tenham pelo menos a mesma largura do padding do conteúdo. Sem esses elementos, os itens no início e no fim da linha podem ser ocultados atrás das barras de sistema e/ou do corte da tela, mesmo quando deslizados completamente para o início/fim da linha.

MainScreen.kt

import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.windowInsetsEndWidth
import androidx.compose.foundation.layout.windowInsetsStartWidth

...

LazyRow(
    horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.list_item_spacing)),
) {
    item {
        Spacer(
            Modifier
                .windowInsetsStartWidth(WindowInsets.safeDrawing)
                .widthIn(min = dimensionResource(R.dimen.screen_edge_padding))
        )
    }
    items(NUM_ITEMS_PER_ROW) { ... }
    item {
        Spacer(
            Modifier
                .windowInsetsEndWidth(WindowInsets.safeDrawing)
                .widthIn(min = dimensionResource(R.dimen.screen_edge_padding))
        )
    }
}
  1. Por fim, adicione um Spacer ao final da LazyColumn para considerar possíveis barras de sistema ou encartes de corte da tela na parte de baixo da tela. Não é necessário um espaçamento equivalente na parte de cima da LazyColumn, porque a barra de apps superior os processa. Se o app usar uma barra de apps inferior em vez de uma barra de apps superior, adicione um Spacer ao início da lista usando o modificador windowInsetsTopHeight. Se o app usar uma barra de apps na parte de cima e na parte de baixo, nenhum espaçador será necessário.

MainScreen.kt

import androidx.compose.foundation.layout.windowInsetsBottomHeight

...

LazyColumn(...){
    items(NUM_ROWS) { ... }
    item {
        Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing))
    }
}

Perfeito! As barras de apps superiores estão totalmente visíveis e, ao rolar até o final de uma linha, você pode ver todas as miniaturas na íntegra.

543706473398114a.gif

Corrigir a tela de detalhes

f622958a8d0c16c8.png

A tela de detalhes não está tão ruim, mas o conteúdo ainda está sendo cortado.

Como a tela de detalhes não tem conteúdo rolável, basta adicionar um modificador windowInsetsPadding à Box de nível superior para corrigir o problema.

DetailScreen.kt

import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.windowInsetsPadding

...

Box(
    modifier = modifier
        .padding(dimensionResource(R.dimen.screen_edge_padding))
        .windowInsetsPadding(WindowInsets.safeDrawing)
) { ... }

bdd6de6010fc139d.png

Corrigir a tela do player

Embora a PlayerScreen já aplique padding a alguns ou todos os encartes da janela da barra de sistema da seção Satisfazer os requisitos de qualidade do Android Automotive OS: navegação, isso não é suficiente para garantir que não seja ocultada agora que o app está renderizando em cortes da tela. Em dispositivos móveis, os cortes da tela quase sempre estão totalmente contidos nas barras de sistema. No entanto, em carros, os cortes da tela podem se estender muito além das barras de sistema, quebrando as suposições.

427227df5e44f554.png

Para corrigir isso, basta mudar o valor inicial da variável windowInsetsForPadding de um valor zero para displayCutout:

PlayerScreen.kt

import androidx.compose.foundation.layout.displayCutout

...

var windowInsetsForPadding = WindowInsets(WindowInsets.displayCutout)

b523d8c1e1423757.gif

Legal! O app realmente aproveita ao máximo a tela e ainda permanece utilizável.

E, se você executar o app em um dispositivo móvel, ele também será mais imersivo. Os itens de lista são renderizados até as bordas da tela, inclusive atrás da barra de navegação.

dc7918499a33df31.png

12. Parabéns

Você migrou e otimizou seu primeiro app para carros estacionados. Agora é hora de aplicar o que você aprendeu ao seu próprio app.

O que testar

Leia mais