1. Antes de comenzar
En este codelab, aprenderás a compilar apps con optimización de distracciones para Android Auto y el SO Android Automotive usando la biblioteca de apps de Android para vehículos. Primero agregarás compatibilidad con Android Auto y, luego, con un mínimo de trabajo adicional, crearás una variante de la app que se pueda ejecutar en el SO Android Automotive. Una vez que la app funcione en ambas plataformas, crearás una pantalla adicional y algunas funciones básicas de interactividad.
Qué no es este codelab
- Una guía para crear apps de música (audio) 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)
- Una guía para crear apps de mensajería para Android Auto (consulta Cómo compilar apps de mensajería para Android Auto si deseas obtener más información al respecto)
Requisitos
- Tener la versión preliminar de Android Studio (los emuladores del SO Android Automotive solo están disponibles a través de la versión preliminar de Android Studio, por lo que si aún no la instalaste, puedes iniciar el codelab con la versión estable mientras se descarga la versión preliminar)
- Experiencia con Kotlin básico
- Conocimientos básicos de los servicios de Android
- Experiencia en la creación de dispositivos virtuales de Android y su ejecución en Android Emulator
- Conocimientos básicos de la modularización de apps para Android
- Conocimientos básicos del patrón de diseño Builder
Qué compilarás
Android Auto | SO Android Automotive |
Qué aprenderás
- Cómo funciona la arquitectura cliente-host de la biblioteca de apps para vehículos
- Cómo escribir tus propias clases
CarAppService
,Session
yScreen
- Cómo compartir tu implementación en Android Auto y el SO Android Automotive
- Cómo usar la consola central de computadora para ejecutar Android Auto en tu máquina de desarrollo
- Cómo ejecutar el emulador del SO Android Automotive
2. Prepárate
Obtén el código
- El código para este codelab se puede encontrar en el directorio
car-app-library-fundamentals
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
car-app-library-fundamentals/start
. El directoriocar-app-library-fundamentals/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.
Observa que está dividido en dos módulos, :app
y :common:data
.
El módulo :app
contiene la IU y la lógica de la app para dispositivos móviles, mientras que el módulo :common:data
contiene la clase de datos del modelo Place
y el repositorio que se usa para leer modelos Place
. Con el objetivo de simplificar los pasos, el repositorio lee desde una lista codificada, pero podría hacerlo desde una base de datos o un servidor de backend en una app real.
El módulo :app
incluye una dependencia en el módulo :common:data
para que pueda leer y presentar la lista de modelos Place
.
3. Aprende sobre la biblioteca de apps de Android para vehículos
La biblioteca de apps de Android para vehículos es un conjunto de bibliotecas de Jetpack que permite a los desarrolladores compilar apps que se pueden usar en vehículos. Proporciona un framework en plantillas que brinda interfaces de usuario optimizadas para la conducción y, a su vez, se encarga de la adaptación de las distintas configuraciones de hardware de los automóviles (por ejemplo, métodos de entrada, tamaños de pantallas y relaciones de aspecto). Todo esto combinado ayuda a los desarrolladores a compilar una app y tener la confianza de que funcionará como se espera en diferentes autos que ejecutan Android Auto y el SO Android Automotive.
Descubre cómo funciona
Las apps compiladas con la biblioteca de apps para vehículos no se ejecutan directamente en Android Auto ni en el SO Android Automotive. En su lugar, emplean una app host que se comunica con las apps cliente y renderiza las interfaces de usuario del cliente por ellas. Android Auto en sí misma es un host, y Google Automotive App Host es el host que se usa en vehículos con el SO Android Automotive con Google integrado. Las siguientes son las clases clave de la biblioteca de apps para vehículos que debes extender cuando compilas tu app:
CarAppService
CarAppService
es una subclase de la clase Service
de Android y actúa como punto de entrada para que las aplicaciones de host se comuniquen con las apps cliente (como la que compilarás en este codelab). Su objetivo principal es crear instancias de Session
con las que interactúa la app host.
Session
Puedes pensar en una Session
como una instancia de una app cliente que se ejecuta en una pantalla del vehículo. Al igual que otros componentes de Android, tiene un ciclo de vida propio que se puede usar para inicializar y anular recursos utilizados durante la existencia de la instancia de Session
. Hay una relación de uno a varios entre CarAppService
y Session
. Por ejemplo, un CarAppService
puede tener dos instancias de Session
: una para una pantalla principal y otra para una pantalla de clúster (en el caso de las apps de navegación compatibles con pantallas de clúster).
Screen
Las instancias de Screen
se encargan de generar las interfaces de usuario que renderizan las apps host. Estas interfaces de usuario se representan con clases Template
, cada una de las cuales modela un tipo de diseño específico, como una cuadrícula o una lista. Cada Session
administra una pila de instancias de Screen
que controlan los flujos de usuarios en las distintas partes de tu app. Como ocurre con una Session
, una Screen
tiene un ciclo de vida propio en el que puedes implementar hooks.
Si aún no comprendes bien estos conceptos, no te preocupes. Escribirás los componentes CarAppService
, Session
y Screen
en la sección Escribe tu CarAppService de este codelab.
4. Establece la configuración inicial
Para comenzar, configura el módulo que contiene el CarAppService
y declara sus dependencias.
Crea el módulo car-app-service
- Con el módulo
:common
seleccionado en la ventana Project, haz clic con el botón derecho y selecciona la opción New > Module. - En el asistente de módulos que se abre, selecciona la plantilla Android Library (para que otros módulos puedan usar este como dependencia) en la lista del lado izquierdo y usa los siguientes valores:
- Module name:
:common:car-app-service
- Package name:
com.example.places.carappservice
- Minimum SDK:
API 23: Android 6.0 (Marshmallow)
Configura dependencias
- En el archivo
build.gradle
de nivel de proyecto, agrega una declaración de variable para la versión de la biblioteca de apps para vehículos como se indica a continuación. Esto te permite usar fácilmente la misma versión en todos los módulos de la app.
build.gradle (proyecto: Places)
buildscript {
ext {
// All versions can be found at https://developer.android.com/jetpack/androidx/releases/car-app
car_app_library_version = '1.3.0-rc01'
...
}
}
- Luego, agrega dos dependencias al archivo
build.gradle
del módulo:common:car-app-service
.
androidx.car.app:app
. Este es el artefacto principal de la biblioteca de apps para vehículos y contiene todas las clases fundamentales que se usan en la compilación de apps. Hay otros tres artefactos que componen la biblioteca:androidx.car.app:app-projected
para funciones específicas de Android Auto,androidx.car.app:app-automotive
para el código de funciones del SO Android Automotive yandroidx.car.app:app-testing
para algunos asistentes útiles a la hora de hacer pruebas de unidades. Utilizarásapp-projected
yapp-automotive
más adelante en este codelab.:common:data
. Este es el mismo módulo de datos que usa la app para dispositivos móviles existente y permite que se use la misma fuente de datos para cada versión de la experiencia en la app.
build.gradle (módulo :common:car-app-service)
dependencies {
...
implementation "androidx.car.app:app:$car_app_library_version"
implementation project(":common:data")
...
}
Con este cambio, el gráfico de dependencias para los módulos propios de la app es el siguiente:
Ahora que se configuraron las dependencias, es hora de escribir el CarAppService
.
5. Escribe tu CarAppService
- Para empezar, crea un archivo llamado
PlacesCarAppService.kt
en el paquetecarappservice
dentro del módulo:common:car-app-service
. - Dentro de este archivo, crea una clase llamada
PlacesCarAppService
, que extiendeCarAppService
.
PlacesCarAppService.kt
class PlacesCarAppService : CarAppService() {
override fun createHostValidator(): HostValidator {
return HostValidator.ALLOW_ALL_HOSTS_VALIDATOR
}
override fun onCreateSession(): Session {
// PlacesSession will be an unresolved reference until the next step
return PlacesSession()
}
}
La clase abstracta CarAppService
implementa métodos Service
, como onBind
y onUnbind
, por ti y previene más anulaciones de esos métodos para garantizar una interoperabilidad adecuada con las aplicaciones host. Solo tienes que implementar createHostValidator
y onCreateSession
.
Se hace referencia al HostValidator
que muestras desde createHostValidator
cuando se vincula tu CarAppService
para garantizar que el host sea de confianza y que la vinculación falle si el host no coincide con los parámetros que defines. Para este codelab (y las pruebas en general), ALLOW_ALL_HOSTS_VALIDATOR
facilita asegurarse de que tu app se conecte, pero no debería usarse en producción. Consulta la documentación de createHostValidator
si deseas obtener más información sobre cómo configurarlo para una app de producción.
En el caso de una app tan simple como esta, onCreateSession
puede devolver una instancia de una Session
. En una app más compleja, este sería un lugar adecuado para inicializar recursos de larga duración como métricas y clientes de registro que se usan mientras tu app se ejecuta en el vehículo.
- Por último, debes agregar el elemento
<service>
que corresponde alPlacesCarAppService
en el archivoAndroidManifest.xml
del módulo:common:car-app-service
para permitir que el sistema operativo (y, por extensión, otras apps, como los hosts) sepa que existe.
AndroidManifest.xml (:common:car-app-service)
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!--
This AndroidManifest.xml will contain all of the elements that should be shared across the
Android Auto and Automotive OS versions of the app, such as the CarAppService <service> element
-->
<application>
<service
android:name="com.example.places.carappservice.PlacesCarAppService"
android:exported="true">
<intent-filter>
<action android:name="androidx.car.app.CarAppService" />
<category android:name="androidx.car.app.category.POI" />
</intent-filter>
</service>
</application>
</manifest>
Aquí hay dos puntos importantes para tener en cuenta:
- El elemento
<action>
permite que las aplicaciones host (y del selector) encuentren la app. - El elemento
<category>
declara la categoría de la app, que determina qué criterios de calidad de la app debe cumplir (más adelante habrá detalles al respecto). Otros valores posibles sonandroidx.car.app.category.NAVIGATION
yandroidx.car.app.category.IOT
.
Crea la clase PlacesSession
- Crea un archivo
PlacesCarAppService.kt
y agrega el siguiente código:
PlacesCarAppService.kt
class PlacesSession : Session() {
override fun onCreateScreen(intent: Intent): Screen {
// MainScreen will be an unresolved reference until the next step
return MainScreen(carContext)
}
}
En el caso de una app simple como esta, puedes mostrar la pantalla principal en onCreateScreen
. No obstante, como este método toma un Intent
como parámetro, una app con más funciones también podría leer de él y propagar una pila de actividades de pantallas o usar otra lógica condicional.
Crea la clase MainScreen
A continuación, crearás un nuevo paquete llamado screen.
- Haz clic con el botón derecho en el paquete
com.example.places.carappservice
y selecciona New > Package (el nombre completo del paquete serácom.example.places.carappservice.screen
). Aquí colocas todas las subclasesScreen
de la app. - En el paquete
screen
, crea un archivo llamadoMainScreen.kt
para contener la claseMainScreen
, que extiendeScreen
. Por ahora, muestra el mensaje simple Hello, world! usandoPaneTemplate
.
MainScreen.kt
class MainScreen(carContext: CarContext) : Screen(carContext) {
override fun onGetTemplate(): Template {
val row = Row.Builder()
.setTitle("Hello, world!")
.build()
val pane = Pane.Builder()
.addRow(row)
.build()
return PaneTemplate.Builder(pane)
.setHeaderAction(Action.APP_ICON)
.build()
}
}
6. Agrega compatibilidad con Android Auto
Si bien ya implementaste toda la lógica necesaria para que la app funcione, hay dos partes más de configuración para establecer antes de que puedas ejecutarla en Android Auto.
Agrega una dependencia en el módulo car-app-service
En el archivo build.gradle
del módulo :app
, agrega lo siguiente:
build.gradle (módulo :app)
dependencies {
...
implementation project(path: ':common:car-app-service')
...
}
Con este cambio, el gráfico de dependencias para los módulos propios de la app es el siguiente:
Esto agrupa el código que acabas de escribir en el módulo :common:car-app-service
junto con otros componentes incluidos en la biblioteca de apps para vehículos, como la actividad de otorgamiento de permisos proporcionada.
Declara los metadatos de com.google.android.gms.car.application
- Haz clic con el botón derecho en el módulo
:common:car-app-service
y selecciona la opción New > Android Resource File. Luego, anula los siguientes valores:
- File name:
automotive_app_desc.xml
- Resource type:
XML
- Root element:
automotiveApp
- Dentro de ese archivo, agrega el siguiente elemento
<uses>
para declarar que tu app usa las plantillas que proporciona la biblioteca de apps para vehículos.
automotive_app_desc.xml
<?xml version="1.0" encoding="utf-8"?>
<automotiveApp>
<uses name="template"/>
</automotiveApp>
- En el archivo
AndroidManifest.xml
del módulo:app
, agrega el siguiente elemento<meta-data>
que hace referencia al archivoautomotive_app_desc.xml
que acabas de crear.
AndroidManifest.xml (:app)
<application ...>
<meta-data
android:name="com.google.android.gms.car.application"
android:resource="@xml/automotive_app_desc" />
...
</application>
Android Auto lee este archivo y le informa cuáles son las capacidades que admite tu app; en este caso, que usa el sistema de plantillas de la biblioteca de apps para vehículos. Luego, esta información se usa para controlar comportamientos como agregar la app al selector de Android Auto y abrirla desde las notificaciones.
Opcional: Escucha cambios de proyección
En ocasiones, quieres saber si el dispositivo de un usuario está conectado a un automóvil. Para averiguarlo, usa la API de CarConnection
, que proporciona LiveData
para observar el estado de conexión.
- Para usar la API de
CarConnection
, primero agrega una dependencia en el módulo:app
en el artefactoandroidx.car.app:app
.
build.gradle (módulo :app)
dependencies {
...
implementation "androidx.car.app:app:$car_app_library_version"
...
}
- A modo de demostración, puedes crear un elemento componible sencillo, como el siguiente, que muestra el estado de conexión actual. En una app real, es posible que este estado se capture en algunos registros, se use para inhabilitar ciertas funciones en la pantalla del teléfono durante la proyección o algo distinto.
MainActivity.kt
@Composable
fun ProjectionState(carConnectionType: Int, modifier: Modifier = Modifier) {
val text = when (carConnectionType) {
CarConnection.CONNECTION_TYPE_NOT_CONNECTED -> "Not projecting"
CarConnection.CONNECTION_TYPE_NATIVE -> "Running on Android Automotive OS"
CarConnection.CONNECTION_TYPE_PROJECTION -> "Projecting"
else -> "Unknown connection type"
}
Text(
text = text,
style = MaterialTheme.typography.bodyMedium,
modifier = modifier
)
}
- Ahora que existe una forma de mostrar los datos, léelos y pásalos al elemento componible, como se demuestra en el siguiente fragmento.
MainActivity.kt
setContent {
val carConnectionType by CarConnection(this).type.observeAsState(initial = -1)
PlacesTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Column {
Text(
text = "Places",
style = MaterialTheme.typography.displayLarge,
modifier = Modifier.padding(8.dp)
)
ProjectionState(
carConnectionType = carConnectionType,
modifier = Modifier.padding(8.dp)
)
PlaceList(places = PlacesRepository().getPlaces())
}
}
}
}
- Si ejecutas la app, debería decir Not projecting.
7. Haz pruebas con la consola central de computadora (DHU)
Con el CarAppService
implementado y la configuración de Android Auto establecida, es hora de ejecutar la app y ver su aspecto.
- Instala la app en tu teléfono y sigue las instrucciones para instalar y ejecutar la DHU.
Con la DHU en funcionamiento, deberías ver el ícono de la app en el selector (si no lo ves, verifica que hayas seguido todos los pasos de la sección anterior; luego, cierra y reinicia la DHU desde la terminal).
Lamentablemente, falló.
- Para ver por qué falló la app, puedes activar o desactivar el ícono de depuración de la esquina superior derecha (solo visible cuando se ejecuta en la DHU) o consultar Logcat en Android Studio.
Error: [type: null, cause: null, debug msg: java.lang.IllegalArgumentException: Min API level not declared in manifest (androidx.car.app.minCarApiLevel) at androidx.car.app.AppInfo.retrieveMinCarAppApiLevel(AppInfo.java:143) at androidx.car.app.AppInfo.create(AppInfo.java:91) at androidx.car.app.CarAppService.getAppInfo(CarAppService.java:380) at androidx.car.app.CarAppBinder.getAppInfo(CarAppBinder.java:255) at androidx.car.app.ICarApp$Stub.onTransact(ICarApp.java:182) at android.os.Binder.execTransactInternal(Binder.java:1285) at android.os.Binder.execTransact(Binder.java:1244) ]
Desde el registro, puedes ver que falta una declaración en el manifiesto para el nivel de API mínimo que admite la app. Antes de agregar esa entrada, es mejor comprender por qué es necesaria.
Como Android, la biblioteca de apps para vehículos también tiene un concepto de niveles de API, ya que debe haber un contrato entre aplicaciones host y cliente para que se comuniquen. Las aplicaciones host admiten un nivel de API determinado y sus funciones asociadas (y, para la retrocompatibilidad, también aquellas de niveles anteriores). Por ejemplo, SignInTemplate
se puede usar en hosts que ejecuten el nivel de API 2 o superior. Pero, si intentaras usarla en un host que solamente admite el nivel de API 1, ese host no conocería el tipo de plantilla ni podría hacer nada significativo con ella.
Durante el proceso de vinculación del host con el cliente, debe haber cierta superposición en los niveles de API admitidos para que la vinculación se realice correctamente. Por ejemplo, si un host admitiera solamente el nivel de API 1, pero una app cliente no pudiera ejecutarse sin funciones del nivel de API 2 (como lo indica esta declaración de manifiesto), las apps no deberían conectarse, porque el cliente no podría ejecutarse correctamente en el host. Por lo tanto, el cliente debe declarar el nivel de API mínimo requerido en su manifiesto para garantizar que solo un host que pueda admitirlo esté vinculado con él.
- Para establecer el nivel de API mínimo admitido, agrega el siguiente elemento
<meta-data>
en el archivoAndroidManfiest.xml
del módulo:common:car-app-service
:
AndroidManifest.xml (:common:car-app-service)
<application>
<meta-data
android:name="androidx.car.app.minCarApiLevel"
android:value="1" />
<service android:name="com.example.places.carappservice.PlacesCarAppService" ...>
...
</service>
</application>
- Vuelve a instalar la app y ábrela en la DHU. Luego, deberías ver lo siguiente:
Para que veas todas las alternativas, prueba a definir un valor mayor (p. ej., 100) en minCarApiLevel
para averiguar qué ocurre cuando intentas iniciar la app si el host y el cliente no son compatibles (pista: la app fallará, como cuando no se define ningún valor).
También es importante tener en cuenta que, como con Android, puedes usar funciones de una API mayor que el nivel mínimo declarado si verificas durante el tiempo de ejecución que el host admite el nivel requerido.
Opcional: Escucha cambios de proyección
- Si agregaste el objeto de escucha
CarConnection
en el paso anterior, deberías ver la actualización de estado en tu teléfono cuando la DHU se esté ejecutando, como se muestra a continuación:
8. Agrega compatibilidad con el SO Android Automotive
Con Android Auto ya en funcionamiento, es hora de ir más allá y ofrecer compatibilidad también con el SO Android Automotive.
Crea el módulo :automotive
- Para crear un módulo que contenga el código específico de la versión del SO Android Automotive de la app, abre File > New > New Module… en Android Studio, selecciona la opción Automotive de la lista de tipos de plantilla de la izquierda y, luego, usa los siguientes valores:
- Application/Library name:
Places
(el mismo que el de la app principal, pero también puedes elegir un nombre distinto si lo deseas) - Module name:
automotive
- Package name:
com.example.places.automotive
- Language:
Kotlin
- Minimum SDK:
API 29: Android 10.0 (Q)
(como se mencionó antes durante la creación del módulo:common:car-app-service
, todos los vehículos con el SO Android Automotive que admiten aplicaciones de la biblioteca de apps para vehículos ejecutan, por lo menos, el nivel de API 29)
- Haz clic en Next; selecciona No Activity en la siguiente pantalla; y, por último, haz clic en Finish.
Agrega dependencias
Al igual que con Android Auto, necesitas declarar una dependencia en el módulo :common:car-app-service
. Cuando lo hagas, podrás compartir tu implementación en ambas plataformas.
Además, debes agregar una dependencia en el artefacto androidx.car.app:app-automotive
. A diferencia del artefacto androidx.car.app:app-projected
, que es opcional para Android Auto, esta dependencia es obligatoria en el SO Android Automotive, ya que incluye la CarAppActivity
que se usa para ejecutar tu app.
- Para agregar dependencias, abre el archivo
build.gradle
y, luego, inserta el siguiente código:
build.gradle (módulo :automotive)
dependencies {
...
implementation project(':common:car-app-service')
implementation "androidx.car.app:app-automotive:$car_app_library_version"
...
}
Con este cambio, el gráfico de dependencias para los módulos propios de la app es el siguiente:
Configura el manifiesto
- Primero, debes declarar dos funciones,
android.hardware.type.automotive
yandroid.software.car.templates_host
, como obligatorias.
android.hardware.type.automotive
es una función del sistema que indica que el dispositivo en sí es un vehículo (consulta FEATURE_AUTOMOTIVE
para obtener más detalles). Solo las apps que marcan esta función como obligatoria se pueden enviar a un segmento de Automotive OS en Play Console (y las apps que se envían a otros segmentos no pueden requerir esta función). android.software.car.templates_host
es una función del sistema que solo está presente en vehículos que tienen el host de plantilla obligatorio para ejecutar apps de plantilla.
AndroidManifest.xml (:automotive)
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-feature
android:name="android.hardware.type.automotive"
android:required="true" />
<uses-feature
android:name="android.software.car.templates_host"
android:required="true" />
...
</manifest>
- A continuación, debes declarar que algunas funciones no son obligatorias.
Esto es para garantizar que tu app sea compatible con la gama de hardware disponible en automóviles con Google integrado. Por ejemplo, si tu app requiere la función android.hardware.screen.portrait
, no es compatible con vehículos con pantallas de orientación horizontal, ya que la orientación es fija en la mayoría de los autos. Esta es la razón por la cual el atributo android:required
está configurado en false
para esas funciones.
AndroidManifest.xml (:automotive)
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
...
<uses-feature
android:name="android.hardware.wifi"
android:required="false" />
<uses-feature
android:name="android.hardware.screen.portrait"
android:required="false" />
<uses-feature
android:name="android.hardware.screen.landscape"
android:required="false" />
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
...
</manifest>
- A continuación, debes agregar una referencia al archivo
automotive_app_desc.xml
, tal como lo hiciste para Android Auto.
Ten en cuenta que esta vez el atributo android:name
es distinto: en lugar de com.google.android.gms.car.application
, es com.android.automotive
. Como antes, esto hace referencia al archivo automotive_app_desc.xml
del módulo :common:car-app-service
, lo cual significa que se usa el mismo recurso en Android Auto y el SO Android Automotive. Ten en cuenta que el elemento <meta-data>
está dentro del elemento <application>
(por lo que debes cambiar la etiqueta application
para que no se cierre por sí misma).
AndroidManifest.xml (:automotive)
<application>
...
<meta-data android:name="com.android.automotive"
android:resource="@xml/automotive_app_desc"/>
...
</application>
- Por último, debes agregar un elemento
<activity>
para laCarAppActivity
que se incluye en la biblioteca.
AndroidManifest.xml (:automotive)
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
...
<application ...>
...
<activity
android:name="androidx.car.app.activity.CarAppActivity"
android:exported="true"
android:launchMode="singleTask"
android:theme="@android:style/Theme.DeviceDefault.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="distractionOptimized"
android:value="true" />
</activity>
</application>
</manifest>
Este es el resultado de todo lo anterior:
android:name
indica el nombre de clase completamente calificado de la claseCarAppActivity
del paqueteapp-automotive
.android:exported
se establece entrue
, ya que una app que no sea esta misma (el selector) debe poder iniciar estaActivity
.android:launchMode
se establece ensingleTask
para que solo pueda haber una instancia deCarAppActivity
a la vez.android:theme
se establece en@android:style/Theme.DeviceDefault.NoActionBar
para que la app ocupe todo el espacio de la pantalla que tiene disponible.- El filtro de intents indica que esa es la
Activity
del selector para la app. - Hay un elemento
<meta-data>
que le indica al sistema que la app se puede usar mientras hay restricciones de UX vigentes, por ejemplo, cuando el vehículo está en movimiento.
Opcional: Copia los íconos de selector desde el módulo :app
Como creaste el módulo :automotive
, tiene los íconos del logotipo verde de Android predeterminado.
- Si lo deseas, copia y pega el directorio de recursos
mipmap
del módulo:app
en el módulo:automotive
para usar los mismos íconos de selector que la app para dispositivos móviles.
9. Haz pruebas con 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 una o más de las siguientes imágenes de emulador. Las imágenes solo se pueden ejecutar en máquinas que tienen su misma arquitectura (x86/ARM).
- Android 12L > Imagen del sistema Automotive con Play Store Intel x86 Atom_64
- Android 12L > Imagen del sistema Automotive con Play Store ARM 64 v8a
- Android 11 > Imagen del sistema Automotive con Play Store Intel x86 Atom_64
- Android 10 > Imagen del sistema Automotive con Play Store Intel x86 Atom_64
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 la definición de dispositivo Automotive (1024p landscape) de la lista y haz clic en Next.
- En la siguiente página, selecciona una imagen del sistema del paso anterior (si elegiste la imagen Android 11/API 30, puede encontrarse en la pestaña x86 Images y no en la pestaña predeterminada Recommended). 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
automotive
.
Cuando ejecutes la app por primera vez, es posible que veas una pantalla como la siguiente:
Si ese es el caso, haz clic en el botón Check for updates, que te llevará a la página de Play Store correspondiente a la app de Google Automotive App Host, donde deberías hacer clic en el botón Instalar. Si no habías accedido a tu cuenta cuando hagas clic en el botón Check for updates, se te dirigirá por el flujo de acceso. Una vez que hayas accedido, podrás volver a abrir la app para hacer clic en el botón y regresar a la página de Play Store.
- Por último, con el host instalado, vuelve a abrir la app desde el selector (el ícono de cuadrícula de nueve puntos ubicado en la fila inferior). Deberías ver lo siguiente:
En el siguiente paso, harás cambios en el módulo :common:car-app-service
para mostrar la lista de lugares y permitir que el usuario inicie la navegación a una ubicación elegida en otra app.
10. Agrega un mapa y una pantalla de detalles
Agrega un mapa a la pantalla principal
- Para comenzar, reemplaza el código en el método
onGetTemplate
de la claseMainScreen
por lo siguiente:
MainScreen.kt
override fun onGetTemplate(): Template {
val placesRepository = PlacesRepository()
val itemListBuilder = ItemList.Builder()
.setNoItemsMessage("No places to show")
placesRepository.getPlaces()
.forEach {
itemListBuilder.addItem(
Row.Builder()
.setTitle(it.name)
// Each item in the list *must* have a DistanceSpan applied to either the title
// or one of the its lines of text (to help drivers make decisions)
.addText(SpannableString(" ").apply {
setSpan(
DistanceSpan.create(
Distance.create(Math.random() * 100, Distance.UNIT_KILOMETERS)
), 0, 1, Spannable.SPAN_INCLUSIVE_INCLUSIVE
)
})
.setOnClickListener { TODO() }
// Setting Metadata is optional, but is required to automatically show the
// item's location on the provided map
.setMetadata(
Metadata.Builder()
.setPlace(Place.Builder(CarLocation.create(it.latitude, it.longitude))
// Using the default PlaceMarker indicates that the host should
// decide how to style the pins it shows on the map/in the list
.setMarker(PlaceMarker.Builder().build())
.build())
.build()
).build()
)
}
return PlaceListMapTemplate.Builder()
.setTitle("Places")
.setItemList(itemListBuilder.build())
.build()
}
Este código lee la lista de instancias de Place
del PlacesRepository
y las convierte en una Row
para que se agreguen a la ItemList
que muestra la PlaceListMapTemplate
.
- Vuelve a ejecutar la app (en una de las dos plataformas o en ambas) para ver el resultado.
Android Auto | SO Android Automotive |
Lamentablemente, se produjo otro error. Parece que falta un permiso.
java.lang.SecurityException: The car app does not have a required permission: androidx.car.app.MAP_TEMPLATES at android.os.Parcel.createExceptionOrNull(Parcel.java:2373) at android.os.Parcel.createException(Parcel.java:2357) at android.os.Parcel.readException(Parcel.java:2340) at android.os.Parcel.readException(Parcel.java:2282) ...
- Para corregir este error, agrega el siguiente elemento
<uses-permission>
en el manifiesto del módulo:common:car-app-service
.
Cualquier app que use la PlaceListMapTemplate
debe declarar este permiso; de lo contrario, la app fallará como se demostró. Ten en cuenta que solo las apps que declaran su categoría como androidx.car.app.category.POI
pueden usar esta plantilla y, a su vez, este permiso.
AndroidManifest.xml (:common:car-app-service)
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="androidx.car.app.MAP_TEMPLATES" />
...
</manifest>
Si ejecutas la app después de agregar el permiso, debería verse de la siguiente manera en cada plataforma:
Android Auto | SO Android Automotive |
El host de la aplicación renderiza el mapa por ti cuando proporcionas los Metadata
necesarios.
Agrega una pantalla de detalles
A continuación, es hora de agregar una pantalla de detalles para permitir a los usuarios ver más información sobre una ubicación específica y tener la opción de navegar a esa ubicación usando su app de navegación preferida o regresar a la lista de otros lugares. Esto se puede hacer con PaneTemplate
, que te permite mostrar hasta cuatro filas de información junto a los botones de acciones opcionales.
- Primero, haz clic con el botón derecho en el directorio
res
del módulo:common:car-app-service
y haz clic en New > Vector Asset. Luego, crea un ícono de navegación usando esta configuración:
- Asset type:
Clip art
- Clip art:
navigation
- Name:
baseline_navigation_24
- Size:
24
dp ×24
dp - Color:
#000000
- Opacity:
100%
- Luego, en el paquete
screen
, crea un archivo llamadoDetailScreen.kt
(junto al archivoMainScreen.kt
existente) y agrega el siguiente código:
DetailScreen.kt
class DetailScreen(carContext: CarContext, private val placeId: Int) : Screen(carContext) {
override fun onGetTemplate(): Template {
val place = PlacesRepository().getPlace(placeId)
?: return MessageTemplate.Builder("Place not found")
.setHeaderAction(Action.BACK)
.build()
val navigateAction = Action.Builder()
.setTitle("Navigate")
.setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.baseline_navigation_24
)
).build()
)
// Only certain intent actions are supported by `startCarApp`. Check its documentation
// for all of the details. To open another app that can handle navigating to a location
// you must use the CarContext.ACTION_NAVIGATE action and not Intent.ACTION_VIEW like
// you might on a phone.
.setOnClickListener { carContext.startCarApp(place.toIntent(CarContext.ACTION_NAVIGATE)) }
.build()
return PaneTemplate.Builder(
Pane.Builder()
.addAction(navigateAction)
.addRow(
Row.Builder()
.setTitle("Coordinates")
.addText("${place.latitude}, ${place.longitude}")
.build()
).addRow(
Row.Builder()
.setTitle("Description")
.addText(place.description)
.build()
).build()
)
.setTitle(place.name)
.setHeaderAction(Action.BACK)
.build()
}
}
Presta especial atención a cómo se compila navigateAction
; la llamada a startCarApp
en su OnClickListener
es la clave para interactuar con otras apps en Android Auto y el SO Android Automotive.
Navega entre pantallas
Ahora que hay dos tipos de pantallas, es hora de agregar navegación entre ellas. La navegación en la biblioteca de apps para vehículos usa un modelo de pila de enviar y resaltar ideal para los flujos de tareas sencillos y adecuados para realizar mientras los usuarios conducen.
- Para navegar desde uno de los elementos de la lista en
MainScreen
a unaDetailScreen
para ese elemento, agrega el siguiente código:
MainScreen.kt
Row.Builder()
...
.setOnClickListener { screenManager.push(DetailScreen(carContext, it.id)) }
...
La navegación de vuelta desde DetailScreen
hasta MainScreen
ya se controló, puesto que se llama a setHeaderAction(Action.BACK)
cuando se compila la PaneTemplate
que se muestra en la DetailScreen
. Cuando un usuario hace clic en la acción del encabezado, el host controla la eliminación de la pantalla actual de la pila, pero tu app puede anular este comportamiento si lo deseas.
- Ahora, ejecuta la app para ver la
DetailScreen
y la navegación en la app en acción.
11. Actualiza el contenido en una pantalla
A menudo, deseas permitir que un usuario interactúe con una pantalla y cambie el estado de los elementos en esa pantalla. Para demostrar cómo hacerlo, desarrollas una funcionalidad que permita a los usuarios alternar entre agregar un lugar a favoritos en la DetailScreen
o quitarlo de allí.
- Primero, agrega una variable local,
isFavorite
, que contenga el estado. En una app real, esto debería almacenarse como parte de la capa de datos, pero una variable local es suficiente a modo de demostración.
DetailScreen.kt
class DetailScreen(carContext: CarContext, private val placeId: Int) : Screen(carContext) {
private var isFavorite = false
...
}
- A continuación, haz clic con el botón derecho en el directorio
res
del módulo:common:car-app-service
y haz clic en New > Vector Asset. Luego, crea un ícono de favoritos usando esta configuración:
- Asset type:
Clip art
- Name:
baseline_favorite_24
- Clip art:
favorite
- Size:
24
dp ×24
dp - Color:
#000000
- Opacity:
100%
- Luego, en
DetailsScreen.kt
, crea unActionStrip
para laPaneTemplate
.
Los componentes de IU de ActionStrip
se colocan en la fila de encabezado opuesta al título y son ideales para las acciones secundarias y terciarias. Dado que la navegación es la acción principal para llevar a cabo en la DetailScreen
, colocar la Action
para agregar a favoritos o quitar de esa sección en una ActionStrip
es una excelente forma de estructurar la pantalla.
DetailScreen.kt
val navigateAction = ...
val actionStrip = ActionStrip.Builder()
.addAction(
Action.Builder()
.setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.baseline_favorite_24
)
).setTint(
if (isFavorite) CarColor.RED else CarColor.createCustom(
Color.LTGRAY,
Color.DKGRAY
)
).build()
)
.setOnClickListener {
isFavorite = !isFavorite
}.build()
)
.build()
...
Aquí hay dos aspectos interesantes:
- Se ajusta el tono del
CarIcon
en función del estado del elemento. - Se usa
setOnClickListener
para reaccionar a entradas del usuario y activar o desactivar el estado favorito.
- No olvides llamar a
setActionStrip
enPaneTemplate.Builder
para usarlo.
DetailScreen.kt
return PaneTemplate.Builder(...)
...
.setActionStrip(actionStrip)
.build()
- Ahora, ejecuta la app y descubre qué sucede:
Interesante… Los clics ocurren, pero la IU no se actualiza.
Esto se debe a que la biblioteca de apps para vehículos tiene un concepto de actualizaciones. Para limitar la distracción del conductor, la actualización del contenido en la pantalla tiene ciertas limitaciones (que varían según la plantilla que se muestra), y tu propio código debe solicitar explícitamente cada actualización llamando al método invalidate
de la clase Screen
. Solo actualizar algún estado al que se hace referencia en la onGetTemplate
no basta para actualizar la IU.
- Para corregir este problema, actualiza el
OnClickListener
de la siguiente manera:
DetailScreen.kt
.setOnClickListener {
isFavorite = !isFavorite
// Request that `onGetTemplate` be called again so that updates to the
// screen's state can be picked up
invalidate()
}
- Vuelve a ejecutar la app para ver que el color del ícono de corazón debería actualizarse con cada clic.
Y, de esta forma, tienes una app básica que se integra bien con Android Auto y el SO Android Automotive.
12. Felicitaciones
Compilaste correctamente tu primera app de la biblioteca de apps para vehículos. Ahora es momento de aplicar lo que aprendiste en tu propia app.
Como se mencionó antes, por el momento, solo ciertas categorías compiladas con las apps de la biblioteca de apps para vehículos se pueden enviar a Play Store. Si tu app es de navegación, lugar de interés (como la app en la que trabajaste en este codelab) o de Internet de las cosas (IoT), puedes comenzar a compilar hoy y hacer su lanzamiento final hasta producción en ambas plataformas.
Se agregan nuevas categorías de app todos los años, por lo que, incluso si no puedes aplicar de inmediato lo que aprendiste, vuelve a consultar en el futuro para saber si es el momento justo para extender tu app a los vehículos.
Pruebas para hacer
- Instala el emulador de un OEM (p. ej., el emulador Polestar 2) y observa cómo la personalización de OEM puede cambiar el aspecto de las aplicaciones de la biblioteca de apps para vehículos en el SO Android Automotive. Ten en cuenta que no todos los emuladores de OEM admiten las aplicaciones de la biblioteca de apps para vehículos.
- Consulta la aplicación de Showcase de ejemplo que demuestra la funcionalidad completa de la biblioteca de apps para vehículos.
Lecturas adicionales
- En Cómo usar la biblioteca de apps de Android para vehículos, se abarca el contenido de este codelab y mucho más.
- Los lineamientos de diseño de la biblioteca de apps de Android para vehículos brindan una descripción detallada de todas las diferentes plantillas y las prácticas recomendadas para seguir cuando crees tu app.
- 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.