Cómo crear y probar una app para usar en el SO Android Automotive con el automóvil estacionado

1. Antes de comenzar

Qué no es este codelab

Requisitos

Qué crearás

En este codelab, aprenderás a migrar al SO Android Automotive Road Reels, una app existente de transmisión de video por Internet para dispositivos móviles

La versión de punto de partida de la app que se ejecuta en un teléfono

La versión completa de la app que se ejecuta en un emulador del SO Android Automotive con un recorte de pantalla

La versión de punto de partida de la app que se ejecuta en un teléfono

La versión completa de la app que se ejecuta en un emulador del SO Android Automotive con un recorte de pantalla

Qué aprenderás

  • Cómo usar el emulador del SO Android Automotive
  • Cómo realizar los cambios necesarios para crear una compilación del SO Android Automotive
  • Suposiciones que suelen hacerse cuando se desarrollan apps para dispositivos móviles que pueden fallar cuando una app se ejecuta en el SO Android Automotive
  • Los diferentes niveles de calidad de las apps para automóviles
  • Cómo usar la sesión multimedia para permitir que otras apps controlen la reproducción de tu app
  • Cómo pueden diferir la IU del sistema y las inserciones de ventanas en los dispositivos con el SO Android Automotive en comparación con los dispositivos móviles

2. Prepárate

Obtén el código

  1. El código para este codelab se puede encontrar en el directorio build-a-parked-app dentro del repositorio car-codelabs de GitHub. Para clonarlo, ejecuta el siguiente comando:
git clone https://github.com/android/car-codelabs.git
  1. También tienes la opción de descargar el repositorio como archivo ZIP:

Abre el proyecto

  • Después de iniciar Android Studio, importa el proyecto. Elige solamente el directorio build-a-parked-app/start. El directorio build-a-parked-app/end contiene el código de la solución, que puedes consultar en cualquier momento si no logras avanzar o si deseas ver el proyecto completo.

Familiarízate con el código

  • Después de abrir el proyecto en Android Studio, dedica un momento a analizar el código de partida.

3. Aprende sobre las apps para usar en el SO Android Automotive con el automóvil estacionado

Las apps destinadas a usarse con el automóvil estacionado conforman un subconjunto de las categorías de apps compatibles con el SO Android Automotive. Al momento de escribir, constan de apps de transmisión de video por Internet, navegadores web y juegos. Estas apps son ideales para automóviles debido al hardware de los automóviles con Google integrado y a la prevalencia en constante crecimiento de los automóviles eléctricos, en los que el tiempo de carga representa una gran oportunidad para que los conductores y los pasajeros interactúen con este tipo de apps.

En muchos sentidos, los automóviles son similares a otros dispositivos con pantalla grande, como las tablets y los dispositivos plegables. Tienen pantallas táctiles con tamaños, resoluciones y relaciones de aspecto similares y que pueden estar en orientación horizontal o vertical (aunque, a diferencia de las tablets, su orientación es fija). También son dispositivos conectados que pueden entrar en la conexión de red y salir de esta. Con todo esto en mente, no sorprende que las apps que ya están optimizadas para pantallas grandes no suelan requerir mucho trabajo para ofrecer una excelente experiencia del usuario en automóviles.

Al igual que para las pantallas grandes, también hay niveles de calidad de las apps para automóviles:

  • Nivel 3 (lista para el automóvil): Tu app es compatible con pantallas grandes y se puede usar mientras el automóvil está estacionado. Si bien es posible que no tenga ninguna función optimizada para automóviles, los usuarios pueden experimentar la app como lo harían en cualquier otro dispositivo Android con pantalla grande. Las apps para dispositivos móviles que cumplen con estos requisitos son aptas para distribuirse en automóviles sin modificaciones a través del programa de apps para dispositivos móviles listas para usar en automóviles.
  • Nivel 2 (optimizada para el automóvil): Tu app brinda una gran experiencia en la pantalla central del automóvil. Para ello, tendrá ingeniería específica del automóvil para incluir capacidades que se pueden usar en los modos de conducción o estacionamiento, según la categoría de tu app.
  • Nivel 1 (diferenciada para el automóvil): Tu app se diseñó para funcionar con una amplia variedad de hardware en automóviles y puede adaptar su experiencia a los modos de conducción y estacionamiento. Proporciona la mejor experiencia del usuario diseñada para las diferentes pantallas de los automóviles, como la consola central, el clúster de instrumentos y las pantallas adicionales, como las panorámicas que se encuentran en muchos automóviles de alta gama.

4. Ejecuta la app en el emulador del SO Android Automotive

Instala Automotive con las imágenes del sistema de Play Store

  1. Primero, abre SDK Manager en Android Studio y selecciona la pestaña SDK Platforms si aún no está seleccionada. En la esquina inferior derecha de la ventana de SDK Manager, asegúrate de que esté marcada la casilla junto a Show package details.
  2. Instala una de las imágenes del emulador Automotive with Play Store que se indican en Agrega imágenes genéricas del sistema. Las imágenes solo se pueden ejecutar en máquinas que tienen su misma arquitectura (x86/ARM).

Crea un dispositivo virtual del SO Android Automotive

  1. Después de abrir el Administrador de dispositivos, selecciona Automotive debajo de la columna Category en la parte izquierda de la ventana. Luego, selecciona el perfil de hardware incluido Automotive (1024p landscape) de la lista y haz clic en Next.

El asistente de Virtual Device Configuration muestra el perfil de hardware "Automotive (1024p landscape)" seleccionado.

  1. En la siguiente página, selecciona la imagen del sistema del paso anterior. Haz clic en Next y selecciona las opciones avanzadas que desees antes de crear el AVD haciendo clic en Finish. Nota: Si eliges la imagen del nivel de API 30, es posible que esté en una pestaña que no sea la de Recommended.

Ejecuta la app

Ejecuta la app en el emulador que acabas de crear usando la configuración de ejecución app existente. Experimenta con la app para probar las diferentes pantallas y comparar su comportamiento si la ejecutas en un emulador de teléfono o tablet.

301e6c0d3675e937.png

5. Crea una compilación del SO Android Automotive

Si bien la app "simplemente funciona", hay algunos cambios pequeños que se deben realizar para que funcione de forma correcta en el SO Android Automotive y cumpla con los requisitos para poder publicarse en Play Store. No todos estos cambios deberían incluirse en la versión para dispositivos móviles de la app, por lo que primero crearás una variante de compilación del SO Android Automotive.

Agrega una dimensión de variantes para el factor de forma

Para comenzar, agrega una dimensión de variantes para el factor de forma al que se orienta la compilación. Para ello, modifica flavorDimensions en el archivo build.gradle.kts. Luego, agrega un bloque productFlavors y variantes para cada factor de forma (mobile y automotive).

Para obtener más información, consulta Configura variantes de productos.

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

Después de actualizar el archivo build.gradle.kts, deberías ver un banner en la parte superior del archivo que te informa que los archivos de Gradle cambiaron desde la última sincronización del proyecto y que posiblemente debas sincronizar el proyecto para que el IDE funcione de forma correcta. Haz clic en el botón Sync Now en ese banner para que Android Studio pueda importar estos cambios en la configuración de compilación.

8685bcde6b21901f.png

A continuación, abre la ventana de herramientas Build Variants desde el elemento de menú Build > Select Build Variant… y selecciona la variante automotiveDebug. De esta manera, te asegurarás de ver los archivos del conjunto de orígenes automotive en la ventana Project y de que se use esta variante de compilación cuando se ejecute la app a través de Android Studio.

19e4aa8135553f62.png

Crea un manifiesto del SO Android Automotive

A continuación, crearás un archivo AndroidManifest.xml para el conjunto de orígenes automotive. Este archivo contiene los elementos necesarios para las apps del SO Android Automotive.

  1. En la ventana Project, haz clic con el botón derecho en el módulo app. En el menú desplegable que aparece, selecciona New > Other > Android Manifest File.
  2. En la ventana New Android Component que se abre, selecciona automotive como Target Source Set para el archivo nuevo. Haz clic en Finish para crear el archivo.

3fe290685a1026f5.png

  1. Dentro del archivo AndroidManifest.xml que se acaba de crear (en la ruta de acceso app/src/automotive/AndroidManifest.xml), agrega lo siguiente:

AndroidManifest.xml (automotive)

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

Se requiere la primera declaración para subir el artefacto de compilación al segmento del SO Android Automotive en Play Console. Google Play usa la presencia de esta función para distribuir la app solo a dispositivos con la función android.hardware.type.automotive (p. ej., automóviles).

Las otras declaraciones son obligatorias para garantizar que la app se pueda instalar en las diversas configuraciones de hardware presentes en los automóviles. Para obtener más información detallada, consulta Funciones obligatorias del SO Android Automotive.

Marca tu app como app de video

La última parte de los metadatos que se debe agregar es el archivo automotive_app_desc.xml. Se usa para declarar la categoría de tu app en el contexto de Android para vehículos y es independiente de la categoría que selecciones para tu app en Play Console.

  1. Haz clic con el botón derecho en el módulo app y selecciona la opción New > Android Resource File e ingresa los siguientes valores antes de hacer clic en OK:
  • File name: automotive_app_desc.xml
  • Resource type: XML
  • Root element: automotiveApp
  • Source set: automotive
  • Directory name: xml

47ac6bf76ef8ad45.png

  1. Dentro de ese archivo, agrega el siguiente elemento <uses> para declarar que tu app es una app de video.

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. En el archivo AndroidManifest.xml del conjunto de orígenes automotive (en el que acabas de agregar los elementos <uses-feature>), agrega un elemento <application> vacío. Dentro de él, agrega el siguiente elemento <meta-data> que hace referencia al archivo automotive_app_desc.xml que acabas de crear.

AndroidManifest.xml (automotive)

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

Con eso, realizaste todos los cambios necesarios para crear una compilación del SO Android Automotive para la app.

6. Cumple con los requisitos de calidad del SO Android Automotive: navegabilidad

Si bien crear una variante de compilación del SO Android Automotive es una parte de adaptar tu app para automóviles, sigue siendo necesario asegurarte de que esta se pueda usar y sea segura.

Agrega opciones de navegación

Mientras ejecutabas la app en el emulador del SO Android Automotive, es posible que hayas notado que no era posible regresar de la pantalla de detalles a la pantalla principal ni de la del reproductor a la de detalles. A diferencia de otros factores de forma, que pueden requerir un botón Atrás o un gesto táctil para habilitar la navegación hacia atrás, no existe tal requisito para los dispositivos con el SO Android Automotive. Por lo tanto, las apps deben proporcionar opciones de navegación en su IU para garantizar que los usuarios puedan navegar sin quedarse atrapados en una pantalla dentro de la app. Este requisito está codificado como el lineamiento de calidad AN-1.

Para admitir la navegación hacia atrás desde la pantalla de detalles hasta la pantalla principal, agrega un parámetro navigationIcon adicional para el elemento CenterAlignedTopAppBar de la pantalla de detalles de la siguiente manera:

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 admitir la navegación hacia atrás desde la pantalla del reproductor hasta la pantalla principal, haz lo siguiente:

  1. Actualiza el elemento componible TopControls para que tome un parámetro de devolución de llamada con el nombre onClose y agrega un IconButton que lo llame cuando se haga clic.

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. Actualiza el elemento componible PlayerControls para que también tome un parámetro de devolución de llamada onClose y lo pase a 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. A continuación, actualiza el elemento componible PlayerScreen para que tome el mismo parámetro y lo pase a su 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 último, en RoadReelsNavHost, proporciona la implementación que se pasa a PlayerScreen:

RoadReelsNavHost.kt

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

Excelente. Ahora el usuario puede moverse entre pantallas sin encontrarse con ningún punto muerto. Además, la experiencia del usuario también puede ser mejor en otros factores de forma. Por ejemplo, en un teléfono alto, cuando la mano del usuario ya está cerca de la parte superior de la pantalla, puede navegar más fácilmente por la app sin necesidad de mover el dispositivo en la mano.

43122e716eeeeb20.gif

Adapta la app para que sea compatible con la orientación de la pantalla

A diferencia de la gran mayoría de los dispositivos móviles, la mayoría de los automóviles tienen una orientación fija. Es decir, admiten orientación horizontal o vertical, pero no ambas, ya que sus pantallas no se pueden rotar. Por este motivo, las apps deben evitar suponer que se admiten ambas orientaciones.

En la sección Crea un manifiesto del SO Android Automotive, agregaste dos elementos <uses-feature> para las funciones android.hardware.screen.portrait y android.hardware.screen.landscape con el atributo required establecido en false. De esta manera, se garantiza que ninguna dependencia de funciones implícita en cualquier orientación de pantalla pueda impedir que la app se distribuya en automóviles. Sin embargo, esos elementos del manifiesto no cambian el comportamiento de la app, sino la forma en que se distribuye.

En la actualidad, la app tiene una función útil con la que se configura automáticamente la orientación de la actividad en modo horizontal cuando se abre el reproductor de video, de modo que los usuarios del teléfono no tengan que tocar su dispositivo para cambiar su orientación si todavía no está en el modo horizontal.

Lamentablemente, ese mismo comportamiento puede generar un bucle parpadeante o un formato letterbox en dispositivos con orientación vertical fija, lo que incluye muchos automóviles que circulan en la ruta en la actualidad.

Para solucionar este problema, puedes agregar una verificación basada en las orientaciones de pantalla que admita el dispositivo actual.

  1. Para simplificar la implementación, primero, agrega lo siguiente en 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. Luego, protege la llamada para establecer la orientación solicitada. Como las apps pueden tener un problema similar en el modo multiventana en dispositivos móviles, también puedes incluir una verificación para no configurar la orientación de forma dinámica en ese 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
    }

    ...
}

La pantalla del reproductor entra en un bucle parpadeante en el emulador de Polestar 2 antes de agregar la verificación (cuando la actividad no controla los cambios de configuración de orientación).

La pantalla del reproductor se muestra en formato letterbox en el emulador de Polestar 2 antes de agregar la verificación (cuando la actividad controla los cambios de configuración de orientación).

La pantalla del reproductor no se muestra en formato letterbox en el emulador de Polestar 2 después de agregar la marca de verificación.

La pantalla del reproductor entra en un bucle parpadeante en el emulador de Polestar 2 antes de agregar la verificación (cuando la actividad no controla los cambios de configuración de orientation).

La pantalla del reproductor se muestra en formato letterbox en el emulador de Polestar 2 antes de agregar la verificación (cuando la actividad controla los cambios de configuración de orientation).

La pantalla del reproductor no se muestra en formato letterbox en el emulador de Polestar 2 después de agregar la marca de verificación.

Como esta es la única ubicación de la app que establece la orientación de la pantalla, ahora la app evita el formato letterbox. En tu propia app, busca atributos screenOrientation o llamadas setRequestedOrientation que sean únicamente para orientaciones horizontales o verticales (incluidos el sensor, la marcha atrás y las variantes del usuario de cada una), y quítalos o protégelos según sea necesario para limitar el formato letterbox. Para obtener más detalles, consulta Modo de compatibilidad de pantallas grandes.

Adapta la app a la capacidad de control de la barra del sistema

Lamentablemente, si bien el cambio anterior garantiza que la app no entre en un bucle parpadeante ni se muestre con formato letterbox, también expone otra suposición que no se cumple, es decir, que las barras del sistema siempre se pueden ocultar. Como los usuarios tienen diferentes necesidades cuando utilizan sus automóviles (en comparación con cuando usan sus teléfonos o tablets), los OEM tienen la opción de evitar que las apps oculten las barras del sistema para garantizar que siempre se pueda acceder a los controles del automóvil, como los de climatización, en la pantalla.

Como resultado, es posible que las apps se rendericen detrás de las barras del sistema cuando lo hagan en modo envolvente y asuman que las barras se pueden ocultar. Puedes observarlo en el paso anterior, ya que los controles de la parte inferior y superior del reproductor ya no estarán visibles cuando la app no se muestre en formato letterbox. En este caso específico, la app ya no se puede navegar, dado que el botón para cerrar el reproductor está oculto, y su funcionalidad se ve impedida, puesto que no se puede usar la barra deslizante de búsqueda.

La solución más fácil sería aplicar el padding de las inserciones de ventana systemBars al reproductor de la siguiente manera:

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(...)
}

Sin embargo, esta solución no es la ideal, ya que causa que los elementos de la IU se muevan cuando desaparecen las animaciones de las barras del sistema.

9c51956e2093820a.gif

Para mejorar la experiencia del usuario, puedes actualizar la app para hacer un seguimiento de las inserciones que se pueden controlar y para aplicar padding solo a las que no se pueden controlar.

  1. Como otras pantallas dentro de la app podrían estar interesadas en controlar las inserciones de ventana, tiene sentido pasar las inserciones controlables como un CompositionLocal. Crea un nuevo archivo LocalControllableInsets.kt en el paquete com.example.android.cars.roadreels y agrega lo siguiente:

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. Configura un OnControllableInsetsChangedListener para detectar cambios.

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. Agrega un CompositionLocalProvider de nivel superior que contenga los elementos componibles de la app y el tema, y que vincule valores a LocalControllableInsets.

MainActivity.kt

import androidx.compose.runtime.CompositionLocalProvider

...

CompositionLocalProvider(LocalControllableInsets provides controllableInsetsTypeMask) {
    RoadReelsTheme {
        RoadReelsApp(calculateWindowSizeClass(this))
    }
}
  1. En el reproductor, lee el valor actual y úsalo para determinar las inserciones que se usarán para el 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(...)
}

El contenido no se mueve cuando se pueden ocultar las barras del sistema.

El contenido permanece visible cuando no se pueden ocultar las barras del sistema.

El contenido no se mueve cuando se pueden ocultar las barras del sistema.

El contenido permanece visible cuando no se pueden ocultar las barras del sistema.

Mucho mejor. El contenido no se mueve, a la vez que los controles son completamente visibles, incluso en los automóviles en los que no se pueden controlar las barras del sistema.

7. Cumple con los requisitos de calidad del SO Android Automotive: distracción del conductor

Por último, hay una diferencia importante entre los automóviles y otros factores de forma: se usan para conducir. Por lo tanto, es muy importante limitar las distracciones mientras se conduce. Todas las apps para usar en el SO Android Automotive con el automóvil estacionado deben pausar la reproducción cuando comienza la conducción. Cuando comienza la conducción, aparece una superposición del sistema, y, a su vez, se llama al evento de ciclo de vida onPause para que la app se superponga. Durante esta llamada, las apps deben pausar la reproducción.

Simula la conducción

Navega a la vista del reproductor en el emulador y comienza a reproducir contenido. Luego, sigue los pasos para simular la conducción y observa que, si bien el sistema oculta la IU de la app, la reproducción no se pausa. Infringe el lineamiento de calidad DD-2 de apps para automóviles.

839af1382c1f10ca.png

Pausa la reproducción cuando comienzas a conducir

  1. Agrega una dependencia en el artefacto androidx.lifecycle:lifecycle-runtime-compose, que contiene el LifecycleEventEffect que ayuda a ejecutar el código en 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. Después de sincronizar el proyecto para descargar la dependencia, agrega un LifecycleEventEffect que se ejecute en el evento ON_PAUSE para pausar la reproducción.

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()
    }
    ...
}

Una vez implementada la corrección, sigue los mismos pasos que hiciste antes para simular la conducción durante la reproducción activa y observa que la reproducción se detiene y se cumple con el requisito DD-2.

8. Prueba la app en el emulador de pantalla distante

Un nuevo parámetro de configuración que comenzará a aparecer en los automóviles es un ajuste de dos pantallas con una pantalla principal en la consola central y una pantalla secundaria en la parte superior del panel, cerca del parabrisas. Las apps se pueden mover de la pantalla central a la pantalla secundaria y viceversa para brindar más opciones a los conductores y pasajeros.

Instala la imagen Automotive Distant Display

  1. Primero, abre SDK Manager en Android Studio y selecciona la pestaña SDK Platforms si aún no está seleccionada. En la esquina inferior derecha de la ventana de SDK Manager, asegúrate de que esté marcada la casilla junto a Show package details.
  2. Instala la imagen del emulador Automotive Distant Display with Google APIs para la arquitectura de tu computadora (x86/ARM).

Crea un dispositivo virtual del SO Android Automotive

  1. Después de abrir el Administrador de dispositivos, selecciona Automotive debajo de la columna Category en la parte izquierda de la ventana. Luego, selecciona el perfil de hardware incluido Automotive Distant Display de la lista y haz clic en Next.
  2. En la siguiente página, selecciona la imagen del sistema del paso anterior. Haz clic en Next y selecciona las opciones avanzadas que desees antes de crear el AVD haciendo clic en Finish.

Ejecuta la app

Ejecuta la app en el emulador que acabas de crear usando la configuración de ejecución app existente. Sigue las instrucciones que se indican en Usa el emulador de pantalla distante para mover la app hacia la pantalla distante y desde esta. Prueba mover la app cuando esté en la pantalla principal o de detalles y en la del reproductor, y prueba interactuar con la app en ambas pantallas.

b277bd18a94e9c1b.png

9. Mejora la experiencia de la app en la pantalla distante

Cuando usaste la app en la pantalla distante, es posible que hayas notado dos cosas:

  1. La reproducción se reinicia cuando la app se mueve hacia la pantalla distante y desde esta
  2. No puedes interactuar con la app mientras esté en la pantalla distante, lo que incluye cambiar el estado de reproducción.

Mejora la continuidad de las apps

El problema por el que se reinicia la reproducción se debe a que se vuelve a crear la actividad por un cambio de configuración. Como la app se escribe con Compose y la configuración que cambia está relacionada con el tamaño, es sencillo permitir que Compose controle los cambios de configuración para ti restringiendo la recreación de actividades para los cambios de configuración basados en el tamaño. De esta manera, la transición entre pantallas se vuelve fluida sin interrupciones en la reproducción ni recargas debido a la recreación de actividades.

AndroidManifest.xml

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

Implementa los controles de reproducción

Para solucionar el problema por el que no se puede controlar la app mientras está en la pantalla distante, puedes implementar MediaSession. Las sesiones multimedia proporcionan una forma universal de interactuar con un reproductor de audio o video. Para obtener más información, consulta Cómo controlar y anunciar la reproducción con una MediaSession.

  1. Agrega una dependencia en el artefacto 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. Crea un MediaSession con su compilador.

PlayerScreen.kt

import androidx.media3.session.MediaSession

@Composable
fun PlayerScreen(...) {
    ...
    val mediaSession = remember(context, player) {
        MediaSession.Builder(context, player).build()
    }
    ...
}
  1. Luego, agrega una línea adicional en el bloque onDispose de DisposableEffect en el elemento componible Player para liberar MediaSession cuando Player salga del árbol de composición.

PlayerScreen.kt

DisposableEffect(Unit) {
    onDispose {
        mediaSession.release()
        player.release()
        ...
    }
}
  1. Por último, cuando estés en la pantalla del reproductor, puedes probar los controles multimedia con el 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

Con eso, la app funciona mucho mejor en automóviles con pantallas distantes. Sin embargo, más que eso, también funciona mejor en otros factores de forma. En los dispositivos que pueden rotar la pantalla o permitir que los usuarios cambien el tamaño de una ventana de la app, la app ahora también se adapta sin problemas en esas situaciones.

Además, gracias a la integración de la sesión multimedia, la reproducción de la app se puede controlar no solo con controles de hardware y software en automóviles, sino también con otras fuentes, como una consulta de Asistente de Google o un botón para pausar en un par de auriculares, lo que les brinda a los usuarios más opciones para controlar la app en todos los factores de forma.

10. Prueba la app en diferentes configuraciones del sistema

Como la app ya funciona bien en la pantalla principal y en la pantalla distante, lo último que debes verificar es cómo maneja las diferentes configuraciones de la barra del sistema y los cortes de pantalla. Según describe en Cómo trabajar con inserciones de ventanas y cortes de pantalla, los dispositivos con el SO Android Automotive pueden tener configuraciones que no cumplan las suposiciones que suelen ser verdaderas en los factores de forma de dispositivos móviles.

En esta sección, descargarás un emulador que se pueda configurar en el tiempo de ejecución, configurarás el emulador para que tenga una barra del sistema izquierda y probarás la app con esa configuración.

Instala la imagen Android Automotive with Google APIs

  1. Primero, abre SDK Manager en Android Studio y selecciona la pestaña SDK Platforms si aún no está seleccionada. En la esquina inferior derecha de la ventana de SDK Manager, asegúrate de que esté marcada la casilla junto a Show package details.
  2. Instala la imagen del emulador Android Automotive with Google APIs del nivel de API 33 para la arquitectura de tu computadora (x86/ARM).

Crea un dispositivo virtual del SO Android Automotive

  1. Después de abrir el Administrador de dispositivos, selecciona Automotive debajo de la columna Category en la parte izquierda de la ventana. Luego, selecciona el perfil de hardware incluido Automotive (1080p landscape) de la lista y haz clic en Next.
  2. En la siguiente página, selecciona la imagen del sistema del paso anterior. Haz clic en Next y selecciona las opciones avanzadas que desees antes de crear el AVD haciendo clic en Finish.

Configura una barra del sistema lateral

Como se detalla en Cómo realizar pruebas con el emulador configurable, hay una variedad de opciones para emular diferentes configuraciones del sistema presentes en los automóviles.

Para los fines de este codelab, se puede usar com.android.systemui.rro.left para probar una configuración diferente de la barra del sistema. Para habilitarlo, usa el siguiente comando:

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

b642703a7278b219.png

Como la app usa el modificador systemBars como contentWindowInsets en Scaffold, el contenido ya se dibuja en un área segura de las barras del sistema. Para ver qué sucedería si la app asumiera que las barras del sistema solo aparecían en la parte inferior y superior de la pantalla, cambia ese parámetro por lo siguiente:

RoadReelsApp.kt

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

¡Uy! La pantalla de lista y de detalles se renderiza detrás de la barra del sistema. Gracias al trabajo anterior, la pantalla del reproductor estaría bien, incluso si las barras del sistema no se pudieran controlar desde entonces.

9898f7298a7dfb4.gif

Antes de pasar a la siguiente sección, asegúrate de revertir el cambio que acabas de realizar en el parámetro windowContentPadding.

11. Trabaja con cortes de pantalla

Por último, algunos automóviles tienen pantallas con cortes de pantalla muy diferentes a los que se ven en los dispositivos móviles. En lugar de las muescas o los cortes de la cámara estenopeica, algunos automóviles con el SO Android Automotive tienen pantallas curvas que hacen que la pantalla no sea rectangular.

Para ver cómo se comporta la app cuando hay un corte de pantalla, primero habilita el corte de pantalla con el siguiente comando:

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

Para probar realmente el comportamiento de la app, habilita también la barra izquierda del sistema que se usó en la última sección, en caso de que aún no lo esté:

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

Sin modificaciones, la app no se renderiza en el corte de pantalla (la forma exacta del corte es difícil de determinar en este momento, pero se aclarará en el siguiente paso). Una app así está bien y ofrece una mejor experiencia que una app que se renderiza en cortes, pero no se adapta a ellos con cuidado.

212628db84981025.gif

Renderiza en el corte de pantalla

Para brindarles a los usuarios la experiencia más envolvente posible, puedes aprovechar mucho más espacio renderizando en el corte de pantalla.

  1. Para renderizar en el corte de pantalla, crea un archivo integers.xml que contenga la anulación específica para los automóviles. Para ello, usa el calificador modo de IU con el valor Car Dock (el nombre es un remanente de cuando solo existía Android Auto, pero también lo usa el SO Android Automotive). Además, como LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS se introdujo en Android R, también agrega el calificador Version de Android con el valor 30. Consulta Cómo usar recursos alternativos para obtener más detalles.

22b7f17657cac3fd.png

  1. En el archivo que acabas de crear (res/values-car-v30/integers.xml), agrega lo siguiente:

integers.xml

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

El valor entero 3 corresponde a LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS y anula el valor predeterminado de 0 de res/values/integers.xml, que corresponde a LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT. Ya se hace referencia a este valor entero en MainActivity.kt para anular el modo establecido por enableEdgeToEdge(). Para obtener más información sobre este atributo, consulta la documentación de referencia.

Ahora, cuando ejecutes la app, observa que el contenido se extiende hasta el corte y se ve muy envolvente. Sin embargo, la barra superior de la aplicación y parte del contenido están parcialmente ocultos por el corte de pantalla, lo que provoca un problema similar a lo que sucedió cuando la app asumió que las barras del sistema solo aparecerían en la parte inferior y superior.

f0eefa42dee6f7c7.gif

Corrige las barras superiores de la aplicación

Para corregir las barras superiores de la aplicación, puedes agregar el siguiente parámetro windowInsets a los elementos componibles CenterAlignedTopAppBar:

RoadReelsApp.kt

import androidx.compose.foundation.layout.safeDrawing

...

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

Como safeDrawing consta de las inserciones displayCutout y systemBars, mejora el parámetro windowInsets predeterminado, que solo usa systemBars cuando se posiciona la barra superior de la aplicación.

Además, como la barra superior de la aplicación se posiciona en la parte superior de la ventana, no debes incluir el componente inferior de las inserciones safeDrawing. Si lo haces, es posible que se agregue padding innecesario.

7d59ebb63ada5f71.gif

Corrige la pantalla principal

Una opción para corregir el contenido en la pantalla principal y de detalles sería usar safeDrawing en lugar de systemBars para contentWindowInsets de Scaffold. Sin embargo, la app se ve mucho menos envolvente con esa opción, ya que el contenido se corta de forma abrupta donde comienza el corte de pantalla, lo que no es mucho mejor que si la app no se renderizara en el corte de pantalla.

6b3824ca3214cbfa.gif

Para obtener una interfaz de usuario más envolvente, puedes controlar las inserciones en cada componente dentro de la pantalla.

  1. Actualiza contentWindowInsets de Scaffold para que sea constantemente de 0 dp (en lugar de solo para PlayerScreen). De esta manera, cada pantalla o componente dentro de una pantalla puede determinar cómo se comporta con respecto a las inserciones.

RoadReelsApp.kt

Scaffold(
    ...,
    contentWindowInsets = WindowInsets(0.dp)
) { ... }
  1. Establece windowInsetsPadding de los elementos componibles Text del encabezado de la fila para usar los componentes horizontales de las inserciones safeDrawing. El componente superior de estas inserciones es controlado por la barra superior de la aplicación, y el componente inferior se mencionará más adelante.

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. Quita el parámetro contentPadding de LazyRow. Luego, al principio y al final de cada LazyRow, agrega un Spacer del ancho del componente safeDrawing correspondiente para asegurarte de que todas las miniaturas se puedan ver por completo. Usa el modificador widthIn para asegurarte de que estos separadores tengan mínimo el mismo ancho que el padding del contenido. Sin estos elementos, los elementos al principio y los finales de la fila podrían estar ocultos detrás de las barras del sistema o el corte de pantalla, incluso cuando se deslicen por completo al principio o al final de la fila.

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 último, agrega Spacer al final de LazyColumn para tener en cuenta las posibles barras del sistema o las inserciones de corte de pantalla en la parte inferior de la pantalla. No es necesario un separador equivalente en la parte superior de LazyColumn, ya que la barra superior de la aplicación se encarga de eso. Si la app usara una barra inferior en lugar de una superior, deberías agregar Spacer al comienzo de la lista con el modificador windowInsetsTopHeight. Además, si la app usara una barra inferior y superior, no se necesitaría ningún separador.

MainScreen.kt

import androidx.compose.foundation.layout.windowInsetsBottomHeight

...

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

Perfecto. Las barras superiores de la aplicación son completamente visibles, y, cuando te desplazas hasta el final de una fila, ahora puedes ver todas las miniaturas en su totalidad.

543706473398114a.gif

Corrige la pantalla de detalles

f622958a8d0c16c8.png

La pantalla de detalles no es tan mala, pero el contenido aún se corta.

Como la pantalla de detalles no tiene contenido desplazable, para solucionarlo, solo se necesita agregar un modificador windowInsetsPadding en el nivel superior Box.

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

Corrige la pantalla del reproductor

Si bien PlayerScreen ya aplica padding para algunas o todas las inserciones de ventana de la barra del sistema, como se comenta en la sección anterior Cumple con los requisitos de calidad del SO Android Automotive: navegabilidad, no es suficiente para asegurarte de que no se oculte ahora que la app se renderiza en cortes de pantalla. En los dispositivos móviles, los recortes de pantalla casi siempre están contenidos, por completo, dentro de las barras del sistema. Sin embargo, en los automóviles, los cortes de pantalla pueden extenderse mucho más allá de las barras del sistema, lo que no cumple las suposiciones.

427227df5e44f554.png

Para solucionar este problema, cambia el valor inicial de la variable windowInsetsForPadding de cero a displayCutout:

PlayerScreen.kt

import androidx.compose.foundation.layout.displayCutout

...

var windowInsetsForPadding = WindowInsets(WindowInsets.displayCutout)

b523d8c1e1423757.gif

Genial. La app aprovecha al máximo la pantalla y sigue siendo funcional.

Además, si ejecutas la app en un dispositivo móvil, también será más envolvente. Los elementos de la lista se renderizan hasta los bordes de la pantalla, incluso detrás de la barra de navegación.

dc7918499a33df31.png

12. Felicitaciones

Migraste y optimizaste, de forma correcta, tu primera app para usarse en el automóvil estacionado. Ahora es el momento de aplicar lo que aprendiste a tu propia app.

Pruebas para hacer

Lecturas adicionales