Conceptos básicos sobre la biblioteca de apps para vehículos

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

Requisitos

Qué compilarás

Android Auto

SO Android Automotive

Una grabación de pantalla que muestra la app en ejecución en Android Auto usando la consola central de computadora

Una grabación de pantalla que muestra la app en ejecución en un emulador del 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 y Screen
  • 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

  1. El código para este codelab se puede encontrar en el directorio car-app-library-fundamentals 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 car-app-library-fundamentals/start. El directorio car-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 depende del módulo :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.

Diagrama de cómo funciona la biblioteca de apps para vehículos. A la izquierda, hay dos cuadros titulados Display. En el centro, hay un cuadro titulado Host. A la derecha, hay un cuadro titulado CarAppService. Dentro del cuadro CarAppService, hay dos cuadros titulados Session. Dentro del primer cuadro Session, hay tres cuadros titulados Screen (uno encima del otro). Dentro del segundo cuadro Session, hay dos cuadros titulados Screen (uno encima del otro). Hay flechas entre cada uno de los cuadros Display y el host, además de entre el host y los cuadros Sessions, para indicar cómo el host administra la comunicación entre todos los componentes distintos.

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

  1. Con el módulo :common seleccionado en la ventana Project, haz clic con el botón derecho y selecciona la opción New > Module.
  2. 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)

Asistente Create New Module con los valores configurados como se describe en este paso

Configura dependencias

  1. 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'
        ...
    }
}
  1. 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 y androidx.car.app:app-testing para algunos asistentes útiles a la hora de hacer pruebas de unidades. Utilizarás app-projected y app-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:

Los módulos :app y :common:car-app-service dependen del módulo :common:data.

Ahora que se configuraron las dependencias, es hora de escribir el CarAppService.

5. Escribe tu CarAppService

  1. Para empezar, crea un archivo llamado PlacesCarAppService.kt en el paquete carappservice dentro del módulo :common:car-app-service.
  2. Dentro de este archivo, crea una clase llamada PlacesCarAppService, que extiende CarAppService.

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.

  1. Por último, debes agregar el elemento <service> que corresponde al PlacesCarAppService en el archivo AndroidManifest.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 son androidx.car.app.category.NAVIGATION y androidx.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.

  1. 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 subclases Screen de la app.
  2. En el paquete screen, crea un archivo llamado MainScreen.kt para contener la clase MainScreen, que extiende Screen. Por ahora, muestra el mensaje simple Hello, world! usando PaneTemplate.

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:

Los módulos :app y :common:car-app-service dependen del módulo :common:data. El módulo :app también depende del módulo :common:car-app-service.

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

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

Asistente New Resource File con los valores configurados como se describe en este paso

  1. 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>
  1. En el archivo AndroidManifest.xml del módulo :app, agrega el siguiente elemento <meta-data> que hace referencia al archivo automotive_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.

  1. Para usar la API de CarConnection, primero agrega una dependencia en el módulo :app en el artefacto androidx.car.app:app.

build.gradle (módulo :app)

dependencies {
    ...
    implementation "androidx.car.app:app:$car_app_library_version"
    ...
}
  1. 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
    )
}
  1. 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())
            }
        }
    }
}
  1. Si ejecutas la app, debería decir Not projecting.

Ahora hay una línea de texto adicional en la pantalla sobre el estado de proyección con la frase &quot;Not projecting&quot;

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.

  1. 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).

  1. Abre la app desde el selector

El selector de Android Auto muestra la cuadrícula de apps, incluida la app de Places.

Lamentablemente, falló.

Aparece una pantalla de error con el mensaje &quot;Android Auto has encountered an unexpected error&quot;. Hay un botón de activación de depuración en la esquina superior derecha de la pantalla.

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

La misma pantalla de error que la figura anterior, pero ahora con el botón de activación de depuración habilitado. Se muestra un seguimiento de pila en la pantalla.

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.

  1. Para establecer el nivel de API mínimo admitido, agrega el siguiente elemento <meta-data> en el archivo AndroidManfiest.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>
  1. Vuelve a instalar la app y ábrela en la DHU. Luego, deberías ver lo siguiente:

La app muestra una pantalla básica &quot;Hello, World&quot;

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:

La línea de texto que muestra el estado de la proyección ahora dice &quot;Projecting&quot;, ya que el teléfono está conectado a la DHU.

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

  1. 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)

El asistente de Create New Module para el módulo del SO Android Automotive que muestra los valores que se indican en este paso

  1. Haz clic en Next; selecciona No Activity en la siguiente pantalla; y, por último, haz clic en Finish.

La segunda pantalla del asistente de Create New Module. Se muestran tres opciones: &quot;No Activity&quot;, &quot;Media Service&quot; y &quot;Messaging Service&quot;. Está seleccionada la opción &quot;No Activity&quot;.

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.

  1. 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:

Los módulos :app y :common:car-app-service dependen del módulo :common:data. Los módulos :app y :automotive dependen del módulo :common:car-app-service.

Configura el manifiesto

  1. Primero, debes declarar dos funciones, android.hardware.type.automotive y android.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>
  1. 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>
  1. 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>
  1. Por último, debes agregar un elemento <activity> para la CarAppActivity 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 clase CarAppActivity del paquete app-automotive.
  • android:exported se establece en true, ya que una app que no sea esta misma (el selector) debe poder iniciar esta Activity.
  • android:launchMode se establece en singleTask para que solo pueda haber una instancia de CarAppActivity 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

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

  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 la definición de dispositivo Automotive (1024p landscape) de la lista y haz clic en Next.

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

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

  1. Ejecuta la app en el emulador que acabas de crear usando la configuración de ejecución automotive.

Las

Cuando ejecutes la app por primera vez, es posible que veas una pantalla como la siguiente:

La app muestra una pantalla con el mensaje &quot;System update required&quot; con un botón que dice &quot;Check for updates&quot; debajo.

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.

La página de Play Store de Google Automotive App Host; hay un botón &quot;Instalar&quot; en la esquina superior derecha.

  1. 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:

La app muestra una pantalla básica &quot;Hello, World&quot;

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

  1. Para comenzar, reemplaza el código en el método onGetTemplate de la clase MainScreen 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.

  1. Vuelve a ejecutar la app (en una de las dos plataformas o en ambas) para ver el resultado.

Android Auto

SO Android Automotive

Se muestra otro seguimiento de pila debido a un error.

La app solo falla y el usuario regresa al selector después de abrirla.

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)
        ...
  1. 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

Se muestra una lista de ubicaciones en el lado izquierdo de la pantalla y, detrás de ella, se muestra un mapa con indicadores correspondientes a las ubicaciones que llena el resto de la pantalla.

Se muestra una lista de ubicaciones en el lado izquierdo de la pantalla y, detrás de ella, se muestra un mapa con indicadores correspondientes a las ubicaciones que llena el resto de la pantalla.

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.

  1. 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%

El asistente de Asset Studio muestra las entradas mencionadas en este paso

  1. Luego, en el paquete screen, crea un archivo llamado DetailScreen.kt (junto al archivo MainScreen.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.

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.

Un diagrama representa cómo funciona la navegación en la app con la biblioteca de apps para vehículos. A la izquierda, hay una pila con solo una MainScreen. Entre ella y la pila central, hay una flecha con la etiqueta &quot;Push DetailScreen&quot;. La pila central tiene una DetailScreen encima de la MainScreen existente. Entre la pila central y la pila derecha, hay una flecha con la etiqueta &quot;Pop&quot;. La pila derecha es igual que la izquierda: solo una MainScreen.

  1. Para navegar desde uno de los elementos de la lista en MainScreen a una DetailScreen 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.

  1. 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í.

  1. 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
    ...
}
  1. 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%

El asistente de Asset Studio muestra las entradas mencionadas en este paso

  1. Luego, en DetailsScreen.kt, crea un ActionStrip para la PaneTemplate.

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.
  1. No olvides llamar a setActionStrip en PaneTemplate.Builder para usarlo.

DetailScreen.kt

return PaneTemplate.Builder(...)
    ...
    .setActionStrip(actionStrip)
    .build()
  1. Ahora, ejecuta la app y descubre qué sucede:

Se muestra DetailScreen. El usuario presiona el ícono de favoritos, pero este no cambia de color como se esperaba.

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.

  1. 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()
}
  1. Vuelve a ejecutar la app para ver que el color del ícono de corazón debería actualizarse con cada clic.

Se muestra DetailScreen. El usuario presiona el ícono de favoritos y, ahora, este cambia de color como se esperaba.

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

Documentos de referencia