Tornar os apps da TV pesquisáveis

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

O app precisa fornecer ao Android TV os campos de dados que podem ser usados para gerar sugestões de pesquisa quando o usuário digita caracteres na caixa de diálogo de pesquisa. Para isso, seu app precisa implementar uma Provedor de conteúdo que veicula as sugestões junto com um searchable.xml que descreve o conteúdo de nuvem e outras informações vitais para o Android TV. Você também precisa de uma atividade que lide com que é acionada quando o usuário seleciona um resultado de pesquisa sugerido. Para mais detalhes, consulte Adicionar sugestões de pesquisa personalizadas. Este guia aborda os principais pontos específicos dos apps para Android TV.

Antes de ler este guia, familiarize-se com os conceitos explicados nas Guia da API Search. Além disso, consulte Adicionar a funcionalidade de pesquisa.

O exemplo de código neste guia vem da App de amostra do Leanback ,

Identificar colunas

O SearchManager descreve os campos de dados esperados, representando-os como colunas de um banco de dados local. Independentemente do formato de seus dados, você deve mapear seus campos de dados para essas colunas, geralmente na classe que acessa seus dados de conteúdo. Para informações sobre como criar uma classe que mapeia seus dados existentes para os campos obrigatórios, consulte Como criar uma tabela de sugestões.

A classe SearchManager inclui várias colunas para o Android TV. Algumas colunas mais importantes estão descritas na tabela a seguir.

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 milésimos de segundo da sua mídia (obrigatório)

O framework de pesquisa requer as seguintes colunas:

Quando os valores dessas colunas do seu conteúdo corresponderem aos valores do mesmo conteúdo de outras provedores encontrados pelos servidores do Google, o sistema fornece uma link direto para seu app nos detalhes visualizar o conteúdo, além de links para apps de outros fornecedores. Isso é discutido com mais detalhes a seção Link direto para seu app na tela de detalhes.

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 os campos de dados, você também precisa especificar _ID para atribuir um ID exclusivo a cada linha.

Kotlin

companion object {
    ....
    private fun buildColumnMap(): MapS<tring, 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 anterior, observe o mapeamento para o SUGGEST_COLUMN_INTENT_DATA_ID . Esta é a parte do URI que aponta para o conteúdo exclusivo para os dados neste linha: a última parte do URI, descrevendo onde o conteúdo está armazenado. A primeira parte do URI, quando for comum a todas as linhas da tabela, está definida no searchable.xml como o android:searchSuggestIntentData, conforme descrito nos Seção Gerenciar sugestões de pesquisa.

Se a primeira parte do URI for diferente para cada linha no tabela, mapeie esse valor com o campo SUGGEST_COLUMN_INTENT_DATA. Quando o usuário seleciona esse conteúdo, a intent acionada fornece os dados de intent do combinação de SUGGEST_COLUMN_INTENT_DATA_ID e o atributo android:searchSuggestIntentData ou o Valor do campo SUGGEST_COLUMN_INTENT_DATA.

Fornecer dados para sugestões de pesquisa

Implementar 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 seu conteúdo provedor para receber sugestões chamando o método query() todas as vezes em que uma letra é digitada. Na sua implementação de query(), seu conteúdo provedor pesquisa seus dados de sugestão e retorna um Cursor que aponta para as linhas que você designou para 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> A o provedor inclui o atributo android:authorities para informar ao sistema namespace do seu provedor de conteúdo. Além disso, você precisa definir o atributo android:exported dele 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 aplicativo deve incluir um res/xml/searchable.xml para definir as configurações de sugestões de pesquisa.

No arquivo res/xml/searchable.xml, inclua android:searchSuggestAuthority para informar ao sistema o namespace do provedor de conteúdo. Ele deve corresponder ao valor de string que você especificar na android:authorities atributo do <provider> no seu arquivo AndroidManifest.xml.

Inclua também um rótulo, que é o nome do aplicativo. As configurações da pesquisa do sistema usam esse rótulo ao enumerar apps pesquisáveis.

O arquivo searchable.xml também deve incluir o android:searchSuggestIntentAction com o valor "android.intent.action.VIEW". para definir a ação da intent que vai fornecer uma sugestão personalizada. Isso é diferente da intent ação para fornecer um termo de pesquisa, conforme descrito na seção a seguir. Para outras maneiras de declarar a ação de intent para sugestões, consulte Declarar o ação da intent.

Junto com a ação da intent, seu app precisa fornecer os dados da intent, que você especifica com a android:searchSuggestIntentData. Essa é a primeira parte do URI que aponta ao conteúdo, que descreve a parte do URI comum a todas as linhas na tabela de mapeamento para essa conteúdo. A parte do URI exclusiva para cada linha é estabelecida com o campo SUGGEST_COLUMN_INTENT_DATA_ID, conforme descrito na seção Identificar colunas. Para ver outras maneiras de declarar os dados de intent para sugestões, consulte Declaração os dados de intent.

O atributo android:searchSuggestSelection=" ?" especifica o valor transmitido como o parâmetro selection do objeto query() . O valor do ponto de interrogação (?) é substituído pelo texto da consulta.

Por fim, você também deve incluir o parâmetro android:includeInGlobalSearch com o valor "true". Aqui está um exemplo 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 que corresponda ao valor em uma das colunas do seu app, como descritos na seção Identificar colunas, o sistema dispara o ACTION_SEARCH. A atividade no app que processa esse A intent procura no repositório colunas com a palavra determinada nos valores e retorna uma lista de itens de conteúdo nessas colunas. No arquivo AndroidManifest.xml, você designa atividade que processa o ACTION_SEARCH como mostrado no exemplo a seguir:

...
  <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 searchable.xml. Para usar a caixa de diálogo de pesquisa global, faça o seguinte: o manifesto precisa descrever qual atividade receberá as consultas de pesquisa. O manifesto também precisa descrever o <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 no artigo Processar pesquisas sugestões e mapeou as SUGGEST_COLUMN_TEXT_1, SUGGEST_COLUMN_PRODUCTION_YEAR e SUGGEST_COLUMN_DURATION, conforme descrito nas a seção Identificar colunas, um link direto para uma ação de assistir para seu conteúdo aparece na tela de detalhes que é iniciada quando o usuário selecionar um resultado da pesquisa:

Link direto na tela de detalhes

Quando o usuário seleciona o link para seu app, identificado pelo botão **Available On** na tela de detalhes, o sistema inicia a atividade que processa o ACTION_VIEW definir como android:searchSuggestIntentAction com o valor "android.intent.action.VIEW" em o arquivo searchable.xml.

Também é possível configurar uma intent personalizada para iniciar sua atividade. Isso é demonstrado App de amostra do Leanback , O app de exemplo inicia a própria LeanbackDetailsFragment para Mostrar os detalhes da mídia selecionada nos seus apps, inicie a atividade que abre a mídia imediatamente para poupar ao usuário um ou dois cliques.

Comportamento de pesquisa

A pesquisa está disponível no Android TV na tela inicial e no seu app. Resultados da pesquisa são diferentes nesses dois casos.

Pesquisar na tela inicial

Quando o usuário faz uma pesquisa na tela inicial, o primeiro resultado aparece em um card de entidade. Se houver apps que podem reproduzir o conteúdo, um link para cada um deles aparece 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 opção de reprodução, os resultados da pesquisa de um app devem corresponder ao título, ano e duração da conteúdo pesquisado.

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

Resultados da pesquisa de TV

Pesquisar dentro do app

O usuário também pode iniciar uma pesquisa no app iniciando 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 em um app de TV, leia Integrar recursos de pesquisa do Android ao seu app e Adicione a funcionalidade de pesquisa.

Para mais informações sobre como personalizar a experiência de pesquisa no app com uma SearchFragment, leia Pesquisar em apps de TV.