Cómo compilar apps multimedia para vehículos

Android Auto y el SO Android Automotive te permiten ofrecerles a los usuarios el contenido de tu app de música en sus vehículos. Una app de música para vehículos debe proporcionar un servicio de navegador multimedia para que Android Auto y el SO Android Automotive (u otra aplicación con un navegador multimedia) puedan descubrir y mostrar tu contenido.

En esta guía, se da por sentado que ya tienes una app de música que reproduce audio en un teléfono y que cumple con la arquitectura de apps de música de Android.

Además, se describen los componentes necesarios de los elementos MediaBrowserService y MediaSession que tu app necesita a fin de que funcione en Android Auto o el SO Android Automotive. Después de que completes la infraestructura de medios principal, podrás agregar compatibilidad con Android Auto y con el SO Android Automotive a tu app de música.

Antes de comenzar

  1. Revisa la documentación de la API de medios de Android.
  2. Revisa los lineamientos de diseño para apps del SO Android Automotive y los lineamientos de diseño para apps de Android Auto.
  3. Revisa los términos y conceptos clave que se indican en esta sección.

Términos y conceptos clave

Servicio de navegador multimedia
Es un servicio de Android que implementa tu app de música y que cumple con la API de MediaBrowserServiceCompat. Tu app usa este servicio para exponer su contenido.
Navegador multimedia
Es una API que usan las apps de música para descubrir servicios de navegador multimedia y mostrar su contenido. Android Auto y el SO Android Automotive usan un navegador multimedia a fin de encontrar el servicio de ese tipo de navegador para tu app.
Elemento multimedia

El navegador multimedia organiza su contenido en un árbol de objetos MediaItem. Un elemento multimedia puede tener una de estas marcas o ambas:

  • Reproducible: Esta marca indica que el elemento es una hoja en el árbol de contenido. El elemento representa una sola transmisión de sonido, como una canción de un álbum, un capítulo de un audiolibro o un episodio de un podcast.
  • Explorable: Esta marca indica que el elemento es un nodo en el árbol de contenido y que tiene elementos secundarios. Por ejemplo, el elemento representa un álbum y sus elementos secundarios son las canciones del álbum.

Un elemento multimedia reproducible y explorable funciona como una lista de reproducción. Puedes seleccionar el elemento para que se reproduzcan todos sus elementos secundarios, o bien puedes navegar por ellos.

Actividades optimizadas para vehículos

Se trata de las actividades de una app del SO Android Automotive que cumplen con los lineamientos de diseño para este SO. La interfaz de estas actividades no está desarrollada por el SO Android Automotive, por lo que debes asegurarte de que tu app cumpla con los lineamientos de diseño. Por lo general, se incluyen objetivos táctiles y tamaños de fuente más grandes, compatibilidad con los modos diurno y nocturno, y relaciones de contraste más altas.

Las interfaces de usuario optimizadas para vehículos solo se muestran si las restricciones de la experiencia del usuario de vehículo (CUXR) no están vigentes, ya que estas interfaces requieren atención extensa o interacción por parte del usuario. Las CUXR no se aplican si el vehículo está detenido o estacionado, pero siempre se aplican cuando está en movimiento.

No es necesario diseñar actividades para Android Auto porque este usa su propia interfaz optimizada para vehículos con la información de tu servicio de navegador multimedia.

Cómo configurar los archivos del manifiesto de tu app

Para poder crear tu servicio de navegador multimedia, debes configurar los archivos de manifiesto de tu app.

Declara tu servicio de navegador multimedia

Tanto Android Auto como el SO Android Automotive se conectan a tu app mediante tu servicio de navegador multimedia para explorar elementos de ese tipo. Debes declarar tu servicio de navegador multimedia en el manifiesto a fin de que Android Auto y el SO Android Automotive descubran el servicio y se conecten a tu app.

En el siguiente fragmento de código, se muestra cómo declarar el servicio de navegador multimedia en el manifiesto. Debes incluir este código en el archivo de manifiesto del módulo del SO Android Automotive y en el archivo de manifiesto de la app de tu teléfono.

<application>
    ...
    <service android:name=".MyMediaBrowserService"
             android:exported="true">
        <intent-filter>
            <action android:name="android.media.browse.MediaBrowserService"/>
        </intent-filter>
    </service>
    ...
<application>

Especifica un ícono de app

Debes especificar un ícono de la app que Android Auto y el SO Android Automotive puedan usar para representar tu app en la IU del sistema.

Puedes especificar el ícono que se usa para representar a tu app con la siguiente declaración de manifiesto:

<!--The android:icon attribute is used by Android Automotive OS-->
<application
    ...
    android:icon="@mipmap/ic_launcher">
    ...
    <!--Used by Android Auto-->
    <meta-data android:name="com.google.android.gms.car.notification.SmallIcon"
               android:resource="@drawable/ic_auto_icon" />
    ...
<application>

Crea tu servicio de navegador multimedia

Puedes extender la clase MediaBrowserServiceCompat para crear un servicio de navegador multimedia. Android Auto y el SO Android Automotive pueden usar tu servicio a los efectos de hacer lo siguiente:

  • Explorar la jerarquía del contenido de tu app con el fin de mostrar un menú al usuario
  • Obtener el token para el objeto MediaSessionCompat de tu app con el fin de controlar la reproducción de audio

Flujo de trabajo del servicio de navegador multimedia

En esta sección, se describe cómo el SO Android Automotive y Android Auto interactúan con tu servicio de navegadores multimedia durante un flujo de trabajo típico de los usuarios.

  1. Un usuario inicia tu app en el SO Android Automotive o en Android Auto.
  2. El SO Android Automotive o Android Auto se comunican con el servicio de navegador multimedia de tu app mediante el método onCreate(). En la implementación del método onCreate(), debes crear y registrar un objeto MediaSessionCompat y su objeto de devolución de llamada.
  3. El SO Android Automotive o Android Auto llaman al método onGetRoot() del servicio para obtener el elemento multimedia raíz en la jerarquía de contenido. El elemento multimedia raíz no se muestra, sino que se usa para recuperar más contenido de la app.
  4. El SO Android Automotive o Android Auto llaman al método onLoadChildren() del servicio para obtener los elementos secundarios del elemento multimedia raíz. El SO Android Automotive y Android Auto muestran estos elementos multimedia como el nivel superior de los elementos de contenido. Consulta Estructura del menú raíz para obtener más información sobre lo que el sistema espera en este nivel.
  5. Si el usuario selecciona un elemento multimedia explorable, se llama de nuevo al método onLoadChildren() del servicio para recuperar los elementos secundarios del elemento de menú seleccionado.
  6. Si el usuario selecciona un elemento multimedia reproducible, el SO Android Automotive o Android Auto llaman al método de devolución de llamada de la sesión multimedia correspondiente para llevar a cabo esa acción.
  7. Si la app lo permite, el usuario también puede hacer búsquedas en el contenido. En este caso, el SO Android Automotive o Android Auto llaman al método onSearch() del servicio.

Compila tu jerarquía de contenido

El SO Android Automotive o Android Auto llaman al servicio de navegador multimedia de tu app para averiguar qué contenido está disponible. Para ello, debes implementar dos métodos en tu servicio de navegador multimedia: onGetRoot() y onLoadChildren().

Implementa onGetRoot

El método onGetRoot() de tu servicio muestra información sobre el nodo raíz de tu jerarquía de contenido. El SO Android Automotive y Android Auto usan este nodo raíz para solicitar el resto del contenido mediante el método onLoadChildren().

En el siguiente fragmento de código, se muestra una implementación simple del método onGetRoot():

Kotlin

override fun onGetRoot(
    clientPackageName: String,
    clientUid: Int,
    rootHints: Bundle?
): BrowserRoot? =
    // Verify that the specified package is allowed to access your
    // content! You'll need to write your own logic to do this.
    if (!isValid(clientPackageName, clientUid)) {
        // If the request comes from an untrusted package, return null.
        // No further calls will be made to other media browsing methods.

        null
    } else MediaBrowserServiceCompat.BrowserRoot(MY_MEDIA_ROOT_ID, null)

Java

@Override
public BrowserRoot onGetRoot(String clientPackageName, int clientUid,
    Bundle rootHints) {

    // Verify that the specified package is allowed to access your
    // content! You'll need to write your own logic to do this.
    if (!isValid(clientPackageName, clientUid)) {
        // If the request comes from an untrusted package, return null.
        // No further calls will be made to other media browsing methods.

        return null;
    }

    return new MediaBrowserServiceCompat.BrowserRoot(MY_MEDIA_ROOT_ID, null);
}

Para obtener un ejemplo detallado de este método, consulta el método onGetRoot() en la app de ejemplo de Universal Android Music Player en GitHub.

Agrega validación de paquetes para onGetRoot()

Cuando se realiza una llamada al método onGetRoot() de tu servicio, el paquete de llamadas pasa información de identificación a tu servicio. El servicio puede usar esta información para decidir si el paquete podrá acceder al contenido. Por ejemplo, puedes restringir el acceso al contenido de tu app a una lista de paquetes aprobados si comparas clientPackageName con tu lista de entidades permitidas y verificas el certificado usado para firmar el APK del paquete. Si no se puede verificar el paquete, muestra null a los efectos de denegar el acceso a tu contenido.

Con el fin de proporcionarles a las apps del sistema (como el SO Android Automotive y Android Auto) acceso al contenido, el servicio debe mostrar siempre un valor de BrowserRoot que no sea nulo cuando estas apps del sistema llamen al método onGetRoot(). La firma de la app del SO Android Automotive puede variar según la marca y el modelo del vehículo, por lo que debes permitir que las conexiones de todas las apps del sistema sean compatibles con ese sistema operativo de forma sólida.

En el siguiente fragmento de código, se muestra cómo el servicio puede validar que el paquete de llamada es una app del sistema:

fun isKnownCaller(
    callingPackage: String,
    callingUid: Int
): Boolean {
    ...
    val isCallerKnown = when {
       // If the system is making the call, allow it.
       callingUid == Process.SYSTEM_UID -> true
       // If the app was signed by the same certificate as the platform
       // itself, also allow it.
       callerSignature == platformSignature -> true
       // ... more cases
    }
    return isCallerKnown
}

Este fragmento de código es un extracto de la clase PackageValidator en la app de ejemplo de Universal Android Music Player en GitHub. Consulta esa clase para obtener un ejemplo más detallado de cómo implementar la validación de paquetes para el método onGetRoot() de tu servicio.

Además de permitir las apps del sistema, debes permitir que Asistente de Google se conecte a tu MediaBrowserService. Ten en cuenta que Asistente de Google tiene nombres de paquete separados para el teléfono (que incluye Android Auto) y el SO Android Automotive.

Implementa onLoadChildren()

Después de recibir el objeto del nodo raíz, el SO Android Automotive y Android Auto compilan un menú de nivel superior llamando a onLoadChildren() en el objeto de nodo raíz para obtener sus elementos secundarios. Las apps cliente compilan submenús llamando al mismo método con los objetos del nodo secundario.

Cada nodo de tu jerarquía de contenido está representado por un objeto MediaBrowserCompat.MediaItem. Cada uno de estos elementos multimedia se identifica mediante una string de ID único. Las apps cliente tratan estas strings de ID como tokens opacos. Si una app cliente quiere examinar un submenú o reproducir un elemento multimedia, pasa el token. Tu app es responsable de asociar el token con el elemento multimedia correspondiente.

Nota: Android Auto y el SO Android Automotive tienen límites estrictos sobre la cantidad de elementos multimedia que pueden mostrar en cada nivel del menú. Esos límites minimizan las distracciones de los conductores y ayudan a operar la app mediante comandos por voz. Para obtener más información, consulta Explora detalles de contenido y Explora vistas.

En el siguiente fragmento de código, se muestra una implementación simple del método onLoadChildren():

Kotlin

override fun onLoadChildren(
    parentMediaId: String,
    result: Result<List<MediaBrowserCompat.MediaItem>>
) {
    // Assume for example that the music catalog is already loaded/cached.

    val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()

    // Check if this is the root menu:
    if (MY_MEDIA_ROOT_ID == parentMediaId) {

        // build the MediaItem objects for the top level,
        // and put them in the mediaItems list
    } else {

        // examine the passed parentMediaId to see which submenu we're at,
        // and put the children of that menu in the mediaItems list
    }
    result.sendResult(mediaItems)
}

Java

@Override
public void onLoadChildren(final String parentMediaId,
    final Result<List<MediaBrowserCompat.MediaItem>> result) {

    // Assume for example that the music catalog is already loaded/cached.

    List<MediaBrowserCompat.MediaItem> mediaItems = new ArrayList<>();

    // Check if this is the root menu:
    if (MY_MEDIA_ROOT_ID.equals(parentMediaId)) {

        // build the MediaItem objects for the top level,
        // and put them in the mediaItems list
    } else {

        // examine the passed parentMediaId to see which submenu we're at,
        // and put the children of that menu in the mediaItems list
    }
    result.sendResult(mediaItems);
}

Para obtener un ejemplo completo de este método, consulta el método onLoadChildren() en la app de ejemplo de Universal Android Music Player en GitHub.

Estructura el menú raíz

Figura 1: El contenido raíz se muestra como pestañas de navegación

Android Auto y el SO Android Automotive tienen restricciones específicas sobre la estructura del menú raíz. Se comunican con MediaBrowserService a través de sugerencias de raíz, que se pueden leer por medio del argumento Bundle que se pasa a onGetRoot(). Si sigues estas sugerencias, el sistema mostrará el contenido raíz de forma óptima como pestañas de navegación (aunque Android Auto no mostrará este contenido de forma predeterminada. Consulta el período de participación que aparece a continuación). Si no sigues estas sugerencias, es posible que el sistema descarte o le quite visibilidad a parte del contenido raíz. Se envían dos sugerencias:

  1. Un límite de la cantidad de elementos secundarios raíz: En la mayoría de los casos, es posible que este número sea cuatro. Esto significa que no se pueden mostrar más de cuatro pestañas.
  2. Marcas compatibles en los elementos secundarios raíz: Puedes esperar que este valor sea MediaItem#FLAG_BROWSABLE. Esto significa que solo los elementos explorables se pueden mostrar como pestañas, y los elementos reproducibles no pueden mostrarse.

Usa el siguiente código para leer las sugerencias de raíz relevantes:

Kotlin

import androidx.media.utils.MediaConstants

// Later, in your MediaBrowserServiceCompat
override fun onGetRoot(
    clientPackageName: String,
    clientUid: Int,
    rootHints: Bundle
): BrowserRoot {

  val maximumRootChildLimit = rootHints.getInt(
      MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_LIMIT,
      /* defaultValue= */ 4)
  val supportedRootChildFlags = rootHints.getInt(
      MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_SUPPORTED_FLAGS,
      /* defaultValue= */ MediaItem.FLAG_BROWSABLE)

  // rest of method..
}

Java

import androidx.media.utils.MediaConstants;

// Later, in your MediaBrowserServiceCompat
@Override
public BrowserRoot onGetRoot(
    String clientPackageName, int clientUid, Bundle rootHints) {

    int maximumRootChildLimit = rootHints.getInt(
        MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_LIMIT,
        /* defaultValue= */ 4);
    int supportedRootChildFlags = rootHints.getInt(
        MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_SUPPORTED_FLAGS,
        /* defaultValue= */ MediaItem.FLAG_BROWSABLE);

    // rest of method...
}

Puedes ramificar la lógica de la estructura de tu jerarquía de contenido en función de los valores de estas sugerencias, especialmente si tu jerarquía varía entre las integraciones de MediaBrowser fuera de Android Auto y el SO Android Automotive. Por ejemplo, si sueles mostrar un elemento raíz reproducible, te recomendamos que, en su lugar, lo anides en un elemento raíz explorable, debido al valor de la sugerencia de marcas compatible.

Además de las sugerencias de raíz, debes seguir algunos lineamientos adicionales a fin de garantizar que las pestañas se procesen de forma óptima:

  1. Proporciona íconos monocromáticos (preferentemente uno blanco) para cada elemento de la pestaña.
  2. Proporciona etiquetas cortas pero significativas para cada elemento de la pestaña. Tener etiquetas breves reduce la posibilidad de que se corten las strings.

Habilita las pestañas en Android Auto

La compatibilidad con las pestañas de navegación es nueva en Android Auto. Anteriormente, Android Auto mostraba el contenido raíz como una lista. Debido a que las pestañas generan un cambio significativo en el comportamiento del acceso a contenido de los usuarios y estas podrían no ser óptimas para algunas apps existentes (es decir, aquellas con elementos raíz reproducibles o con muchos elementos raíz navegables), de forma predeterminada, Android Auto ofrece a los desarrolladores habilitar las pestañas hasta mayo de 2021, cuando todas las apps se pasarán a la interfaz con pestañas. Antes de esa fecha, el contenido raíz se seguirá presentando como una lista de forma predeterminada, pero podrás habilitar las pestañas para tus usuarios mediante el envío de un extra en el Bundle de la BrowserRoot de tu servicio, en el método onGetRoot().

El siguiente fragmento de código muestra cómo habilitar las pestañas:

Kotlin

val TABS_OPT_IN_HINT = "android.media.browse.AUTO_TABS_OPT_IN_HINT"

override fun onGetRoot(
    @NonNull clientPackageName: String,
    clientUid: Int,
    @Nullable rootHints: Bundle
): BrowserRoot {
    val extras = Bundle()
    extras.putBoolean(TABS_OPT_IN_HINT, true)
    return BrowserRoot(ROOT_ID, extras)
}

Java

static final String TABS_OPT_IN_HINT =
  "android.media.browse.AUTO_TABS_OPT_IN_HINT";

@Override
public BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid,
   @Nullable Bundle rootHints) {
   Bundle extras = new Bundle();
   extras.putBoolean(TABS_OPT_IN_HINT, true);
   return new BrowserRoot(ROOT_ID, extras);
}

Alentamos a los desarrolladores a que mantengan las apps habilitadas para Android Auto a fin de que sus equipos prueben las pestañas primero, antes de habilitar la función para los usuarios.

Muestra material gráfico multimedia

El material gráfico de los elementos multimedia se debe pasar como un URI local mediante ContentResolver.SCHEME_CONTENT o ContentResolver.SCHEME_ANDROID_RESOURCE. Este URI local debe resolverse como un mapa de bits o una interfaz dibujable en vector en los recursos de la aplicación. Para los objetos MediaDescription que representan elementos de la jerarquía de contenido, pasa el URI a través de setIconUri(). Para los objetos MediaMetadata que representan el elemento que se está reproduciendo, pasa el URI a través de putString() mediante cualquiera de las siguientes claves:

A continuación, se muestra un ejemplo de cómo descargar arte desde un URI web y exponerlo a través de un URI local:

  1. Descarga tu archivo de imagen (el siguiente fragmento de código usa Glide).

    Kotlin

    val artFile = Glide.with(context)
      .downloadOnly()
      .load(imageUri)
      .submit()
      .get()
    

    Java

    File artFile = Glide.with(context)
      .downloadOnly()
      .load(imageUri)
      .submit()
      .get();
    
  2. Compila un URI de content:// para el archivo. La sesión y el servicio del navegador multimedia deben pasar este URI a Android Auto y el SO Android Automotive.

    Kotlin

    fun File.asAlbumArtContentURI(): Uri {
      return Uri.Builder()
        .scheme(ContentResolver.SCHEME_CONTENT)
        .authority(AUTHORITY)
        .appendPath(this.path)
        .build()
    }
    

    Java

    public static Uri asAlbumArtContentURI(File file) {
      return Uri.Builder()
        .scheme(ContentResolver.SCHEME_CONTENT)
        .authority(AUTHORITY)
        .appendPath(file.getPath())
        .build();
    }
    
  3. Permite el acceso al archivo en el método ContentProvider.openFile().

    Kotlin

    override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
      val context = this.context ?: return null
      val file = File(uri.path)
      if (!file.exists()) {
        throw FileNotFoundException(uri.path)
      }
      // Only allow access to files under cache path
      val cachePath = context.cacheDir.path
      if (!file.path.startsWith(cachePath)) {
        throw FileNotFoundException()
      }
      return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
    }
    

    Java

    @Nullable
    @Override
    public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode)
        throws FileNotFoundException {
      Context context = this.getContext();
      File file = new File(uri.getPath());
      if (!file.exists()) {
        throw new FileNotFoundException(uri.getPath());
      }
      // Only allow access to files under cache path
      String cachePath = context.getCacheDir().getPath();
      if (!file.getPath().startsWith(cachePath)) {
        throw new FileNotFoundException();
      }
      return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
    }
    

Para obtener más información sobre los proveedores de contenido, consulta Cómo crear un proveedor de contenido.

Aplica los estilos de contenido

Después de crear la jerarquía de contenido con elementos que se pueden reproducir o en los que se puede navegar, puedes aplicar estilos de contenido que determinen cómo se muestran esos elementos en el automóvil.

Puedes usar los siguientes estilos de contenido:

Elementos de listas

Este estilo de contenido prioriza los títulos y los metadatos por sobre las imágenes.

Elementos de cuadrícula

Este estilo de contenido prioriza las imágenes por sobre los títulos y los metadatos.

Establece estilos de contenido predeterminados

Puedes establecer valores globales predeterminados para la manera en la que se muestran los elementos multimedia si incluyes ciertas constantes en el paquete de extras BrowserRoot del método onGetRoot() de tu servicio. Android Auto y el SO Android Automotive leen el paquete de extras asociado con cada elemento en el árbol de navegación y buscan esas constantes para determinar el estilo apropiado.

Usa el siguiente código para declarar estas constantes en tu app:

Kotlin

/** Declares that ContentStyle is supported */
val CONTENT_STYLE_SUPPORTED = "android.media.browse.CONTENT_STYLE_SUPPORTED"

/**
 * Bundle extra indicating the presentation hint for playable media items.
 */
val CONTENT_STYLE_PLAYABLE_HINT = "android.media.browse.CONTENT_STYLE_PLAYABLE_HINT"

/**
 * Bundle extra indicating the presentation hint for browsable media items.
 */
val CONTENT_STYLE_BROWSABLE_HINT = "android.media.browse.CONTENT_STYLE_BROWSABLE_HINT"

/**
 * Specifies the corresponding items should be presented as lists.
 */
val CONTENT_STYLE_LIST_ITEM_HINT_VALUE = 1

/**
 * Specifies that the corresponding items should be presented as grids.
 */
val CONTENT_STYLE_GRID_ITEM_HINT_VALUE = 2

/**
 * Specifies that the corresponding items should be presented as lists and are
 * represented by a vector icon. This adds a small margin around the icons
 * instead of filling the full available area.
 */
val CONTENT_STYLE_CATEGORY_LIST_ITEM_HINT_VALUE = 3

/**
 * Specifies that the corresponding items should be presented as grids and are
 * represented by a vector icon. This adds a small margin around the icons
 * instead of filling the full available area.
 */
val CONTENT_STYLE_CATEGORY_GRID_ITEM_HINT_VALUE = 4

Java

/* Declares that ContentStyle is supported */
public static final String CONTENT_STYLE_SUPPORTED =
   "android.media.browse.CONTENT_STYLE_SUPPORTED";

/*
 * Bundle extra indicating the presentation hint for playable media items.
 */
public static final String CONTENT_STYLE_PLAYABLE_HINT =
   "android.media.browse.CONTENT_STYLE_PLAYABLE_HINT";

/*
 * Bundle extra indicating the presentation hint for browsable media items.
 */
public static final String CONTENT_STYLE_BROWSABLE_HINT =
   "android.media.browse.CONTENT_STYLE_BROWSABLE_HINT";

/*
 * Specifies the corresponding items should be presented as lists.
 */
public static final int CONTENT_STYLE_LIST_ITEM_HINT_VALUE = 1;

/*
 * Specifies that the corresponding items should be presented as grids.
 */
public static final int CONTENT_STYLE_GRID_ITEM_HINT_VALUE = 2;

/*
 * Specifies that the corresponding items should be presented as lists and are
 * represented by a vector icon. This adds a small margin around the icons
 * instead of filling the full available area.
 */
public static final int CONTENT_STYLE_CATEGORY_LIST_ITEM_HINT_VALUE = 3

/*
 * Specifies that the corresponding items should be presented as grids and are
 * represented by a vector icon. This adds a small margin around the icons
 * instead of filling the full available area.
 */
public static final int CONTENT_STYLE_CATEGORY_GRID_ITEM_HINT_VALUE = 4

Una vez que hayas declarado estas constantes, inclúyelas en el paquete de extras del método onGetRoot() de tu servicio para establecer el estilo predeterminado del contenido. En el siguiente fragmento de código, se muestra cómo establecer el estilo de contenido predeterminado de los elementos explorables en cuadrículas y el correspondiente a los elementos reproducibles en listas:

Kotlin

@Nullable
override fun onGetRoot(
    @NonNull clientPackageName: String,
    clientUid: Int,
    @Nullable rootHints: Bundle
): BrowserRoot {
    val extras = Bundle()
    extras.putBoolean(CONTENT_STYLE_SUPPORTED, true)
    extras.putInt(CONTENT_STYLE_BROWSABLE_HINT, CONTENT_STYLE_GRID_ITEM_HINT_VALUE)
    extras.putInt(CONTENT_STYLE_PLAYABLE_HINT, CONTENT_STYLE_LIST_ITEM_HINT_VALUE)
    return BrowserRoot(ROOT_ID, extras)
}

Java

@Nullable
@Override
public BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid,
   @Nullable Bundle rootHints) {
   Bundle extras = new Bundle();
   extras.putBoolean(CONTENT_STYLE_SUPPORTED, true);
   extras.putInt(CONTENT_STYLE_BROWSABLE_HINT, CONTENT_STYLE_GRID_ITEM_HINT_VALUE);
   extras.putInt(CONTENT_STYLE_PLAYABLE_HINT, CONTENT_STYLE_LIST_ITEM_HINT_VALUE);
   return new BrowserRoot(ROOT_ID, extras);
}

Define estilos de contenido por elemento

La API de estilo de contenido te permite anular el estilo de contenido predeterminado de cualquier elemento secundario de un elemento multimedia explorable. Para anular el valor predeterminado, crea un paquete de extras en la MediaDescription de un elemento multimedia.

En el siguiente fragmento de código, se muestra cómo crear un objeto MediaItem explorable que anule el estilo de contenido predeterminado:

Kotlin

private fun createBrowsableMediaItem(
    mediaId: String,
    folderName: String,
    iconUri: Uri
): MediaBrowser.MediaItem {
    val mediaDescriptionBuilder = MediaDescription.Builder()
    mediaDescriptionBuilder.setMediaId(mediaId)
    mediaDescriptionBuilder.setTitle(folderName)
    mediaDescriptionBuilder.setIconUri(iconUri)
    val extras = Bundle()
    extras.putInt(CONTENT_STYLE_BROWSABLE_HINT, CONTENT_STYLE_LIST_ITEM_HINT_VALUE)
    extras.putInt(CONTENT_STYLE_PLAYABLE_HINT, CONTENT_STYLE_GRID_ITEM_HINT_VALUE)
    mediaDescriptionBuilder.setExtras(extras)
    return MediaBrowser.MediaItem(
        mediaDescriptionBuilder.build(), MediaBrowser.MediaItem.FLAG_BROWSABLE)
}

Java

private MediaBrowser.MediaItem createBrowsableMediaItem(String mediaId,
   String folderName, Uri iconUri) {
   MediaDescription.Builder mediaDescriptionBuilder = new MediaDescription.Builder();
   mediaDescriptionBuilder.setMediaId(mediaId);
   mediaDescriptionBuilder.setTitle(folderName);
   mediaDescriptionBuilder.setIconUri(iconUri);
   Bundle extras = new Bundle();
   extras.putInt(CONTENT_STYLE_BROWSABLE_HINT, CONTENT_STYLE_LIST_ITEM_HINT_VALUE);
   extras.putInt(CONTENT_STYLE_PLAYABLE_HINT, CONTENT_STYLE_GRID_ITEM_HINT_VALUE);
   mediaDescriptionBuilder.setExtras(extras);
   return new MediaBrowser.MediaItem(
       mediaDescriptionBuilder.build(), MediaBrowser.MediaItem.FLAG_BROWSABLE);
}

Agrupa elementos con sugerencias de títulos

Para agrupar elementos multimedia relacionados, usa una sugerencia que se relacione con ese elemento. Cada elemento multimedia de un grupo debe declarar un paquete de extras en su MediaDescription que use una string idéntica. Esa string se usa como título del grupo y se puede localizar.

En el siguiente fragmento de código, se muestra cómo crear un MediaItem con un encabezado de subgrupo de "Songs":

Kotlin

val EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT = "android.media.browse.CONTENT_STYLE_GROUP_TITLE_HINT"

private fun createMediaItem(
    mediaId: String,
    folderName: String,
    iconUri: Uri
): MediaBrowser.MediaItem {
    val mediaDescriptionBuilder = MediaDescription.Builder()
    mediaDescriptionBuilder.setMediaId(mediaId)
    mediaDescriptionBuilder.setTitle(folderName)
    mediaDescriptionBuilder.setIconUri(iconUri)
    val extras = Bundle()
    extras.putString(EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT, "Songs")
    mediaDescriptionBuilder.setExtras(extras)
    return MediaBrowser.MediaItem(
        mediaDescriptionBuilder.build(), /* playable or browsable flag*/)
}

Java

public static final String EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT =
  "android.media.browse.CONTENT_STYLE_GROUP_TITLE_HINT";

private MediaBrowser.MediaItem createMediaItem(String mediaId, String folderName, Uri iconUri) {
   MediaDescription.Builder mediaDescriptionBuilder = new MediaDescription.Builder();
   mediaDescriptionBuilder.setMediaId(mediaId);
   mediaDescriptionBuilder.setTitle(folderName);
   mediaDescriptionBuilder.setIconUri(iconUri);
   Bundle extras = new Bundle();
   extras.putString(EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT, "Songs");
   mediaDescriptionBuilder.setExtras(extras);
   return new MediaBrowser.MediaItem(
       mediaDescriptionBuilder.build(), /* playable or browsable flag*/);
}

Tu app debe pasar todos los elementos multimedia que deseas agrupar como un bloque contiguo. Por ejemplo, supongamos que quieres mostrar dos grupos de elementos multimedia, "Canciones" y "Álbumes" (en ese orden), y que tu app pasa cinco elementos multimedia en el siguiente orden:

  1. Elemento multimedia A con extras.putString(EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT, "Songs")
  2. Elemento multimedia B con extras.putString(EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT, "Albums")
  3. Elemento multimedia C con extras.putString(EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT, "Songs")
  4. Elemento multimedia D con extras.putString(EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT, "Songs")
  5. Elemento multimedia E con extras.putString(EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT, "Albums")

Debido a que los elementos multimedia de los grupos "Canciones" y "Álbumes" no se mantienen juntos en bloques contiguos, Android Auto y el SO Android Automotive interpretarían esto como los siguientes cuatro grupos:

  • Grupo 1 llamado "Canciones" que contiene el elemento multimedia A
  • Grupo 2 llamado "Álbumes" que contiene el elemento multimedia B
  • Grupo 3 llamado "Canciones" que contiene elementos multimedia C y D
  • Grupo 4 llamado "Álbumes" que contiene el elemento multimedia E

Para mostrar estos elementos en dos grupos, la app pasaría las apps en el siguiente orden:

  1. Elemento multimedia A con extras.putString(EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT, "Songs")
  2. Elemento multimedia C con extras.putString(EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT, "Songs")
  3. Elemento multimedia D con extras.putString(EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT, "Songs")
  4. Elemento multimedia B con extras.putString(EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT, "Albums")
  5. Elemento multimedia E con extras.putString(EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT, "Albums")

Muestra indicadores de metadatos adicionales

Figura 2: Vista de reproducción con metadatos que identifican la canción y el artista, así como un ícono que indica contenido explícito

Puedes incluir indicadores de metadatos adicionales a fin de proporcionar información de un vistazo sobre el contenido en el árbol del navegador multimedia y durante la reproducción. Dentro del árbol de navegación, Android Auto y el SO Android Automotive leen los extras asociados con un elemento y buscan ciertas constantes para determinar qué indicadores mostrar. Durante la reproducción de contenido multimedia, Android Auto y el SO Android Automotive leen los metadatos de la sesión multimedia y buscan determinadas constantes para determinar los indicadores que se mostrarán.

Usa el siguiente código para declarar las constantes del indicador de metadatos en tu app:

Kotlin

// Bundle extra indicating that a song contains explicit content.
var EXTRA_IS_EXPLICIT = "android.media.IS_EXPLICIT"

/**
* Bundle extra indicating the download status of a media item.
* Same as MediaDescriptionCompat.EXTRA_DOWNLOAD_STATUS.
*/
var EXTRA_DOWNLOAD_STATUS = "android.media.extra.DOWNLOAD_STATUS"

/**
* Bundle extra indicating the played state of long-form content (such as podcast
* episodes or audiobooks).
*/
var EXTRA_PLAY_COMPLETION_STATE = "android.media.extra.PLAYBACK_STATUS"

/**
* Value for EXTRA_PLAY_COMPLETION_STATE that indicates the media item has
* not been played at all.
*/
var STATUS_NOT_PLAYED = 0

/**
* Value for EXTRA_PLAY_COMPLETION_STATE that indicates the media item has
* been partially played (i.e. the current position is somewhere in the middle).
*/
var STATUS_PARTIALLY_PLAYED = 1

/**
* Value for EXTRA_PLAY_COMPLETION_STATE that indicates the media item has
* been completed.
*/
var STATUS_FULLY_PLAYED = 2

Java

// Bundle extra indicating that a song contains explicit content.
String EXTRA_IS_EXPLICIT = "android.media.IS_EXPLICIT";

/**
 * Bundle extra indicating the download status of a media item.
 * Same as MediaDescriptionCompat.EXTRA_DOWNLOAD_STATUS.
 */
String EXTRA_DOWNLOAD_STATUS = "android.media.extra.DOWNLOAD_STATUS";

/**
 * Bundle extra indicating the played state of long-form content (such as podcast
 * episodes or audiobooks).
 */
String EXTRA_PLAY_COMPLETION_STATE = "android.media.extra.PLAYBACK_STATUS";

/**
 * Value for EXTRA_PLAY_COMPLETION_STATE that indicates the media item has
 * not been played at all.
 */
int STATUS_NOT_PLAYED = 0;

/**
 * Value for EXTRA_PLAY_COMPLETION_STATE that indicates the media item has
 * been partially played (i.e. the current position is somewhere in the middle).
 */
int STATUS_PARTIALLY_PLAYED = 1;

/**
 * Value for EXTRA_PLAY_COMPLETION_STATE that indicates the media item has
 * been completed.
 */
int STATUS_FULLY_PLAYED = 2;

Una vez que hayas declarado estas constantes, puedes usarlas para mostrar los indicadores de metadatos. A fin de mostrar los indicadores que aparecen mientras el usuario explora el árbol del navegador multimedia, crea un paquete de extras que incluya una o más de estas constantes y pasa ese paquete al método MediaDescription.Builder.setExtras().

En el siguiente fragmento de código, se describe cómo mostrar los indicadores para un elemento multimedia explícito con reproducción parcial:

Kotlin

val extras = Bundle()
extras.putLong(EXTRA_IS_EXPLICIT, 1)
extras.putInt(EXTRA_PLAY_COMPLETION_STATE, STATUS_PARTIALLY_PLAYED)
val description = MediaDescriptionCompat.Builder()
.setMediaId(/*...*/)
.setTitle(resources.getString(/*...*/))
.setExtras(extras)
.build()
return MediaBrowserCompat.MediaItem(description, /* flags */)

Java

Bundle extras = new Bundle();
extras.putLong(EXTRA_IS_EXPLICIT, 1);
extras.putInt(EXTRA_PLAY_COMPLETION_STATE, STATUS_PARTIALLY_PLAYED);

MediaDescriptionCompat description = new MediaDescriptionCompat.Builder()
  .setMediaId(/*...*/)
  .setTitle(resources.getString(/*...*/))
  .setExtras(extras)
  .build();
return new MediaBrowserCompat.MediaItem(description, /* flags */);

Si quieres mostrar los indicadores de un elemento multimedia que se está reproduciendo, puedes declarar los valores Long de EXTRA_IS_EXPLICIT o EXTRA_DOWNLOAD_STATUS en el método MediaMetadata.Builder() de tu mediaSession. No puedes mostrar el indicador EXTRA_PLAY_COMPLETION_STATE en la vista de reproducción.

En el siguiente fragmento de código, se muestra cómo indicar que la canción actual de la vista de reproducción es explícita y que se descargó:

Kotlin

mediaSession.setMetadata(
  MediaMetadata.Builder()
  .putString(
    MediaMetadata.METADATA_KEY_DISPLAY_TITLE, "Song Name")
  .putString(
    MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE, "Artist name")
  .putString(MediaMetadata.METADATA_KEY_ALBUM_ART_URI, albumArtUri.toString())
  .putLong(
    EXTRA_IS_EXPLICIT, 1)
  .putLong(
    EXTRA_DOWNLOAD_STATUS, MediaDescriptionCompat.STATUS_DOWNLOADED)
  .build())

Java

mediaSession.setMetadata(
    new MediaMetadata.Builder()
        .putString(
            MediaMetadata.METADATA_KEY_DISPLAY_TITLE, "Song Name")
        .putString(
            MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE, "Artist name")
        .putString(MediaMetadata.METADATA_KEY_ALBUM_ART_URI, albumArtUri.toString())
        .putLong(
            EXTRA_IS_EXPLICIT, 1)
        .putLong(
            EXTRA_DOWNLOAD_STATUS, MediaDescriptionCompat.STATUS_DOWNLOADED)
        .build());

Figura 3: Vista de reproducción con la opción "Resultados de la búsqueda" para ver elementos multimedia relacionados con la búsqueda por voz del usuario

Tu app puede proporcionar resultados de la búsqueda contextuales que se muestren a los usuarios cuando inicien una búsqueda. Android Auto y el SO Android Automotive mostrarán estos resultados a través de interfaces de búsqueda o condiciones que cambian a partir de búsquedas realizadas con anterioridad en la sesión.

Para mostrar resultados de la búsqueda explorables, debes crear una constante e incluirla en el paquete de extras del método onGetRoot() de tu servicio.

En el siguiente fragmento de código, se muestra cómo habilitar la compatibilidad en el método onGetRoot():

Kotlin

// Bundle extra indicating that onSearch() is supported
val EXTRA_MEDIA_SEARCH_SUPPORTED = "android.media.browse.SEARCH_SUPPORTED"

@Nullable
fun onGetRoot(
    @NonNull clientPackageName: String,
    clientUid: Int,
    @Nullable rootHints: Bundle
): BrowserRoot {
    val extras = Bundle()
    extras.putBoolean(EXTRA_MEDIA_SEARCH_SUPPORTED, true)
    return BrowserRoot(ROOT_ID, extras)
}

Java

public static final String EXTRA_MEDIA_SEARCH_SUPPORTED =
   "android.media.browse.SEARCH_SUPPORTED";

@Nullable
@Override
public BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid,
   @Nullable Bundle rootHints) {
   Bundle extras = new Bundle();
   extras.putBoolean(EXTRA_MEDIA_SEARCH_SUPPORTED, true);
   return new BrowserRoot(ROOT_ID, extras);
}

Para comenzar a proporcionar resultados de la búsqueda, anula el método onSearch() en el servicio de navegador multimedia. Android Auto y el SO Android Automotive reenvían los términos de búsqueda del usuario a este método cada vez que un usuario invoca una interfaz de búsqueda o una condición de "resultados de la búsqueda". Puedes organizar los resultados de la búsqueda del método onSearch() de tu servicio mediante elementos de título para que sean más explorables. Por ejemplo, si la app reproduce música, podrás organizar los resultados por "Álbum", "Artista" y "Canciones".

En el siguiente fragmento de código, se muestra una implementación simple del método onSearch():

Kotlin

fun onSearch(query: String, extras: Bundle) {
  // Detach from results to unblock the caller (if a search is expensive)
  result.detach()
  object:AsyncTask() {
    internal var searchResponse:ArrayList
    internal var succeeded = false
    protected fun doInBackground(vararg params:Void):Void {
      searchResponse = ArrayList()
      if (doSearch(query, extras, searchResponse))
      {
        succeeded = true
      }
      return null
    }
    protected fun onPostExecute(param:Void) {
      if (succeeded)
      {
        // Sending an empty List informs the caller that there were no results.
        result.sendResult(searchResponse)
      }
      else
      {
        // This invokes onError() on the search callback
        result.sendResult(null)
      }
      return null
    }
  }.execute()
}
// Populates resultsToFill with search results. Returns true on success or false on error
private fun doSearch(
    query: String,
    extras: Bundle,
    resultsToFill: ArrayList
): Boolean {
  // Implement this method
}

Java

@Override
public void onSearch(final String query, final Bundle extras,
                        Result<List<MediaItem>> result) {

  // Detach from results to unblock the caller (if a search is expensive)
  result.detach();

  new AsyncTask<Void, Void, Void>() {
    List<MediaItem> searchResponse;
    boolean succeeded = false;
    @Override
    protected Void doInBackground(Void... params) {
      searchResponse = new ArrayList<MediaItem>();
      if (doSearch(query, extras, searchResponse)) {
        succeeded = true;
      }
      return null;
    }

    @Override
    protected void onPostExecute(Void param) {
      if (succeeded) {
       // Sending an empty List informs the caller that there were no results.
       result.sendResult(searchResponse);
      } else {
        // This invokes onError() on the search callback
        result.sendResult(null);
      }
    }
  }.execute()
}

/** Populates resultsToFill with search results. Returns true on success or false on error */
private boolean doSearch(String query, Bundle extras, ArrayList<MediaItem> resultsToFill) {
    // Implement this method
}

Habilita el control de reproducción

Android Auto y el SO Android Automotive envían comandos de control de reproducción mediante el elemento MediaSessionCompat del servicio. Debes registrar una sesión y, luego, implementar los métodos de devolución de llamada asociados.

Registra una sesión multimedia

En el método onCreate() del servicio de tu navegador multimedia, crea un objeto MediaSessionCompat y, luego, llama a setSessionToken() a fin de registrar la sesión multimedia.

En el siguiente fragmento de código, se muestra cómo crear y registrar una sesión multimedia:

Kotlin

override fun onCreate() {
    super.onCreate()

    ...
    // Start a new MediaSession
    val session = MediaSessionCompat(this, "session tag").apply {
        // Set a callback object to handle play control requests, which
        // implements MediaSession.Callback
        setCallback(MyMediaSessionCallback())
    }
    sessionToken = session.sessionToken

    ...
}

Java

public void onCreate() {
    super.onCreate();

    ...
    // Start a new MediaSession
    MediaSessionCompat session = new MediaSessionCompat(this, "session tag");
    setSessionToken(session.getSessionToken());

    // Set a callback object to handle play control requests, which
    // implements MediaSession.Callback
    session.setCallback(new MyMediaSessionCallback());

    ...
}

Cuando creas el objeto de la sesión multimedia, estableces un objeto de devolución de llamada que se usa a fin de manejar las solicitudes de control de reproducción. A los efectos de crear este objeto de devolución de llamada, debes proporcionar una implementación de la clase MediaSessionCompat.Callback para tu app. En la siguiente sección, se explica cómo implementar este objeto.

Implementa comandos de reproducción

Cuando un usuario solicita la reproducción de un elemento multimedia de tu app, el SO Android Automotive y Android Auto usan la clase MediaSessionCompat.Callback del objeto MediaSessionCompat de tu app que se obtuvo del servicio de navegador multimedia de la app. Cuando un usuario desea controlar la reproducción de contenido, como pausar la reproducción u omitir la pista, Android Auto y el SO Android Automotive invocan a uno de los métodos del objeto de devolución de llamada.

Para controlar la reproducción de contenido, tu app debe extender la clase abstracta MediaSessionCompat.Callback e implementar los métodos compatibles con tu app.

Debes implementar todos los siguientes métodos de devolución de llamada que correspondan al tipo de contenido que ofrece tu app:

onPrepare()
Se invoca cuando cambia la fuente multimedia. El SO Android Automotive también invoca este método inmediatamente después del inicio. Debes implementar este método en tu app de música.
onPlay()
Se invoca si el usuario opta por reproducir contenido sin elegir un elemento específico. Tu app debe reproducir su contenido predeterminado. Si la reproducción se detuvo con onPause(), tu app debería reanudar la reproducción.

Nota: La app no debería comenzar a reproducir música automáticamente cuando el SO Android Automotive o Android Auto se conecten a tu servicio de navegador multimedia. Para obtener más información, consulta Configura el estado de reproducción inicial.

onPlayFromMediaId()
Se invoca cuando el usuario elige reproducir un elemento específico. El método recibe el ID que tu servicio de navegador multimedia asignó al elemento multimedia en tu jerarquía de contenido.
onPlayFromSearch()
Se invoca cuando el usuario elige reproducir un elemento desde una búsqueda. La app debe hacer la elección apropiada según la string de búsqueda que se pasa.
onPause()
Se invoca cuando el usuario elige pausar la reproducción.
onSkipToNext()
Se invoca cuando el usuario elige omitir el elemento siguiente.
onSkipToPrevious()
Se invoca cuando el usuario elige omitir el elemento anterior.
onStop()
Se invoca cuando el usuario elige detener la reproducción.

La app debe anular estos métodos para proporcionar la funcionalidad deseada. No es necesario que implementes un método si la app no lo admite. Por ejemplo, si tu app reprodujera una transmisión en vivo (como una transmisión deportiva), no tendría sentido implementar el método onSkipToNext(); podrías usar la implementación predeterminada de onSkipToNext() en su lugar.

Tu app no necesita una lógica especial para reproducir contenido en las bocinas del vehículo. Si la app recibe una solicitud para reproducir contenido, deberá reproducir el audio de la misma manera que lo hace normalmente (por ejemplo, por el altavoz o los auriculares del teléfono del usuario). Android Auto y el SO Android Automotive envían automáticamente el contenido de audio al sistema del automóvil a fin de reproducirlo en las bocinas del vehículo.

Si deseas obtener más información para reproducir contenido de audio, consulta Cómo reproducir contenido multimedia, Cómo administrar la reproducción de audio y ExoPlayer.

Establece acciones de reproducción estándar

Android Auto y el SO Android Automotive muestran los controles de reproducción según las acciones habilitadas en el objeto PlaybackStateCompat.

De forma predeterminada, la app debe admitir las siguientes acciones:

Adicionalmente, es posible que tu app admita las siguientes acciones si son relevantes para su contenido:

Además, te recomendamos que crees una cola de reproducción que se pueda mostrar al usuario. Para ello, debes llamar a los métodos setQueue() y setQueueTitle(), habilitar la acción ACTION_SKIP_TO_QUEUE_ITEM y definir la devolución de llamada onSkipToQueueItem().

Android Auto y el SO Android Automotive muestran los botones para cada acción habilitada, así como la cola de reproducción si eliges crear una.

Reserva un espacio sin usar

Android Auto y el SO Android Automotive reservan espacio en la IU para las acciones ACTION_SKIP_TO_PREVIOUS y ACTION_SKIP_TO_NEXT. Además, Android Auto reserva espacio para la cola de reproducción. Si tu app no admite una de estas funciones, Android Auto y el SO Android Automotive usarán el espacio para mostrar cualquier acción personalizada que crees.

Si no quieres llenar esos espacios con acciones personalizadas, puedes reservarlos para que Android Auto y el SO Android Automotive dejen el espacio en blanco cuando tu app no admita la función correspondiente. Para ello, llama al método setExtras() con un paquete de extras que contenga constantes que correspondan a cada una de las funciones reservadas. Configura en true cada constante para la que desees reservar espacio.

En el siguiente fragmento de código, se muestran las constantes que puedes usar para reservar el espacio sin usar:

Kotlin

// Use these extras to show the transport control buttons for the corresponding actions,
// even when they are not enabled in the PlaybackState.
private const val PLAYBACK_SLOT_RESERVATION_SKIP_TO_NEXT =
        "android.media.playback.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_NEXT"
private const val PLAYBACK_SLOT_RESERVATION_SKIP_TO_PREV =
        "android.media.playback.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_PREVIOUS"
private const val PLAYBACK_SLOT_RESERVATION_QUEUE =
        "android.media.playback.ALWAYS_RESERVE_SPACE_FOR.ACTION_QUEUE"

Java

// Use these extras to show the transport control buttons for the corresponding actions,
// even when they are not enabled in the PlaybackState.
private static final String PLAYBACK_SLOT_RESERVATION_SKIP_TO_NEXT =
    "android.media.playback.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_NEXT";
private static final String PLAYBACK_SLOT_RESERVATION_SKIP_TO_PREV =
    "android.media.playback.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_PREVIOUS";
private static final String PLAYBACK_SLOT_RESERVATION_QUEUE =
    "android.media.playback.ALWAYS_RESERVE_SPACE_FOR.ACTION_QUEUE";

Establece el PlaybackState inicial

A medida que Android Auto y el SO Android Automotive se comunican con tu servicio de navegador multimedia, tu sesión multimedia comunica el estado de la reproducción de contenido mediante PlaybackState. Tu app no debería iniciar automáticamente la reproducción de música cuando el SO Android Automotive o Android Auto se conecten a tu servicio de navegador multimedia. En su lugar, utiliza Android Auto y el SO Android Automotive a fin de reanudar o iniciar la reproducción según el estado del automóvil o las acciones del usuario.

Para ello, configura el elemento PlaybackState inicial de tu sesión multimedia en STATE_STOPPED, STATE_PAUSED, STATE_NONE o STATE_ERROR.

Las sesiones multimedia dentro de Android Auto y el SO Android Automotive duran solo lo que dure el viaje, por lo que los usuarios inician y detienen estas sesiones con frecuencia. A fin de promover una experiencia fluida entre viajes, lleva un registro del estado de sesión anterior del usuario (por ejemplo, el último elemento multimedia que se reprodujo, el PlaybackState y la fila), para que cuando la app de música reciba una solicitud de reanudación, el usuario pueda retomar automáticamente desde donde dejó.

Agrega acciones de reproducción personalizadas

Puedes agregar acciones de reproducción personalizadas para mostrar las acciones adicionales que admite tu app de música. Si el espacio lo permite (y no está reservado), Android agregará las acciones personalizadas a los controles de transporte. De lo contrario, las acciones personalizadas se mostrarán en el menú ampliado. Las acciones personalizadas se muestran en el orden en que se agreguen a PlaybackState.

Las acciones personalizadas deben proporcionar un comportamiento distinto de las acciones estándar y no deben usarse para reemplazar o duplicar estas últimas.

Puedes agregar acciones personalizadas con el método addCustomAction() en la clase PlaybackStateCompat.Builder.

En el siguiente fragmento de código, se muestra cómo agregar una acción "Iniciar un canal de radio":

Kotlin

stateBuilder.addCustomAction(
        PlaybackStateCompat.CustomAction.Builder(
                CUSTOM_ACTION_START_RADIO_FROM_MEDIA,
                resources.getString(R.string.start_radio_from_media),
                startRadioFromMediaIcon
        ).run {
            setExtras(customActionExtras)
            build()
        }
)

Java

stateBuilder.addCustomAction(new PlaybackStateCompat.CustomAction.Builder(
    CUSTOM_ACTION_START_RADIO_FROM_MEDIA,
    resources.getString(R.string.start_radio_from_media), startRadioFromMediaIcon)
    .setExtras(customActionExtras)
    .build());

Para obtener un ejemplo detallado de este método, consulta el método setCustomAction() en la app de ejemplo de Universal Android Music Player en GitHub.

Después de crear tu acción personalizada, la sesión multimedia puede responder a ella mediante la anulación del método onCustomAction().

En el siguiente fragmento de código, se muestra cómo tu app puede responder a una acción "Iniciar un canal de radio":

Kotlin

override fun onCustomAction(action: String, extras: Bundle?) {
    when(action) {
        CUSTOM_ACTION_START_RADIO_FROM_MEDIA -> {
            ...
        }
    }
}

Java

@Override
public void onCustomAction(@NonNull String action, Bundle extras) {
    if (CUSTOM_ACTION_START_RADIO_FROM_MEDIA.equals(action)) {
        ...
    }
}

Para obtener un ejemplo detallado de este método, consulta el método onCustomAction en la app de ejemplo de Universal Android Music Player en GitHub.

Íconos para acciones personalizadas

Cada acción personalizada que creas requiere un recurso de ícono. Las apps para vehículos se pueden ejecutar en diferentes tamaños y densidades de pantallas, de modo que los íconos que proporcionas deben ser interfaces dibujables en vector. Una interfaz dibujable en vector te permite ajustar la escala de elementos sin perder los detalles y también facilita la alineación de los bordes y las esquinas según los límites de píxeles en resoluciones más pequeñas.

Si tienes una acción personalizada con estado (por ejemplo, si activa o desactiva una configuración de reproducción), proporciona diferentes íconos para los distintos estados de manera que los usuarios puedan percibir los cambios visualmente cuando seleccionen la acción.

Proporciona un estilo de ícono alternativo para las acciones inhabilitadas

Para los casos en los que una acción personalizada no está disponible en el contexto actual, reemplaza el ícono de acción personalizada por uno alternativo que muestre que la acción está inhabilitada.

Figura 4: Ejemplo de íconos de acción personalizada inhabilitada

Cómo admitir acciones de voz

Tu app de música debe admitir acciones de voz a fin de ayudar a proporcionar a los conductores una experiencia más segura y conveniente que minimice las distracciones. Por ejemplo, si tu app ya está reproduciendo un elemento multimedia, el usuario puede decir "Reproducir Rapsodia bohemia" para indicarle a tu app que reproduzca una canción diferente sin mirar ni tocar la pantalla del vehículo.

A fin de obtener un ejemplo detallado para implementar acciones de reproducción habilitadas para voz en tu app, consulta Asistente de Google y las apps de música.

Cómo implementar protecciones contra distracción

Debido a que el teléfono del usuario está conectado a las bocinas del vehículo mientras usa Android Auto, debes tomar precauciones adicionales para ayudar a evitar la distracción del conductor.

Detecta el modo de vehículo

Las apps de música de Android Auto no deben iniciar la reproducción de audio en las bocinas del vehículo a menos que el usuario inicie la reproducción de forma consciente (por ejemplo, presionando el botón de reproducción en tu app). Ni siquiera una alarma programada por el usuario desde la app de música debería iniciar la reproducción de música en las bocinas del vehículo. A los efectos de cumplir con este requisito, la app deberá determinar si el teléfono está en modo de vehículo antes de reproducir audio. Tu app puede comprobar si el teléfono está en este modo. Para ello, debe llamar al método getCurrentModeType().

Si el teléfono del usuario está en modo de vehículo, las apps de música que admiten alarmas deberán hacer una de las siguientes acciones:

  • Desactivar la alarma
  • Reproducir la alarma mediante STREAM_ALARM y proporcionar una IU en la pantalla del teléfono para desactivarla

En el siguiente fragmento de código, se muestra cómo comprobar si una app se está ejecutando en modo de vehículo:

Kotlin

fun isCarUiMode(c: Context): Boolean {
    val uiModeManager = c.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager
    return if (uiModeManager.currentModeType == Configuration.UI_MODE_TYPE_CAR) {
        LogHelper.d(TAG, "Running in Car mode")
        true
    } else {
        LogHelper.d(TAG, "Running in a non-Car mode")
        false
    }
}

Java

 public static boolean isCarUiMode(Context c) {
      UiModeManager uiModeManager = (UiModeManager) c.getSystemService(Context.UI_MODE_SERVICE);
      if (uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_CAR) {
            LogHelper.d(TAG, "Running in Car mode");
            return true;
      } else {
          LogHelper.d(TAG, "Running in a non-Car mode");
          return false;
        }
  }

Controla los anuncios de medios

De forma predeterminada, Android Auto muestra una notificación cuando los metadatos de los elementos multimedia cambian durante una sesión de reproducción de audio. Cuando una app de música cambia de reproducir música a ejecutar un anuncio, mostrar una notificación al usuario resulta una distracción (y es innecesario). Para evitar que Android Auto muestre una notificación en este caso, debes establecer la clave de metadatos de medios android.media.metadata.ADVERTISEMENT en 1, como se muestra en el siguiente fragmento de código:

Kotlin

const val EXTRA_METADATA_ADVERTISEMENT = "android.media.metadata.ADVERTISEMENT"
...
override fun onPlayFromMediaId(mediaId: String, extras: Bundle?) {
    MediaMetadataCompat.Builder().apply {
        // ...
        if (isAd(mediaId)) {
            putLong(EXTRA_METADATA_ADVERTISEMENT, 1)
        }
        // ...
        mediaSession.setMetadata(build())
    }
}

Java

public static final String EXTRA_METADATA_ADVERTISEMENT =
    "android.media.metadata.ADVERTISEMENT";

@Override
public void onPlayFromMediaId(String mediaId, Bundle extras) {
    MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();
    // ...
    if (isAd(mediaId)) {
        builder.putLong(EXTRA_METADATA_ADVERTISEMENT, 1);
    }
    // ...
    mediaSession.setMetadata(builder.build());
}

Cómo controlar los errores generales

Cuando la app experimenta un error, debes establecer el estado de reproducción en STATE_ERROR y proporcionar un mensaje de error con el método setErrorMessage(). Los mensajes de error se deben mostrar al usuario y se deben localizar según la configuración regional actual del usuario. Android Auto y el SO Android Automotive pueden mostrar el mensaje de error al usuario.

Para obtener más información sobre los estados de error, consulta Cómo trabajar con una sesión multimedia: estados y errores.

Si un usuario de Android Auto necesita abrir la app del teléfono para resolver un error, el mensaje deberá proporcionarle esa información. Por ejemplo, el mensaje de error deberá decir "Acceder a [nombre de tu app]" en lugar de "Acceder".

Otros recursos