1. Antes de comenzar
Qué no es este codelab
- Una guía para crear apps de música (audio, p. ej., música, radio, podcasts) para Android Auto y el SO Android Automotive (consulta Cómo compilar apps de música para automóviles si deseas obtener más información al respecto)
Requisitos
- La versión más reciente de Android Studio
- Experiencia con Kotlin básico
- Debes tener experiencia en la creación de dispositivos virtuales de Android y su ejecución en Android Emulator.
- Debes tener conocimientos básicos de Jetpack Compose.
- Debes tener conocimientos de los efectos secundarios.
- Debes tener conocimientos de las inserciones de ventanas.
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 |
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
- El código para este codelab se puede encontrar en el directorio
build-a-parked-app
dentro del repositoriocar-codelabs
de GitHub. Para clonarlo, ejecuta el siguiente comando:
git clone https://github.com/android/car-codelabs.git
- 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 directoriobuild-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 vehículos 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 son adaptativas 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
- 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.
- Instala la imagen del emulador Android Automotive with Google APIs del nivel de API 33 que se indica 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
- 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 (1408p landscape) de la lista y haz clic en Next.
- 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.
5. Actualiza el manifiesto para declarar la compatibilidad con el 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. Estos cambios se pueden realizar de manera tal que el mismo APK o paquete de aplicación sea compatible con dispositivos móviles y que tengan el SO Android Automotive. El primer conjunto de cambios es actualizar el archivo AndroidManifest.xml
para indicar que la app es compatible con dispositivos que ejecutan el SO Android Automotive y que es una app de video.
Declara la función de hardware automotriz
Para indicar que tu app es compatible con dispositivos con el SO Android Automotive, agrega el siguiente elemento <uses-feature>
al archivo AndroidManifest.xml:
AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
...
<uses-feature
android:name="android.hardware.type.automotive"
android:required="false" />
...
</manifest>
Usar un valor de false
para el atributo android:required
permite que el APK o el paquete de aplicación generados se distribuyan a dispositivos con el SO Android Automotive y a dispositivos móviles. Consulta Elige un tipo de segmento para el SO Android Automotive para obtener más información.
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.
- 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:
main
- Directory name:
xml
- 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>
- Dentro del elemento
<application>
existente, agrega el siguiente elemento<meta-data>
que hace referencia al archivoautomotive_app_desc.xml
que acabas de crear.
AndroidManifest.xml
<application ...>
<meta-data
android:name="com.android.automotive"
android:resource="@xml/automotive_app_desc" />
</application>
Con eso, realizaste todos los cambios necesarios para declarar la compatibilidad con el SO Android Automotive.
6. Cumple con los requisitos de calidad del SO Android Automotive: navegabilidad
Si bien declarar la compatibilidad con el SO Android Automotive es una parte de adaptar tu app para automóviles, aún es necesario asegurarte de que esta se pueda usar y sea segura.
Agrega indicaciones visuales 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
...
} else if (route?.startsWith(Screen.Detail.name) == true) {
CenterAlignedTopAppBar(
title = { Text(stringResource(R.string.bbb_title)) },
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:
- Actualiza el elemento componible
TopControls
para que tome un parámetro de devolución de llamada con el nombreonClose
y agrega unIconButton
que lo llame cuando se haga clic.
PlayerControls.kt
import androidx.compose.material.icons.twotone.Close
...
@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) { ... }
}
}
- Actualiza el elemento componible
PlayerControls
para que también tome un parámetro de devolución de llamadaonClose
y lo pase aTopControls
.
PlayerControls.kt
fun PlayerControls(
uiState: PlayerUiState,
onClose: () -> Unit,
onPlayPause: () -> Unit,
onSeek: (seekToMillis: Long) -> Unit,
modifier: Modifier = Modifier,
) {
AnimatedVisibility(
visible = uiState.isShowingControls,
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 = uiState.mediaMetadata.title?.toString(),
onClose = onClose
)
...
}
}
}
- A continuación, actualiza el elemento componible
PlayerScreen
para que tome el mismo parámetro y lo pase a suPlayerControls
.
PlayerScreen.kt
@Composable
fun PlayerScreen(
onClose: () -> Unit,
modifier: Modifier = Modifier,
viewModel: PlayerViewModel = viewModel()
) {
...
PlayerControls(
modifier = Modifier
.fillMaxSize(),
uiState = playerUiState,
onClose = onClose,
onPlayPause = { if (playerUiState.isPlaying) viewModel.pause() else viewModel.play() },
onSeek = viewModel::seekTo
)
}
- Por último, en
RoadReelsNavHost
, proporciona la implementación que se pasa aPlayerScreen
:
RoadReelsNavHost.kt
composable(route = Screen.Player.name, ...) {
PlayerScreen(onClose = { navController.popBackStack() })
}
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.
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.
- 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)
}
}
- 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
...
DisposableEffect(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 (activity.supportedOrientations().contains(SupportedOrientation.Landscape)
&& !activity.isInMultiWindowMode
) {
activity.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 | 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 | 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 (incluidas las variantes sensor
, reverse
y user
de cada una), y quítalos o protégelos según sea necesario para limitar el formato letterbox. Para obtener más información, consulta Modo de compatibilidad del dispositivo.
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 den por sentado 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, ya no se puede navegar por la app, 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.
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.
- 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 archivoLocalControllableInsets.kt
en el paquetecom.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 }
- 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)
}
}
- Agrega un
CompositionLocalProvider
de nivel superior que contenga los elementos componibles de la app y el tema, y que vincule valores aLocalControllableInsets
.
MainActivity.kt
import androidx.compose.runtime.CompositionLocalProvider
...
CompositionLocalProvider(LocalControllableInsets provides controllableInsetsTypeMask) {
RoadReelsTheme {
RoadReelsApp(calculateWindowSizeClass(this))
}
}
- En el reproductor, lee el valor actual y úsalo para determinar las inserciones que se ocultarán y las que se utilizará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.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.unit.dp
import com.example.android.cars.roadreels.LocalControllableInsets
...
val controllableInsetsTypeMask by rememberUpdatedState(LocalControllableInsets.current)
DisposableEffect(Unit) {
...
windowInsetsController.hide(WindowInsetsCompat.Type.systemBars().and(controllableInsetsTypeMask))
...
}
...
// 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. |
Mucho mejor: el contenido no se mueve y, al mismo tiempo, 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 las restricciones de la experiencia del usuario se activen y evitar que se reanude la reproducción mientras estas restricciones estén activas. Cuando se activan las restricciones de la experiencia del usuario, aparece una superposición del sistema y, a su vez, se llama al evento de ciclo de vida onPause
para que se superponga sobre la app. 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.
Pausa la reproducción cuando comienzas a conducir
- Agrega una dependencia en el artefacto
androidx.lifecycle:lifecycle-runtime-compose
, que contiene elLifecycleEventEffect
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)
- Después de sincronizar el proyecto para descargar la dependencia, agrega un
LifecycleEventEffect
que se ejecute en el eventoON_PAUSE
para pausar la reproducción (y, de manera opcional, en el eventoON_RESUME
para reanudarla).
PlayerScreen.kt
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LifecycleEventEffect
...
@Composable
fun PlayerScreen(...) {
...
LifecycleEventEffect(Lifecycle.Event.ON_PAUSE) {
viewModel.pause()
}
LifecycleEventEffect(Lifecycle.Event.ON_RESUME) {
viewModel.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
- Primero, abre Preview en 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.
- Instala la imagen del emulador Automotive Distant Display with Google Play del nivel de API 33 para la arquitectura de tu computadora (x86/ARM).
Crea un dispositivo virtual del SO Android Automotive
- 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 with Google Play de la lista y haz clic en Next.
- 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.
9. Mejora la experiencia en la app en la pantalla distante
Cuando usaste la app en la pantalla distante, es posible que hayas notado dos cosas:
- La reproducción presenta saltos cuando la app se mueve hacia la pantalla distante y desde esta.
- 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
Los saltos en la reproducción se deben 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.
- 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)
- En
PlayerViewModel
, agrega una variable para contener la sesión multimedia y crea unMediaSession
con su compilador.
PlayverViewModel.kt
import androidx.media3.session.MediaSession
...
class PlayerViewModel(...) {
...
private var mediaSession: MediaSession? = null
init {
viewModelScope.launch {
_player.onEach { player ->
playerUiStateUpdateJob?.cancel()
mediaSession?.release()
if (player != null) {
initializePlayer(player)
mediaSession = MediaSession.Builder(application, player).build()
playerUiStateUpdateJob = viewModelScope.launch {... }
}
}.collect()
}
}
}
- Luego, agrega una línea adicional en el método
onCleared
para liberarMediaSession
cuando ya no se necesitePlayerViewModel
.
PlayerViewModel.kt
override fun onCleared() {
super.onCleared()
mediaSession?.release()
_player.value?.release()
}
- Por último, cuando estés en la pantalla del reproductor (con la app en la pantalla principal o distante), podrás 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
Restringe la reanudación de la reproducción
Si bien la compatibilidad con MediaSession
permite controlar la reproducción mientras la app está en la pantalla distante, presenta un problema nuevo. En particular, permite que se reanude la reproducción mientras se aplican restricciones de experiencia del usuario, lo que incumple el lineamiento de calidad DD-2 (otra vez). Para probarlo por tu cuenta, haz lo siguiente:
- Inicia la reproducción.
- Simula la conducción.
- Usa el comando
media_session dispatch
. Observa que la reproducción se reanuda aunque la app esté oculta.
Para solucionar este problema, puedes escuchar las restricciones de la experiencia del usuario del dispositivo y solo permitir la reanudación de la reproducción mientras estén activas. Esto incluso se puede hacer de una manera en la que se pueda usar la misma lógica para dispositivos móviles y el SO Android Automotive.
- En el archivo
build.gradle
del móduloapp
, agrega lo siguiente para incluir la biblioteca de Android Automotive y, luego, realiza una sincronización de Gradle:
build.gradle.kts
android {
...
useLibrary("android.car")
}
- Haz clic con el botón derecho en el paquete
com.example.android.cars.roadreels
y selecciona New > Kotlin Class/File. IngresaRoadReelsPlayer
como nombre y haz clic en el tipo Class. - En el archivo que acabas de crear, agrega la siguiente implementación inicial de la clase. Cuando se extiende
ForwardingSimpleBasePlayer
, es sencillo modificar los comandos y las interacciones admitidos para un reproductor unido anulando el métodogetState()
.
RoadReelsPlayer.kt
import android.content.Context
import androidx.media3.common.ForwardingSimpleBasePlayer
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer
@UnstableApi
class RoadReelsPlayer(context: Context) :
ForwardingSimpleBasePlayer(ExoPlayer.Builder(context).build()) {
private var shouldPreventPlay = false
override fun getState(): State {
val state = super.getState()
return state.buildUpon()
.setAvailableCommands(
state.availableCommands.buildUpon().removeIf(COMMAND_PLAY_PAUSE, shouldPreventPlay)
.build()
).build()
}
}
- En
PlayerViewModel.kt
, actualiza la declaración de la variable del reproductor para usar una instancia deRoadReelsPlayer
en lugar deExoPlayer
. En este momento, el comportamiento será exactamente el mismo que antes, ya queshouldPreventPlay
nunca se actualiza desde su valor predeterminado defalse
.
PlayerViewModel.kt
init {
...
_player.update { RoadReelsPlayer(application) }
}
- Para comenzar a hacer un seguimiento de las restricciones de la experiencia del usuario, agrega el siguiente bloque
init
y la implementaciónhandleRelease
:
RoadReelsPlayer.kt
import android.car.Car
import android.car.drivingstate.CarUxRestrictions
import android.car.drivingstate.CarUxRestrictionsManager
import android.content.pm.PackageManager
import com.google.common.util.concurrent.ListenableFuture
...
@UnstableApi
class RoadReelsPlayer(context: Context) :
ForwardingSimpleBasePlayer(ExoPlayer.Builder(context).build()) {
...
private var pausedByUxRestrictions = false
private lateinit var carUxRestrictionsManager: CarUxRestrictionsManager
init {
with(context) {
// Only listen to UX restrictions if the device is running Android Automotive OS
if (packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) {
val car = Car.createCar(context)
carUxRestrictionsManager =
car.getCarManager(Car.CAR_UX_RESTRICTION_SERVICE) as CarUxRestrictionsManager
// Get the initial UX restrictions and update the player state
shouldPreventPlay =
carUxRestrictionsManager.currentCarUxRestrictions.isRequiresDistractionOptimization
invalidateState()
// Register a listener to update the player state as the UX restrictions change
carUxRestrictionsManager.registerListener { carUxRestrictions: CarUxRestrictions ->
shouldPreventPlay = carUxRestrictions.isRequiresDistractionOptimization
if (!shouldPreventPlay && pausedByUxRestrictions) {
handleSetPlayWhenReady(true)
invalidateState()
} else if (shouldPreventPlay && isPlaying) {
pausedByUxRestrictions = true
handleSetPlayWhenReady(false)
invalidateState()
}
}
}
addListener(object : Player.Listener {
override fun onEvents(player: Player, events: Player.Events) {
if (events.contains(EVENT_IS_PLAYING_CHANGED) && isPlaying) {
pausedByUxRestrictions = false
}
}
})
}
}
...
override fun handleRelease(): ListenableFuture<*> {
if (::carUxRestrictionsManager.isInitialized) {
carUxRestrictionsManager.unregisterListener()
}
return super.handleRelease()
}
}
Ten en cuenta lo siguiente:
CarUxRestrictionsManager
se almacena como una variablelateinit
, ya que no se crea una instancia ni se usa en dispositivos que no tengan el SO Android Automotive, pero se debe limpiar su objeto de escucha cuando se lance el reproductor.- Solo se hace referencia al valor
isRequiresDistractionOptimization
cuando se determina el estado de restricción de la UX. Si bien la claseCarUxRestrictions
contiene detalles adicionales sobre qué restricciones están activas, no es necesario hacer referencia a ellas, ya que solo están diseñadas para que las usen las apps con optimización de distracciones (como las apps de navegación), ya que esas apps siguen siendo visibles mientras las restricciones están activas. - Después de cualquier actualización de la variable
shouldPreventPlay
, se llama ainvalidateState()
para informar a los usuarios sobre el estado del cambio del reproductor. - En el objeto de escucha, la reproducción se pausa o se reanuda automáticamente llamando a
handleSetPlayWhenReady
con el valor adecuado.
- Ahora, prueba reanudar la reproducción mientras simulas la conducción como se describe al comienzo de esta sección y observa que no se reanuda.
- Por último, dado que
RoadReelsPlayer
controla la pausa de la reproducción cuando las restricciones de la experiencia del usuario se vuelven activas, no es necesario queLifecycleEventEffect
pause el reproductor duranteON_PAUSE
. En su lugar, se puede cambiar aON_STOP
, de modo que la reproducción se detenga cuando el usuario salga de la app para ir al selector o abrir otra app.
PlayerScreen.kt
LifecycleEventEffect(Lifecycle.Event.ON_START) {
viewModel.play()
}
LifecycleEventEffect(Lifecycle.Event.ON_STOP) {
viewModel.pause()
}
Resumen
Con eso, la app funciona mucho mejor en automóviles, tanto con pantallas distantes como sin ellas. 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 pregunta a 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, aprenderás a configurar el emulador para que tenga una barra del sistema izquierda y probarás la app con esa configuración.
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
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.
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.top_and_right
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.
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.
- 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 el valor que usarás,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.
- 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 dio por sentado que las barras del sistema solo aparecerían en la parte inferior y superior.
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.WindowInsetsSides
import androidx.compose.foundation.layout.only
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.
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.
Para obtener una interfaz de usuario más envolvente, puedes controlar las inserciones en cada componente dentro de la pantalla.
- Actualiza
contentWindowInsets
deScaffold
para que sea constantemente de 0 dp (en lugar de solo paraPlayerScreen
). 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)
) { ... }
- Establece
windowInsetsPadding
de los elementos componiblesText
del encabezado de la fila para usar los componentes horizontales de las insercionessafeDrawing
. 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))
)
...
}
- Quita el parámetro
contentPadding
deLazyRow
. Luego, al principio y al final de cadaLazyRow
, agrega unSpacer
del ancho del componentesafeDrawing
correspondiente para asegurarte de que todas las miniaturas se puedan ver por completo. Usa el modificadorwidthIn
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))
)
}
}
- Por último, agrega
Spacer
al final deLazyColumn
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 deLazyColumn
, 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 agregarSpacer
al comienzo de la lista con el modificadorwindowInsetsTopHeight
. 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))
}
}
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.
Corrige la pantalla de detalles
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)
) { ... }
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.
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)
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.
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
- Anula algunos de los valores de los recursos de dimensión para aumentar el tamaño de los elementos cuando se ejecuten en un automóvil.
- Prueba aún más configuraciones del emulador configurable.
- Prueba la app con algunas de las imágenes del emulador de OEM disponibles.
Lecturas adicionales
- Cómo compilar apps para usar en el SO Android Automotive con el auto estacionado
- Compilación de apps de video para el SO Android Automotive
- Cómo crear juegos para el SO Android Automotive
- Cómo compilar navegadores para el SO Android Automotive
- En la página Calidad de las apps para Android para vehículos, se describen los criterios con los que debe cumplir tu app para generar una excelente experiencia del usuario y aprobar la revisión de Play Store. Asegúrate de filtrar la categoría de la app.