Cómo compilar una app de navegación

En esta página, se detallan las diferentes funciones de la biblioteca de apps para vehículos que puedes usar para implementar la funcionalidad de tu app de navegación paso a paso.

Declara la compatibilidad con la navegación en tu manifiesto

Tu app de navegación debe declarar la categoría de app para vehículos androidx.car.app.category.NAVIGATION en el filtro de intents de su CarAppService:

<application>
    ...
   <service
       ...
        android:name=".MyNavigationCarAppService"
        android:exported="true">
      <intent-filter>
        <action android:name="androidx.car.app.CarAppService" />
        <category android:name="androidx.car.app.category.NAVIGATION"/>
      </intent-filter>
    </service>
    ...
</application>

Admite intents de navegación

Para admitir intents de navegación en tu app, incluidos aquellos que provienen de Asistente de Google cuando se realiza una consulta por voz, tu app debe controlar el intent CarContext.ACTION_NAVIGATE en su Session.onCreateScreen y Session.onNewIntent.

Si deseas obtener detalles sobre el formato del intent, consulta la documentación de CarContext.startCarApp.

Accede a las plantillas de navegación

Las apps de navegación pueden acceder a las siguientes plantillas, que muestran una plataforma en segundo plano con el mapa y, durante la navegación activa, instrucciones paso a paso.

  • NavigationTemplate: También muestra un mensaje informativo opcional y estimaciones de viaje durante la navegación activa.
  • MapWithContentTemplate: Una plantilla que permite que una app renderice tarjetas de mapa con algún tipo de contenido (por ejemplo, una lista). Por lo general, el contenido se renderiza como una superposición sobre las tarjetas del mapa, con las áreas visibles y estables del mapa que se ajustan al contenido.

Si quieres obtener más información para diseñar la interfaz de usuario de tu app de navegación con estas plantillas, consulta el documento sobre apps de navegación.

Para obtener acceso a las plantillas de navegación, la app debe declarar el permiso androidx.car.app.NAVIGATION_TEMPLATES en su archivo AndroidManifest.xml:

<manifest ...>
  ...
  <uses-permission android:name="androidx.car.app.NAVIGATION_TEMPLATES"/>
  ...
</manifest>

Se requiere un permiso adicional para dibujar mapas.

Cómo migrar a MapWithContentTemplate

A partir del nivel de API 7 de la app para vehículos, MapTemplate, PlaceListNavigationTemplate y RoutePreviewNavigationTemplate dejan de estar disponibles. Se seguirá admitiendo el uso de plantillas obsoletas, pero se recomienda migrar a MapWithContentTemplate.

La funcionalidad que proporcionan estas plantillas se puede implementar con MapWithContentTemplate. Consulta los siguientes fragmentos para ver ejemplos:

MapTemplate

Kotlin

// MapTemplate (deprecated)
val template = MapTemplate.Builder()
    .setPane(paneBuilder.build())
    .setActionStrip(actionStrip)
    .setHeader(header)
    .setMapController(mapController)
    .build()

// MapWithContentTemplate
val template = MapWithContentTemplate.Builder()
    .setContentTemplate(
        PaneTemplate.Builder(paneBuilder.build())
            .setHeader(header)
            .build())
    .setActionStrip(actionStrip)
    .setMapController(mapController)
    .build()

Java

// MapTemplate (deprecated)
MapTemplate template = new MapTemplate.Builder()
    .setPane(paneBuilder.build())
    .setActionStrip(actionStrip)
    .setHeader(header)
    .setMapController(mapController)
    .build();

// MapWithContentTemplate
MapWithContentTemplate template = new MapWithContentTemplate.Builder()
    .setContentTemplate(new PaneTemplate.Builder(paneBuilder.build())
        .setHeader(header)
        build())
    .setActionStrip(actionStrip)
    .setMapController(mapController)
    .build();

PlaceListNavigationTemplate

Kotlin

// PlaceListNavigationTemplate (deprecated)
val template = PlaceListNavigationTemplate.Builder()
    .setItemList(itemListBuilder.build())
    .setHeader(header)
    .setActionStrip(actionStrip)
    .setMapActionStrip(mapActionStrip)
    .build()

// MapWithContentTemplate
val template = MapWithContentTemplate.Builder()
    .setContentTemplate(
        ListTemplate.Builder()
            .setSingleList(itemListBuilder.build())
            .setHeader(header)
            .build())
    .setActionStrip(actionStrip)
    .setMapController(
        MapController.Builder()
            .setMapActionStrip(mapActionStrip)
            .build())
    .build()

Java

// PlaceListNavigationTemplate (deprecated)
PlaceListNavigationTemplate template = new PlaceListNavigationTemplate.Builder()
    .setItemList(itemListBuilder.build())
    .setHeader(header)
    .setActionStrip(actionStrip)
    .setMapActionStrip(mapActionStrip)
    .build();

// MapWithContentTemplate
MapWithContentTemplate template = new MapWithContentTemplate.Builder()
    .setContentTemplate(new ListTemplate.Builder()
        .setSingleList(itemListBuilder.build())
        .setHeader(header)
        .build())
    .setActionStrip(actionStrip)
    .setMapController(new MapController.Builder()
        .setMapActionStrip(mapActionStrip)
        .build())
    .build();

RoutePreviewNavigationTemplate

Kotlin

// RoutePreviewNavigationTemplate (deprecated)
val template = RoutePreviewNavigationTemplate.Builder()
    .setItemList(
        ItemList.Builder()
            .addItem(
                Row.Builder()
                    .setTitle(title)
                    .build())
            .build())
    .setHeader(header)
    .setNavigateAction(
        Action.Builder()
            .setTitle(actionTitle)
            .setOnClickListener { ... }
            .build())
    .setActionStrip(actionStrip)
    .setMapActionStrip(mapActionStrip)
    .build()

// MapWithContentTemplate
val template = MapWithContentTemplate.Builder()
    .setContentTemplate(
        ListTemplate.Builder()
            .setSingleList(
                ItemList.Builder()
                    .addItem(
                        Row.Builder()
                            .setTitle(title)
                            .addAction(
                                Action.Builder()
                                    .setTitle(actionTitle)
                                    .setOnClickListener { ... }
                                    .build())
                            .build())
                    .build())
            .setHeader(header)
            .build())
    .setActionStrip(actionStrip)
    .setMapController(
        MapController.Builder()
            .setMapActionStrip(mapActionStrip)
            .build())
    .build()

Java

// RoutePreviewNavigationTemplate (deprecated)
RoutePreviewNavigationTemplate template = new RoutePreviewNavigationTemplate.Builder()
    .setItemList(new ItemList.Builder()
        .addItem(new Row.Builder()
            .setTitle(title))
            .build())
        .build())
    .setHeader(header)
    .setNavigateAction(new Action.Builder()
        .setTitle(actionTitle)
        .setOnClickListener(() -> { ... })
        .build())
    .setActionStrip(actionStrip)
    .setMapActionStrip(mapActionStrip)
    .build();

// MapWithContentTemplate
MapWithContentTemplate template = new MapWithContentTemplate.Builder()
    .setContentTemplate(new ListTemplate.Builder()
        .setSingleList(new ItemList.Builder()
            .addItem(new Row.Builder()
                  .setTitle(title))
                  .addAction(new Action.Builder()
                      .setTitle(actionTitle)
                      .setOnClickListener(() -> { ... })
                      .build())
                  .build())
            .build()))
        .setHeader(header)
        .build())
    .setActionStrip(actionStrip)
    .setMapController(new MapController.Builder()
        .setMapActionStrip(mapActionStrip)
        .build())
    .build();

Las apps de navegación deben comunicar metadatos de navegación adicionales al host. El host usa esa información con el fin de proporcionarla a la consola central del vehículo y evitar conflictos de las aplicaciones de navegación en relación con los recursos compartidos.

Los metadatos de navegación se proporcionan en el servicio para vehículos de NavigationManager, al que se puede acceder desde el CarContext:

Kotlin

val navigationManager = carContext.getCarService(NavigationManager::class.java)

Java

NavigationManager navigationManager = carContext.getCarService(NavigationManager.class);

Inicia, finaliza y detén la navegación

Para que el host administre varias apps de navegación, notificaciones de rutas y datos del clúster del vehículo, debe conocer el estado actual de la navegación. Cuando un usuario inicia la navegación, la app debe llamar a NavigationManager.navigationStarted. Del mismo modo, cuando finaliza la navegación (por ejemplo, cuando el usuario llega a su destino o cancela la navegación), la app debe llamar a NavigationManager.navigationEnded.

Solo llama a NavigationManager.navigationEnded cuando el usuario termine de usar la navegación. En cambio, si necesitas volver a calcular la ruta en medio de un viaje, usa Trip.Builder.setLoading(true).

En ocasiones, el host necesitará una app para detener la navegación y llamará a onStopNavigation en un objeto NavigationManagerCallback proporcionado por tu app a través de NavigationManager.setNavigationManagerCallback. En este caso, la app deberá dejar de emitir la información de próximo paso en la pantalla del clúster, las notificaciones de navegación y las indicaciones por voz.

Actualiza la información del viaje

Durante la navegación activa, llama a NavigationManager.updateTrip. La información que se brinde en esta llamada se usará en las pantallas del clúster y de aviso del vehículo. Según el vehículo que se conduzca, no se muestra toda la información al usuario. Por ejemplo, la consola central de computadora (DHU) muestra el Step agregado a Trip, pero no muestra la información de Destination.

Dibuja en la pantalla del clúster

Para proporcionar una experiencia del usuario lo más envolvente posible, te recomendamos ir más allá de mostrar los metadatos básicos en la pantalla del clúster del vehículo. A partir del nivel de API 6 de la app para vehículos, las apps de navegación tienen la opción de procesar su propio contenido directamente en la pantalla del clúster (en vehículos compatibles), con las siguientes limitaciones:

  • La API de la pantalla del clúster no es compatible con los controles de entrada.
  • Lineamiento de calidad de las apps para vehículos NF-9: La pantalla del clúster solo debe mostrar mosaicos de mapas. De manera opcional, se puede mostrar una ruta de navegación activa en estos mosaicos.
  • La API de la pantalla del clúster solo admite el uso de NavigationTemplate.
    • A diferencia de las pantallas principales, es posible que las pantallas del clúster no muestren de manera coherente todos los elementos de la IU de NavigationTemplate, como las instrucciones paso a paso, las tarjetas de hora de llegada y las acciones. Las tarjetas de mapas son el único elemento de la IU que se muestra de manera coherente.

Declara la compatibilidad del clúster

Para permitir que la aplicación de host sepa que tu app admite la renderización en pantallas de clústeres, debes agregar un elemento <category> de androidx.car.app.category.FEATURE_CLUSTER a tu <intent-filter> de CarAppService, como se muestra en el siguiente fragmento:

<application>
    ...
   <service
       ...
        android:name=".MyNavigationCarAppService"
        android:exported="true">
      <intent-filter>
        <action android:name="androidx.car.app.CarAppService" />
        <category android:name="androidx.car.app.category.NAVIGATION"/>
        <category android:name="androidx.car.app.category.FEATURE_CLUSTER"/>
      </intent-filter>
    </service>
    ...
</application>

Administración del ciclo de vida y el estado

A partir del nivel de API 6, el flujo del ciclo de vida de la app para vehículos se mantiene igual, pero ahora CarAppService::onCreateSession toma un parámetro de tipo SessionInfo que brinda información adicional sobre la Session que se crea (como el tipo de pantalla y el conjunto de plantillas compatibles).

Las apps tienen la opción de usar la misma clase de Session para controlar el clúster y la pantalla principal, o crear Sessions específicas de la pantalla para personalizar el comportamiento en cada pantalla (como se muestra en el siguiente fragmento).

Kotlin

override fun onCreateSession(sessionInfo: SessionInfo): Session {
  return if (sessionInfo.displayType == SessionInfo.DISPLAY_TYPE_CLUSTER) {
    ClusterSession()
  } else {
    MainDisplaySession()
  }
}

Java

@Override
@NonNull
public Session onCreateSession(@NonNull SessionInfo sessionInfo) {
  if (sessionInfo.getDisplayType() == SessionInfo.DISPLAY_TYPE_CLUSTER) {
    return new ClusterSession();
  } else {
    return new MainDisplaySession();
  }
}

No hay garantías sobre cuándo o si se proporciona la pantalla del clúster, y también es posible que la Session del clúster sea la única Session (por ejemplo, si el usuario cambió la pantalla principal por otra app mientras está navegando activamente por tu app). El acuerdo "estándar" es que la app obtiene el control de la visualización del clúster solo después de que se haya llamado a NavigationManager::navigationStarted. Sin embargo, es posible que la app obtenga la pantalla del clúster mientras no haya una navegación activa, o bien que nunca se le proporcione la pantalla del clúster. Depende de tu app manejar estas situaciones renderizando el estado inactivo de los mosaicos de mapas de tu app.

El host crea instancias independientes de Binder y CarContext por Session. Esto significa que, cuando se usan métodos como ScreenManager::push o Screen::invalidate, solo se ve afectada la Session desde la que se los llama. Las apps deben crear sus propios canales de comunicación entre estas instancias si se necesita la comunicación entre Session (por ejemplo, con transmisiones, un singleton compartido, entre otros).

Prueba la compatibilidad con clústeres

Puedes probar tu implementación en Android Auto y el SO Android Automotive. Para Android Auto, esto se hace configurando la consola central de la computadora para emular una pantalla del clúster secundaria. En el SO Android Automotive, las imágenes genéricas del sistema del nivel de API 30 y versiones posteriores emulan la pantalla de un clúster.

Personaliza TravelEstimate con texto o un ícono

Para personalizar la estimación de viaje con texto, un ícono o ambos, usa los métodos setTripIcon o setTripText de la clase TravelEstimate.Builder. NavigationTemplate usa TravelEstimate para establecer de forma opcional el texto y los íconos junto a la hora estimada de llegada, el tiempo restante y distancia restante, o en lugar de ellos.

Figura 1. Duración estimada del viaje con ícono y texto personalizados

En el siguiente fragmento, se usan setTripIcon y setTripText para personalizar la estimación de viaje:

Kotlin

TravelEstimate.Builder(Distance.create(...), DateTimeWithZone.create(...))
      ...
      .setTripIcon(CarIcon.Builder(...).build())
      .setTripText(CarText.create(...))
      .build()

Java

new TravelEstimate.Builder(Distance.create(...), DateTimeWithZone.create(...))
      ...
      .setTripIcon(CarIcon.Builder(...).build())
      .setTripText(CarText.create(...))
      .build();

Proporciona notificaciones paso a paso

Brinda instrucciones de navegación paso a paso (TBT) a través de una notificación de navegación actualizada con frecuencia. Para que se la considere una notificación de navegación en la pantalla del vehículo, el compilador de la notificación deberá hacer lo siguiente:

  1. Marcar la notificación como en curso con el método NotificationCompat.Builder.setOngoing
  2. Establecer la categoría de la notificación en Notification.CATEGORY_NAVIGATION
  3. Extender la notificación con un CarAppExtender

Se mostrará una notificación de navegación en el widget de riel, en la parte inferior de la pantalla del vehículo. Si el nivel de importancia de la notificación se establece en IMPORTANCE_HIGH, también se mostrará como una notificación de atención (HUN). Si no se establece la importancia con el método CarAppExtender.Builder.setImportance, se usará la importancia del canal de notificaciones.

La app puede establecer un PendingIntent en el CarAppExtender que se le enviará cuando el usuario presione la HUN o el widget de riel.

Si se llama a NotificationCompat.Builder.setOnlyAlertOnce con un valor true, la HUN tendrá una única alerta de notificación de importancia alta.

En el siguiente fragmento, se muestra cómo compilar una notificación de navegación:

Kotlin

NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
    ...
    .setOnlyAlertOnce(true)
    .setOngoing(true)
    .setCategory(NotificationCompat.CATEGORY_NAVIGATION)
    .extend(
        CarAppExtender.Builder()
            .setContentTitle(carScreenTitle)
            ...
            .setContentIntent(
                PendingIntent.getBroadcast(
                    context,
                    ACTION_OPEN_APP.hashCode(),
                    Intent(ACTION_OPEN_APP).setComponent(
                        ComponentName(context, MyNotificationReceiver::class.java)),
                        0))
            .setImportance(NotificationManagerCompat.IMPORTANCE_HIGH)
            .build())
    .build()

Java

new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
    ...
    .setOnlyAlertOnce(true)
    .setOngoing(true)
    .setCategory(NotificationCompat.CATEGORY_NAVIGATION)
    .extend(
        new CarAppExtender.Builder()
            .setContentTitle(carScreenTitle)
            ...
            .setContentIntent(
                PendingIntent.getBroadcast(
                    context,
                    ACTION_OPEN_APP.hashCode(),
                    new Intent(ACTION_OPEN_APP).setComponent(
                        new ComponentName(context, MyNotificationReceiver.class)),
                        0))
            .setImportance(NotificationManagerCompat.IMPORTANCE_HIGH)
            .build())
    .build();

Actualiza la notificación TBT con frecuencia sobre los cambios de distancia, lo que actualizará el widget de riel y solo mostrará la notificación como una HUN. Puedes controlar el comportamiento de la HUN si configuras la importancia de la notificación con CarAppExtender.Builder.setImportance. Cuando se establece la importancia en IMPORTANCE_HIGH, se muestra una HUN. Si se establece en cualquier otro valor, solo se actualiza el widget de riel.

Actualiza el contenido de PlaceListNavigationTemplate

Puedes permitir que los conductores actualicen el contenido con solo presionar un botón mientras exploran listas de lugares compiladas con PlaceListNavigationTemplate. Para habilitar la actualización de la lista, implementa el método onContentRefreshRequested de la interfaz OnContentRefreshListener y usa PlaceListNavigationTemplate.Builder.setOnContentRefreshListener si buscas configurar el objeto de escucha en la plantilla.

En el siguiente fragmento, se muestra cómo configurar el objeto de escucha en la plantilla:

Kotlin

PlaceListNavigationTemplate.Builder()
    ...
    .setOnContentRefreshListener {
        // Execute any desired logic
        ...
        // Then call invalidate() so onGetTemplate() is called again
        invalidate()
    }
    .build()

Java

new PlaceListNavigationTemplate.Builder()
        ...
        .setOnContentRefreshListener(() -> {
            // Execute any desired logic
            ...
            // Then call invalidate() so onGetTemplate() is called again
            invalidate();
        })
        .build();

El botón de actualización solo se muestra en el encabezado de PlaceListNavigationTemplate si el objeto de escucha tiene un valor.

Cuando el conductor hace clic en el botón de actualización, se llama al método onContentRefreshRequested de la implementación de OnContentRefreshListener. En onContentRefreshRequested, llama al método Screen.invalidate. Luego, el host volverá a llamar al método Screen.onGetTemplate de tu app para recuperar la plantilla con el contenido actualizado. Consulta Cómo actualizar el contenido de una plantilla para obtener más información. Siempre que la siguiente plantilla que muestra onGetTemplate sea del mismo tipo, se contará como una actualización y no se sumará a la cuota de la plantilla.

Proporciona indicaciones de audio

Para reproducir las indicaciones de navegación en las bocinas del vehículo, tu app deberá solicitar un foco de audio. Como parte de tu AudioFocusRequest, establece el uso en AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE. Además, establece la ganancia del foco en AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK.

Simula la navegación

Para verificar la funcionalidad de navegación de tu app cuando la envías a Google Play Store, tu app debe implementar la devolución de llamada NavigationManagerCallback.onAutoDriveEnabled. Cuando se realice esta devolución de llamada, la app deberá simular la navegación al destino seleccionado cuando el usuario inicie la navegación. Tu app puede salir de este modo cada vez que el ciclo de vida de la Session actual alcanza el estado Lifecycle.Event.ON_DESTROY.

Puedes probar si se llama a tu implementación de onAutoDriveEnabled si ejecutas lo siguiente desde una línea de comandos:

adb shell dumpsys activity service CAR_APP_SERVICE_NAME AUTO_DRIVE

Esto se muestra en el siguiente ejemplo:

adb shell dumpsys activity service androidx.car.app.samples.navigation.car.NavigationCarAppService AUTO_DRIVE

App de vehículo de navegación predeterminada

En Android Auto, la app de vehículo de navegación predeterminada corresponde a la última app de navegación que inició el usuario. La app predeterminada recibe intents de navegación cuando el usuario invoca comandos de navegación con Asistente o cuando otra app envía un intent para iniciar la navegación.

Muestra alertas de navegación en contexto

Alert muestra información importante al conductor, con acciones opcionales, sin salir del contexto de la pantalla de navegación. Para brindar la mejor experiencia al conductor, Alert funciona dentro de NavigationTemplate lo que evita que se bloquee la ruta de navegación y minimiza la distracción del conductor.

Alert solo está disponible en NavigationTemplate. Para notificar a un usuario fuera de NavigationTemplate, considera usar una notificación de atención (HUN), como se explica en Visualización de notificaciones.

Por ejemplo, usa Alert para lo siguiente:

  • Informar al conductor sobre una actualización relevante para la navegación actual, como un cambio en las condiciones de tráfico
  • Pedirle al conductor una actualización relacionada con la navegación actual, como la existencia de un radar de velocidad
  • Proponer una próxima tarea y preguntar al conductor si la aceptará; por ejemplo, si está dispuesto a recoger a alguien en el camino

En su forma básica, una Alert consta de un título y el tiempo de duración de Alert. La duración se representa con una barra de progreso. De manera opcional, puedes agregar un subtítulo, un ícono y hasta dos elementos Action.

Figura 2. Alerta de navegación en contexto

Una vez que se muestra una Alert, esta no transfiere a otra plantilla si, como resultado de la interacción con un conductor, se abandona la NavigationTemplate. Permanece en la NavigationTemplate original hasta que se agote el tiempo de espera de Alert, el usuario realice una acción o la app la descarte la Alert.

Crea una alerta

Usa Alert.Builder para crear una instancia Alert:

Kotlin

Alert.Builder(
        /*alertId*/ 1,
        /*title*/ CarText.create("Hello"),
        /*durationMillis*/ 5000
    )
    // The fields below are optional
    .addAction(firstAction)
    .addAction(secondAction)
    .setSubtitle(CarText.create(...))
    .setIcon(CarIcon.APP_ICON)
    .setCallback(...)
    .build()

Java

new Alert.Builder(
        /*alertId*/ 1,
        /*title*/ CarText.create("Hello"),
        /*durationMillis*/ 5000
    )
    // The fields below are optional
    .addAction(firstAction)
    .addAction(secondAction)
    .setSubtitle(CarText.create(...))
    .setIcon(CarIcon.APP_ICON)
    .setCallback(...)
    .build();

Si deseas escuchar la cancelación o el descarte de Alert, crea una implementación de la interfaz de AlertCallback. Las rutas de llamadas de AlertCallback son las siguientes:

Configura la duración de las alertas

Elige una duración de Alert que coincida con las necesidades de tu app. La duración recomendada para una Alert de navegación es de 10 segundos. Consulta la sección de alertas de navegación para obtener más información.

Muestra una alerta

Para mostrar una Alert, llama al método AppManager.showAlert disponible desde el elemento CarContext de tu app.

// Show an alert
carContext.getCarService(AppManager.class).showAlert(alert)
  • Llamar a showAlert con un Alert que tiene un alertId igual al ID del Alert que se muestra actualmente no realiza ninguna acción. La Alert no se actualiza. Para actualizar una Alert, debes volver a crearla con un alertId nuevo.
  • Si llamas a showAlert con una Alert que tiene un alertId diferente a la Alert que aparece en ese momento, se descarta la Alert que se muestra.

Descarta una alerta

Si bien una Alert se descarta automáticamente debido al tiempo de espera o a la interacción del conductor, también puedes descartar una Alert de forma manual, por ejemplo, si su información se vuelve obsoleta. Para descartar una Alert, llama al método dismissAlert con el alertId de la Alert.

// Dismiss the same alert
carContext.getCarService(AppManager.class).dismissAlert(alert.getId())

Llamar a dismissAlert con un alertId que no coincide con la Alert que se muestra actualmente no tiene ningún efecto. No arroja una excepción.