Cómo permitir las búsquedas en apps para TV

Android TV usa la interfaz de búsqueda de Android para recuperar datos de contenido de las apps instaladas y enviar los resultados de la búsqueda al usuario. Es posible incluir los datos del contenido de tu app con estos resultados, a fin de ofrecer al usuario acceso instantáneo al contenido.

Tu app debe proporcionar Android TV con los campos de datos desde los que genera resultados de búsqueda sugerida a medida que el usuario ingresa caracteres en el diálogo de búsqueda. Para esto, tu app debe implementar un proveedor de contenido que realice sugerencias, junto con un archivo de configuración searchable.xml que describa el proveedor de contenido y otra información útil para Android TV. Además, necesitas una actividad que controle el intent que se activa cuando el usuario selecciona un resultado de búsqueda sugerida. Todo esto se describe de forma más detallada en Cómo agregar sugerencias personalizadas. Aquí se describen los puntos principales de las apps de Android TV.

En esta lección, obtendrás información sobre cómo usar la búsqueda en Android para hacer que tu app se pueda buscar en Android TV. Antes de realizar esta lección, asegúrate de familiarizarte con los conceptos que se explican en la guía de API de búsqueda. Consulta también la capacitación Cómo agregar funcionalidad de búsqueda.

En este debate, se describe parte del código de la app de muestra de Android Leanback en el repositorio de GitHub de Android TV.

Cómo identificar columnas

La clase SearchManager describe los campos de datos esperados mediante una representación en columnas de una base de datos local. Independientemente del formato de los datos, debes asignar los campos de datos a estas columnas, por lo general, en la clase que accede a tus datos de contenido. Para obtener más información sobre cómo compilar una clase que asigne tus datos existentes a los campos requeridos, consulta Cómo compilar una tabla de sugerencias.

La clase SearchManager incluye varias columnas para Android TV. Algunas de las más importantes se describen a continuación.

Valor Descripción
SUGGEST_COLUMN_TEXT_1 El nombre del contenido (obligatorio)
SUGGEST_COLUMN_TEXT_2 Una descripción del contenido
SUGGEST_COLUMN_RESULT_CARD_IMAGE Una imagen/póster/carátula para el contenido
SUGGEST_COLUMN_CONTENT_TYPE El tipo de MIME del contenido multimedia
SUGGEST_COLUMN_VIDEO_WIDTH El ancho de la resolución del contenido multimedia
SUGGEST_COLUMN_VIDEO_HEIGHT La altura de la resolución del contenido multimedia
SUGGEST_COLUMN_PRODUCTION_YEAR El año de producción del contenido (obligatorio)
SUGGEST_COLUMN_DURATION La duración en milisegundos del contenido multimedia (obligatorio)

Para el marco de trabajo de búsqueda, se necesitan las siguientes columnas:

Cuando los valores de estas columnas que corresponden a tu contenido coincidan con los valores correspondientes al mismo contenido de otros proveedores que se encuentren en los servidores de Google, el sistema proporcionará un vínculo directo a tu app en la vista de detalles de tu contenido, junto con vínculos a las apps de otros proveedores. Esto se explica a continuación con más detalles, en Vínculo profundo a tu app en la pantalla de detalles.

La clase de la base de datos de tu app puede definir las columnas de la siguiente manera:

Kotlin

    class VideoDatabase {
        companion object {
            // The columns we'll include in the video database table
            val KEY_NAME = SearchManager.SUGGEST_COLUMN_TEXT_1
            val KEY_DESCRIPTION = SearchManager.SUGGEST_COLUMN_TEXT_2
            val KEY_ICON = SearchManager.SUGGEST_COLUMN_RESULT_CARD_IMAGE
            val KEY_DATA_TYPE = SearchManager.SUGGEST_COLUMN_CONTENT_TYPE
            val KEY_IS_LIVE = SearchManager.SUGGEST_COLUMN_IS_LIVE
            val KEY_VIDEO_WIDTH = SearchManager.SUGGEST_COLUMN_VIDEO_WIDTH
            val KEY_VIDEO_HEIGHT = SearchManager.SUGGEST_COLUMN_VIDEO_HEIGHT
            val KEY_AUDIO_CHANNEL_CONFIG = SearchManager.SUGGEST_COLUMN_AUDIO_CHANNEL_CONFIG
            val KEY_PURCHASE_PRICE = SearchManager.SUGGEST_COLUMN_PURCHASE_PRICE
            val KEY_RENTAL_PRICE = SearchManager.SUGGEST_COLUMN_RENTAL_PRICE
            val KEY_RATING_STYLE = SearchManager.SUGGEST_COLUMN_RATING_STYLE
            val KEY_RATING_SCORE = SearchManager.SUGGEST_COLUMN_RATING_SCORE
            val KEY_PRODUCTION_YEAR = SearchManager.SUGGEST_COLUMN_PRODUCTION_YEAR
            val KEY_COLUMN_DURATION = SearchManager.SUGGEST_COLUMN_DURATION
            val KEY_ACTION = SearchManager.SUGGEST_COLUMN_INTENT_ACTION
            ...
        }
        ...
    }
    

Java

    public class VideoDatabase {
        //The columns we'll include in the video database table
        public static final String KEY_NAME = SearchManager.SUGGEST_COLUMN_TEXT_1;
        public static final String KEY_DESCRIPTION = SearchManager.SUGGEST_COLUMN_TEXT_2;
        public static final String KEY_ICON = SearchManager.SUGGEST_COLUMN_RESULT_CARD_IMAGE;
        public static final String KEY_DATA_TYPE = SearchManager.SUGGEST_COLUMN_CONTENT_TYPE;
        public static final String KEY_IS_LIVE = SearchManager.SUGGEST_COLUMN_IS_LIVE;
        public static final String KEY_VIDEO_WIDTH = SearchManager.SUGGEST_COLUMN_VIDEO_WIDTH;
        public static final String KEY_VIDEO_HEIGHT = SearchManager.SUGGEST_COLUMN_VIDEO_HEIGHT;
        public static final String KEY_AUDIO_CHANNEL_CONFIG =
                SearchManager.SUGGEST_COLUMN_AUDIO_CHANNEL_CONFIG;
        public static final String KEY_PURCHASE_PRICE = SearchManager.SUGGEST_COLUMN_PURCHASE_PRICE;
        public static final String KEY_RENTAL_PRICE = SearchManager.SUGGEST_COLUMN_RENTAL_PRICE;
        public static final String KEY_RATING_STYLE = SearchManager.SUGGEST_COLUMN_RATING_STYLE;
        public static final String KEY_RATING_SCORE = SearchManager.SUGGEST_COLUMN_RATING_SCORE;
        public static final String KEY_PRODUCTION_YEAR = SearchManager.SUGGEST_COLUMN_PRODUCTION_YEAR;
        public static final String KEY_COLUMN_DURATION = SearchManager.SUGGEST_COLUMN_DURATION;
        public static final String KEY_ACTION = SearchManager.SUGGEST_COLUMN_INTENT_ACTION;
    ...
    

Cuando compilas la asignación desde las columnas de SearchManager hasta tus campos de datos, también debes especificar el _ID para otorgarle a cada fila un ID único.

Kotlin


    companion object {
        ....

        private fun buildColumnMap(): Map<String, String> {
            return mapOf(
              KEY_NAME to KEY_NAME,
              KEY_DESCRIPTION to KEY_DESCRIPTION,
              KEY_ICON to KEY_ICON,
              KEY_DATA_TYPE to KEY_DATA_TYPE,
              KEY_IS_LIVE to KEY_IS_LIVE,
              KEY_VIDEO_WIDTH to KEY_VIDEO_WIDTH,
              KEY_VIDEO_HEIGHT to KEY_VIDEO_HEIGHT,
              KEY_AUDIO_CHANNEL_CONFIG to KEY_AUDIO_CHANNEL_CONFIG,
              KEY_PURCHASE_PRICE to KEY_PURCHASE_PRICE,
              KEY_RENTAL_PRICE to KEY_RENTAL_PRICE,
              KEY_RATING_STYLE to KEY_RATING_STYLE,
              KEY_RATING_SCORE to KEY_RATING_SCORE,
              KEY_PRODUCTION_YEAR to KEY_PRODUCTION_YEAR,
              KEY_COLUMN_DURATION to KEY_COLUMN_DURATION,
              KEY_ACTION to KEY_ACTION,
              BaseColumns._ID to ("rowid AS " + BaseColumns._ID),
              SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID to ("rowid AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID),
              SearchManager.SUGGEST_COLUMN_SHORTCUT_ID to ("rowid AS " + SearchManager.SUGGEST_COLUMN_SHORTCUT_ID)
            )
        }
    }
    

Java

    ...
      private static HashMap<String, String> buildColumnMap() {
        HashMap<String, String> map = new HashMap<String, String>();
        map.put(KEY_NAME, KEY_NAME);
        map.put(KEY_DESCRIPTION, KEY_DESCRIPTION);
        map.put(KEY_ICON, KEY_ICON);
        map.put(KEY_DATA_TYPE, KEY_DATA_TYPE);
        map.put(KEY_IS_LIVE, KEY_IS_LIVE);
        map.put(KEY_VIDEO_WIDTH, KEY_VIDEO_WIDTH);
        map.put(KEY_VIDEO_HEIGHT, KEY_VIDEO_HEIGHT);
        map.put(KEY_AUDIO_CHANNEL_CONFIG, KEY_AUDIO_CHANNEL_CONFIG);
        map.put(KEY_PURCHASE_PRICE, KEY_PURCHASE_PRICE);
        map.put(KEY_RENTAL_PRICE, KEY_RENTAL_PRICE);
        map.put(KEY_RATING_STYLE, KEY_RATING_STYLE);
        map.put(KEY_RATING_SCORE, KEY_RATING_SCORE);
        map.put(KEY_PRODUCTION_YEAR, KEY_PRODUCTION_YEAR);
        map.put(KEY_COLUMN_DURATION, KEY_COLUMN_DURATION);
        map.put(KEY_ACTION, KEY_ACTION);
        map.put(BaseColumns._ID, "rowid AS " +
                BaseColumns._ID);
        map.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID, "rowid AS " +
                SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID);
        map.put(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID, "rowid AS " +
                SearchManager.SUGGEST_COLUMN_SHORTCUT_ID);
        return map;
      }
    ...
    

En el ejemplo anterior, observa la asignación del campo SUGGEST_COLUMN_INTENT_DATA_ID. Esta es la parte del URI que apunta al contenido que es único de los datos de esta fila; es decir, la última parte del URI, en la que se describe dónde se almacena el contenido. La primera parte del URI, si es común para todas las filas de la tabla, se define en el archivo searchable.xml como el atributo android:searchSuggestIntentData, como se describe a continuación en Cómo controlar sugerencias de búsqueda.

Si la primera parte del URI es diferente para cada fila de la tabla, debes asignar ese valor al campo SUGGEST_COLUMN_INTENT_DATA. Cuando el usuario seleccione este contenido, el intent que se activa proporcionará los datos del intent desde la combinación del SUGGEST_COLUMN_INTENT_DATA_ID y el atributo android:searchSuggestIntentData o el valor del campo SUGGEST_COLUMN_INTENT_DATA.

Cómo proporcionar datos de sugerencias de búsqueda

Implementa un proveedor de contenido que muestre sugerencias de términos de búsqueda en el diálogo de búsqueda de Android TV. El sistema llama al método query() cada vez que se escribe una letra, a fin de consultar al proveedor de contenido en busca de sugerencias. En tu implementación de query(), el proveedor de contenido busca tus datos de sugerencia y muestra un Cursor que apunta a las filas que designaste para sugerencias.

Kotlin

    fun query(uri: Uri, projection: Array<String>, selection: String, selectionArgs: Array<String>,
            sortOrder: String): Cursor {
        // Use the UriMatcher to see what kind of query we have and format the db query accordingly
        when (URI_MATCHER.match(uri)) {
            SEARCH_SUGGEST -> {
                Log.d(TAG, "search suggest: ${selectionArgs[0]} URI: $uri")
                if (selectionArgs == null) {
                    throw IllegalArgumentException(
                            "selectionArgs must be provided for the Uri: $uri")
                }
                return getSuggestions(selectionArgs[0])
            }
            else -> throw IllegalArgumentException("Unknown Uri: $uri")
        }
    }

    private fun getSuggestions(query: String): Cursor {
        val columns = arrayOf<String>(
                BaseColumns._ID,
                VideoDatabase.KEY_NAME,
                VideoDatabase.KEY_DESCRIPTION,
                VideoDatabase.KEY_ICON,
                VideoDatabase.KEY_DATA_TYPE,
                VideoDatabase.KEY_IS_LIVE,
                VideoDatabase.KEY_VIDEO_WIDTH,
                VideoDatabase.KEY_VIDEO_HEIGHT,
                VideoDatabase.KEY_AUDIO_CHANNEL_CONFIG,
                VideoDatabase.KEY_PURCHASE_PRICE,
                VideoDatabase.KEY_RENTAL_PRICE,
                VideoDatabase.KEY_RATING_STYLE,
                VideoDatabase.KEY_RATING_SCORE,
                VideoDatabase.KEY_PRODUCTION_YEAR,
                VideoDatabase.KEY_COLUMN_DURATION,
                VideoDatabase.KEY_ACTION,
                SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID
        )
        return videoDatabase.getWordMatch(query.toLowerCase(), columns)
    }
    

Java

    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
            String sortOrder) {
        // Use the UriMatcher to see what kind of query we have and format the db query accordingly
        switch (URI_MATCHER.match(uri)) {
            case SEARCH_SUGGEST:
                Log.d(TAG, "search suggest: " + selectionArgs[0] + " URI: " + uri);
                if (selectionArgs == null) {
                    throw new IllegalArgumentException(
                            "selectionArgs must be provided for the Uri: " + uri);
                }
                return getSuggestions(selectionArgs[0]);
            default:
                throw new IllegalArgumentException("Unknown Uri: " + uri);
        }
    }

    private Cursor getSuggestions(String query) {
        query = query.toLowerCase();
        String[] columns = new String[]{
            BaseColumns._ID,
            VideoDatabase.KEY_NAME,
            VideoDatabase.KEY_DESCRIPTION,
            VideoDatabase.KEY_ICON,
            VideoDatabase.KEY_DATA_TYPE,
            VideoDatabase.KEY_IS_LIVE,
            VideoDatabase.KEY_VIDEO_WIDTH,
            VideoDatabase.KEY_VIDEO_HEIGHT,
            VideoDatabase.KEY_AUDIO_CHANNEL_CONFIG,
            VideoDatabase.KEY_PURCHASE_PRICE,
            VideoDatabase.KEY_RENTAL_PRICE,
            VideoDatabase.KEY_RATING_STYLE,
            VideoDatabase.KEY_RATING_SCORE,
            VideoDatabase.KEY_PRODUCTION_YEAR,
            VideoDatabase.KEY_COLUMN_DURATION,
            VideoDatabase.KEY_ACTION,
            SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID
        };
        return videoDatabase.getWordMatch(query, columns);
    }
    ...
    

En tu archivo de manifiesto, el proveedor de contenido recibe un tratamiento especial. En lugar de ser etiquetado como una actividad, se lo describe como un <provider>. El proveedor incluye el atributo android:authorities para indicarle al sistema cuál es el espacio de nombres de tu proveedor de contenido. Además, debes establecer su atributo android:exported como "true", de modo que la búsqueda global de Android pueda usar los resultados que se muestran.

    <provider android:name="com.example.android.tvleanback.VideoContentProvider"
        android:authorities="com.example.android.tvleanback"
        android:exported="true" />
    

Cómo controlar sugerencias de búsqueda

Tu app debe incluir un archivo res/xml/searchable.xml para configurar los ajustes de las sugerencias de búsqueda. Este incluye el atributo android:searchSuggestAuthority para indicarle al sistema cuál es el espacio de nombres de tu proveedor de contenido. Debe coincidir con el valor de la string que especificaste en el atributo android:authorities del elemento <provider> del archivo AndroidManifest.xml.

Tu app debe incluir una etiqueta, que es el nombre de la app. La configuración de la búsqueda del sistema usa esta etiqueta cuando enumera las apps en las que se puede buscar.

El archivo searchable.xml también debe incluir la android:searchSuggestIntentAction con el valor "android.intent.action.VIEW" para definir la acción del intent, a fin de proporcionar una sugerencia personalizada. Esta acción es diferente a la que realiza el intent para proporcionar un término de búsqueda, que se explica a continuación. Consulta Cómo declarar la acción del intent para conocer otras maneras de declarar esta acción para sugerencias.

Junto con la acción del intent, tu app debe proporcionar los datos del intent, que debes especificar con el atributo android:searchSuggestIntentData. Esta es la primera parte del URI, que apunta al contenido. Describe la parte del URI que es común a todas las filas de la tabla de asignación de ese contenido. La parte del URI que es única para cada fila se define con el campo SUGGEST_COLUMN_INTENT_DATA_ID, como se describió anteriormente en Cómo identificar columnas. Consulta también Cómo declarar los datos del intent para conocer otras maneras de declarar estos datos para sugerencias.

Además, ten en cuenta el atributo android:searchSuggestSelection=" ?", que especifica el valor pasado al parámetro selection del método query(), donde el valor del signo de pregunta (?) es reemplazado por el texto de la pregunta.

Finalmente, también debes incluir el atributo android:includeInGlobalSearch con el valor "true". El siguiente es un ejemplo del archivo searchable.xml:

    <searchable xmlns:android="http://schemas.android.com/apk/res/android"
        android:label="@string/search_label"
        android:hint="@string/search_hint"
        android:searchSettingsDescription="@string/settings_description"
        android:searchSuggestAuthority="com.example.android.tvleanback"
        android:searchSuggestIntentAction="android.intent.action.VIEW"
        android:searchSuggestIntentData="content://com.example.android.tvleanback/video_database_leanback"
        android:searchSuggestSelection=" ?"
        android:searchSuggestThreshold="1"
        android:includeInGlobalSearch="true">
    </searchable>
    

Cómo controlar términos de búsqueda

Tan pronto como en el diálogo de búsqueda haya una palabra que coincida con el valor de una de las columnas de tu app (que se describieron anteriormente en Cómo identificar columnas), el sistema activará el intent ACTION_SEARCH. La actividad de tu app que controla ese intent busca en el repositorio columnas con la palabra determinada en sus valores, y muestra una lista de elementos de contenido con esas columnas. En el archivo AndroidManifest.xml, debes designar la actividad que controla el intent ACTION_SEARCH de la siguiente manera:

    ...
      <activity
          android:name="com.example.android.tvleanback.DetailsActivity"
          android:exported="true">

          <!-- Receives the search request. -->
          <intent-filter>
              <action android:name="android.intent.action.SEARCH" />
              <!-- No category needed, because the Intent will specify this class component -->
          </intent-filter>

          <!-- Points to searchable meta data. -->
          <meta-data android:name="android.app.searchable"
              android:resource="@xml/searchable" />
      </activity>
    ...
      <!-- Provides search suggestions for keywords against video meta data. -->
      <provider android:name="com.example.android.tvleanback.VideoContentProvider"
          android:authorities="com.example.android.tvleanback"
          android:exported="true" />
    ...
    

Además, la actividad debe describir la configuración que se puede buscar con una referencia al archivo searchable.xml. Para usar el diálogo de búsqueda global, el manifiesto debe describir qué actividad debería recibir consultas de búsqueda. El manifiesto también debe describir el elemento <provider>, tal como se describe en el archivo searchable.xml.

Vínculo directo a tu app en la pantalla de detalles

Si estableciste la configuración de búsqueda como se describe en Cómo controlar sugerencias de búsqueda y asignaste los campos SUGGEST_COLUMN_TEXT_1, SUGGEST_COLUMN_PRODUCTION_YEAR y SUGGEST_COLUMN_DURATION como se describe en Cómo identificar columnas, aparecerá un vínculo directo a una acción de reproducción de tu contenido en la pantalla de detalles, que se inicia cuando el usuario selecciona un resultado de búsqueda, como se muestra en la figura 1.

Vínculo directo en la pantalla de detalles

Figura 1: En la pantalla de detalles, se muestra un vínculo directo para la app de ejemplo Videos by Google (Leanback) Sintel: © copyright Blender Foundation, www.sintel.org.

Cuando el usuario selecciona el vínculo a tu app, al que puede identificar con el botón "Disponible en" en la pantalla de detalles, el sistema inicia la actividad que controla la ACTION_VIEW (definida como android:searchSuggestIntentAction con el valor "android.intent.action.VIEW" en el archivo searchable.xml).

También puede configurar un intent personalizado para que inicie tu actividad, lo que se demuestra en la app de ejemplo de Android Leanback, en el repositorio de GitHub de Android TV. Ten en cuenta que la app de ejemplo inicia su propio LeanbackDetailsFragment para mostrar los detalles del contenido seleccionado, pero deberías iniciar la actividad que reproduce el contenido de manera inmediata para evitar que el usuario tenga que hacer clic una o dos veces más.

Comportamiento de búsqueda

En Android TV, la búsqueda está disponible en la pantalla principal y dentro de tu app. En cada caso, los resultados de las búsquedas son diferentes.

Búsqueda desde la pantalla principal

Cuando buscas desde la pantalla principal, el primer resultado aparece en una tarjeta de entidad. Si hay apps que puedan reproducir el contenido, aparecerá un vínculo a cada una en la parte inferior de la tarjeta.

Reproducción de resultados de búsqueda de TV

No puedes colocar de manera programática una app en la tarjeta de entidad. Para poder ser incluidos como una opción de reproducción, los resultados de búsqueda de una app deben coincidir con el título, el año y la duración del contenido. Por ejemplo, la app de ejemplo del leanback-assistant muestra resultados para 'Big Buck Bunny' que incluyen leanback-assistant como opción para ver la película.

Es posible que haya más resultados de búsqueda disponibles debajo de la tarjeta. Para verlos, el usuario debe mantener presionado el control remoto y desplazarse hacia abajo. Los resultados de cada app aparecen en una fila separada. No es posible controlar el orden de las filas. En primer lugar, se enumeran las apps que admiten acciones de reproducción.

Resultados de búsqueda de TV

Búsqueda desde tu app

El usuario puede realizar una búsqueda desde dentro de tu app, iniciando el micrófono desde el control remoto o un controlador para juegos. Los resultados de la búsqueda se muestran en una sola fila, encima del contenido de la app. Tu app genera resultados de búsqueda usando su propio proveedor de búsqueda global.

Resultados de búsqueda dentro de apps para TV

Más información

Para obtener más información sobre cómo realizar búsquedas en una app para TV, consulta Descripción general de la búsqueda y Cómo agregar funcionalidad de búsqueda.

Para obtener más información sobre cómo personalizar la experiencia de búsqueda dentro de la con un `SearchFragment`, consulta Cómo realizar búsquedas en apps para TV.

También puedes echar un vistazo a las apps de ejemplo Android Leanback y leanback-assistant.