Tornar os apps da TV pesquisáveis

O Android TV usa a interface de pesquisa para recuperar dados de conteúdo de apps instalados e exibir os resultados da pesquisa para o usuário. Os dados de conteúdo do seu app podem ser incluídos nesses resultados para oferecer ao usuário acesso instantâneo ao conteúdo do seu app.

Seu app precisa fornecer o Android TV com os campos de dados a partir dos quais gera resultados de pesquisa sugeridos à medida que o usuário insere caracteres na caixa de diálogo de pesquisa. Para fazer isso, o app precisa implementar um Provedor de conteúdo que ofereça sugestões, além de um arquivo de configuração searchable.xml que descreva o provedor de conteúdo e outras informações importantes para o Android TV. Você também precisa de uma atividade que processe o intent que é disparado quando o usuário seleciona um resultado de pesquisa sugerido. Tudo isso é descrito em mais detalhes em Como adicionar sugestões personalizadas. Aqui estão descritos os principais pontos para apps para o Android TV.

Esta lição baseia-se no seu conhecimento sobre o uso da pesquisa no Android para mostrar como tornar seu app pesquisável no Android TV. Verifique se você está familiarizado com os conceitos explicados no Guia da Search API antes de acompanhar esta lição. Consulte também o treinamento Como adicionar a funcionalidade de pesquisa.

Esta discussão descreve parte do código do app de amostra Android Leanback no repositório GitHub do Android TV (link em inglês).

Identificar colunas

O SearchManager descreve os campos de dados esperados, representando-os como colunas de um banco de dados local. Seja qual for o formato dos seus dados, você precisa mapear seus campos de dados para essas colunas, geralmente na classe que acessa seus dados de conteúdo. Para saber mais sobre como criar uma classe que mapeia seus dados existentes para os campos obrigatórios, consulte Criar uma tabela de sugestões.

A classe SearchManager inclui várias colunas para o Android TV. Algumas das mais importantes estão descritas abaixo.

Valor Descrição
SUGGEST_COLUMN_TEXT_1 O nome do seu conteúdo (obrigatório)
SUGGEST_COLUMN_TEXT_2 Descrição de texto do seu conteúdo
SUGGEST_COLUMN_RESULT_CARD_IMAGE Uma imagem, um pôster ou uma capa para seu conteúdo
SUGGEST_COLUMN_CONTENT_TYPE O tipo MIME da sua mídia
SUGGEST_COLUMN_VIDEO_WIDTH A largura da resolução da sua mídia
SUGGEST_COLUMN_VIDEO_HEIGHT A altura da resolução da sua mídia
SUGGEST_COLUMN_PRODUCTION_YEAR O ano de produção do seu conteúdo (obrigatório)
SUGGEST_COLUMN_DURATION A duração, em milissegundos, da sua mídia (obrigatório)

O framework de pesquisa requer as seguintes colunas:

Quando os valores dessas colunas do seu conteúdo correspondem aos valores do mesmo conteúdo de outros provedores encontrados pelos servidores do Google, o sistema oferece um link direto para seu app na visualização de detalhes do conteúdo, além de links para apps de outros provedores. Isso é discutido com mais detalhes na seção Link direto para seu app na tela de detalhes abaixo.

A classe do banco de dados do seu aplicativo pode definir as colunas da seguinte maneira:

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

Ao criar o mapa das colunas SearchManager para seus campos de dados, você também precisa especificar o _ID para atribuir a cada linha um código exclusivo.

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;
      }
    ...
    

No exemplo acima, observe o mapeamento para o campo SUGGEST_COLUMN_INTENT_DATA_ID. Essa é a parte do URI que aponta para o conteúdo exclusivo dos dados nessa linha, ou seja, a última parte do URI que descreve onde o conteúdo está armazenado. A primeira parte do URI, quando é comum a todas as linhas da tabela, é configurada no arquivo searchable.xml como o atributo android:searchSuggestIntentData, conforme descrito na seção Processar sugestões de pesquisa abaixo.

Se a primeira parte do URI for diferente para cada linha da tabela, você precisará mapear esse valor com o campo SUGGEST_COLUMN_INTENT_DATA. Quando o usuário seleciona esse conteúdo, o intent disparado fornece os dados de intent a partir da combinação do SUGGEST_COLUMN_INTENT_DATA_ID e do atributo android:searchSuggestIntentData ou do valor do campo SUGGEST_COLUMN_INTENT_DATA.

Fornecer dados para sugestões de pesquisa

Implemente um provedor de conteúdo para retornar sugestões de termos de pesquisa à caixa de diálogo de pesquisa do Android TV. O sistema consulta o provedor de conteúdo em busca de sugestões chamando o método query() sempre que uma letra é digitada. Na implementação de query(), o provedor de conteúdo pesquisa seus dados de sugestões e retorna um Cursor que aponta para as linhas designadas para as sugestões.

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);
    }
    ...
    

No arquivo de manifesto, o provedor de conteúdo recebe tratamento especial. Em vez de ser marcado como uma atividade, ele é descrito como um <provider>. O provedor inclui o atributo android:authorities para informar ao sistema o namespace do provedor de conteúdo. Além disso, você precisa configurar o atributo android:exported como "true" para que a pesquisa global do Android possa usar os resultados retornados.

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

Processar sugestões de pesquisa

Seu app precisa incluir um arquivo res/xml/searchable.xml para definir as configurações de sugestões de pesquisa. Ele inclui o atributo android:searchSuggestAuthority para informar ao sistema o namespace do seu provedor de conteúdo. Ele precisa corresponder ao valor da string especificada no atributo android:authorities do elemento <provider> do seu arquivo AndroidManifest.xml.

Seu app precisa incluir um rótulo, que é o nome do aplicativo. As configurações de pesquisa do sistema usam esse rótulo ao enumerar apps pesquisáveis.

O arquivo searchable.xml também precisa incluir android:searchSuggestIntentAction com o valor "android.intent.action.VIEW" para definir a ação de intent que oferecerá uma sugestão personalizada. Ela é diferente da ação de intent que fornece um termo de pesquisa, explicada abaixo. Consulte também Declarar a ação de intent para conhecer outras maneiras de declarar a ação de intent para sugestões.

Com a ação de intent, seu app precisa fornecer os dados de intent especificados com o atributo android:searchSuggestIntentData. Essa é a primeira parte do URI que aponta para o conteúdo. Ela descreve a parte do URI comum a todas as linhas na tabela de mapeamento para esse conteúdo. A parte do URI que é exclusiva para cada linha é estabelecida com o campo SUGGEST_COLUMN_INTENT_DATA_ID, conforme descrito acima em Identificar colunas. Consulte também Declarar dados de intent para conhecer outras maneiras de declarar os dados de intent para sugestões.

Além disso, observe o atributo android:searchSuggestSelection=" ?", que especifica o valor transmitido como o parâmetro selection do método query(), em que o valor de ponto de interrogação (?) é substituído pelo texto da consulta.

Finalmente, você também precisa incluir o atributo android:includeInGlobalSearch com o valor "true". Veja o exemplo de um arquivo 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>
    

Processar termos de pesquisa

Assim que a caixa de diálogo de pesquisa tiver uma palavra correspondente ao valor de uma das colunas do seu app (descritas em Identificar colunas acima), o sistema acionará o intent ACTION_SEARCH. A atividade no seu app que processa esse intent procura no repositório colunas que tenham a palavra especificada nos valores delas e retorna uma lista de itens de conteúdo com essas colunas. No arquivo AndroidManifest.xml, designe a atividade que processa o intent ACTION_SEARCH desta forma:

    ...
      <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" />
    ...
    

A atividade também precisa descrever a configuração pesquisável com uma referência ao arquivo searchable.xml. Para usar a caixa de diálogo de pesquisa global, o manifesto precisa descrever qual atividade receberá as consultas de pesquisa. O manifesto também precisa descrever o elemento <provider>, exatamente como descrito no arquivo searchable.xml.

Link direto para seu app na tela de detalhes

Se você tiver definido a configuração de pesquisa conforme descrito em Processar sugestões de pesquisa e mapeado os campos SUGGEST_COLUMN_TEXT_1, SUGGEST_COLUMN_PRODUCTION_YEAR e SUGGEST_COLUMN_DURATION, conforme descrito em Identificar colunas, um link direto a uma ação de assistir aparecerá para seu conteúdo na tela de detalhes exibida quando o usuário seleciona um resultado de pesquisa, como mostrado na figura 1.

Link direto na tela de detalhes

Figura 1. A tela de detalhes exibe um link direto para o app de exemplo Vídeos do Google (Leanback). Sintel: © copyright Blender Foundation, www.sintel.org.

Quando o usuário seleciona o link para seu app, identificado pelo botão "Disponível em" na tela de detalhes, o sistema inicia a atividade que processa a ACTION_VIEW (configurada como android:searchSuggestIntentAction com o valor "android.intent.action.VIEW" no arquivo searchable.xml).

Você também pode configurar um intent personalizado para iniciar sua atividade. Isso é demonstrado no app de amostra Android Leanback no repositório GitHub do Android TV (link em inglês). Observe que o app de amostra inicia o próprio LeanbackDetailsFragment para mostrar os detalhes da mídia selecionada, mas você precisa iniciar a atividade que reproduz a mídia imediatamente para que o usuário economize um ou dois cliques.

Comportamento de pesquisa

A pesquisa está disponível no Android TV na tela inicial e dentro do seu app. Os resultados da pesquisa são diferentes para esses dois casos.

Pesquisar na tela inicial

Quando você pesquisa na tela inicial, o primeiro resultado aparece em um card de entidade. Se houver apps que possam reproduzir conteúdo, um link para cada um deles aparecerá na parte inferior do card.

Reprodução de resultados da pesquisa de TV

Não é possível colocar um app programaticamente no card de entidade. Para ser incluído como uma opção de reprodução, os resultados da pesquisa de um app precisam corresponder ao título, ano e duração do conteúdo. Por exemplo, o app de amostra leanback-assistant (link em inglês) retorna resultados para "Big Buck Bunny" que incluem o leanback-assistant como uma opção para assistir o filme.

Mais resultados de pesquisa podem estar disponíveis abaixo do card. Para vê-los, o usuário precisa pressionar o controle remoto e rolar para baixo. Os resultados para cada app aparecem em uma linha separada. Não é possível controlar a ordem das linhas. Apps compatíveis com ações de assistir são listados primeiro.

Resultados da pesquisa de TV

Pesquisar dentro do app

Um usuário pode iniciar uma pesquisa no seu app ativando o microfone pelo controle remoto ou controlador de gamepad. Os resultados da pesquisa são exibidos em uma única linha na parte superior do conteúdo do app. Seu app gera resultados da pesquisa usando o próprio provedor de pesquisa global.

Resultados da pesquisa no app de TV

Saiba mais

Para saber mais sobre como pesquisar um app de TV, leia Visão geral de pesquisa e Adicionar funcionalidade de pesquisa.

Para saber mais sobre como personalizar a experiência de pesquisa no app com um `SearchFragment`, leia Pesquisar em apps de TV.

Veja também os apps de amostra Android Leanback e leanback-assistant.