使 TV 应用可供搜索

Android TV 使用 Android 搜索界面从已安装应用中检索内容数据,并向用户提供搜索结果。您的应用的内容数据可以包含在这些结果中,以便用户能够即时访问应用中的内容。

应用必须为 Android TV 提供数据字段,当用户在搜索对话框中输入字符时,Android TV 可以从这些字段生成建议的搜索结果。为此,您的应用必须实现一个提供建议的 Content Provider,以及一个用于描述 Content Provider 和其他 Android TV 重要信息的 searchable.xml 配置文件。您还需要一个 activity 来处理用户选择建议的搜索结果时触发的 intent。如需了解详情,请参阅添加自定义搜索建议。本指南介绍了针对 Android TV 应用的要点。

在阅读本指南之前,请确保您熟悉 Search API 指南中介绍的概念。此外,请参阅添加搜索功能

本指南中的示例代码来自 Leanback 示例应用

标识列

SearchManager 通过将预期数据字段表示为本地数据库的列来描述这些字段。无论数据的格式如何,您都必须将数据字段映射到这些列(通常在访问内容数据的类中)。如需了解如何构建一个将现有数据映射到所需字段的类,请参阅 构建建议表

SearchManager 类包含多个 Android TV 列。下表介绍了一些较为重要的列。

说明
SUGGEST_COLUMN_TEXT_1 内容的名称(必填)
SUGGEST_COLUMN_TEXT_2 内容的文本说明
SUGGEST_COLUMN_RESULT_CARD_IMAGE 内容的图片、海报或封面
SUGGEST_COLUMN_CONTENT_TYPE 媒体的 MIME 类型
SUGGEST_COLUMN_VIDEO_WIDTH 媒体的分辨率宽度
SUGGEST_COLUMN_VIDEO_HEIGHT 媒体的分辨率高度
SUGGEST_COLUMN_PRODUCTION_YEAR 内容的制作年份(必需)
SUGGEST_COLUMN_DURATION 媒体的时长(以毫秒为单位)(必填)

搜索框架需要以下列:

当您的内容的这些列的值与 Google 服务器发现的其他提供商的相同内容的值匹配时,系统会在内容的详细信息视图中提供指向您应用的深层链接,以及指向其他提供商的应用的链接。有关详情,请参阅详情屏幕中指向应用的深层链接部分。

应用的数据库类可能会按如下所示定义列:

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

构建从 SearchManager 列到数据字段的映射时,您还必须指定 _ID,以便为每一行分配一个唯一 ID。

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

在前面的示例中,请注意到 SUGGEST_COLUMN_INTENT_DATA_ID 字段的映射。这是 URI 中指向对应行中数据独有内容的部分,即 URI 的最后一部分,用于描述内容的存储位置。当 URI 的第一部分对表中的所有行通用时,该部分在 searchable.xml 文件中设置为 android:searchSuggestIntentData 属性,如处理搜索建议部分中所述。

如果 URI 的第一部分对于表中每一行都是不同的,请使用 SUGGEST_COLUMN_INTENT_DATA 字段映射该值。当用户选择此内容时,触发的 intent 会提供 SUGGEST_COLUMN_INTENT_DATA_IDandroid:searchSuggestIntentData 属性或 SUGGEST_COLUMN_INTENT_DATA 字段值的组合中的 intent 数据。

提供搜索建议数据

实现内容提供程序,以将搜索字词建议返回到 Android TV 搜索对话框。每次输入字母时,系统都会通过调用 query() 方法向 content provider 查询建议。在您的 query() 实现中,content provider 会搜索您的建议数据,并返回 Cursor,它指向您指定为建议的行。

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

在您的清单文件中,内容提供程序会受到特殊对待。它不会被标记为 activity,而是被描述为 <provider>。提供程序包含 android:authorities 属性,用于将 content provider 的命名空间告知系统。此外,必须将其 android:exported 属性设置为 "true",以便 Android 全局搜索可以使用从中返回的结果。

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

处理搜索建议

您的应用必须包含一个 res/xml/searchable.xml 文件,才能配置搜索建议设置。

res/xml/searchable.xml 文件中,添加 android:searchSuggestAuthority 属性,以告知系统 content provider 的命名空间。此值必须与您在 AndroidManifest.xml 文件 <provider> 元素的 android:authorities 属性中指定的字符串值一致。

此外,还要添加标签,这是应用的名称。系统搜索设置在枚举可搜索的应用时会使用此标签。

searchable.xml 文件还必须包含值为 "android.intent.action.VIEW" android:searchSuggestIntentAction,以定义用于提供自定义建议的 intent 操作。这与用于提供搜索字词的 intent 操作不同,如以下部分所述。如需了解为建议声明 intent 操作的其他方式,请参阅声明 intent 操作

应用还必须随 intent 操作一起提供 intent 数据,您可以使用 android:searchSuggestIntentData 属性指定该数据。这是 URI 中指向内容的第一部分,用于描述该内容的映射表中所有行共有的 URI 部分。URI 中每行的唯一部分通过 SUGGEST_COLUMN_INTENT_DATA_ID 字段建立,如标识列部分中所述。如需了解声明 intent 数据以提供建议的其他方式,请参阅声明 intent 数据

android:searchSuggestSelection=" ?" 属性指定作为 query() 方法的 selection 参数传递的值。问号 (?) 值会替换为查询文本。

最后,您还必须添加值为 "true" android:includeInGlobalSearch 属性。以下是一个 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>

处理搜索字词

只要搜索对话框中包含的字词与应用的某个列中的值匹配(如标识列部分中所述),系统就会立即触发 ACTION_SEARCH intent。应用中处理该 intent 的 activity 会在代码库中搜索值中包含指定字词的列,并返回包含这些列的内容项列表。在 AndroidManifest.xml 文件中,您可以指定用于处理 ACTION_SEARCH intent 的 activity,如以下示例所示:

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

此外,activity 还必须描述引用了 searchable.xml 文件的可搜索配置。如需使用全局搜索对话框,清单必须描述哪个 activity 应接收搜索查询。清单还必须完全按照 searchable.xml 文件中的说明描述 <provider> 元素。

详情屏幕中指向应用的深层链接

如果您已按照处理搜索建议部分中所述设置了搜索配置,并映射了 SUGGEST_COLUMN_TEXT_1SUGGEST_COLUMN_PRODUCTION_YEARSUGGEST_COLUMN_DURATION 字段(如标识列部分中所述),当用户选择搜索结果时,系统会启动一个指向内容观看操作的 深层链接

详情屏幕中的深层链接

当用户选择您的应用的链接(由详情屏幕上的 **Available On** 按钮标识)时,系统会启动相应 activity,用于处理设为 android:searchSuggestIntentAction 且在 searchable.xml 文件中的值 "android.intent.action.VIEW"ACTION_VIEW

您还可以设置自定义 intent 来启动您的 activity。这在 Leanback 示例应用中进行了演示。请注意,示例应用会启动自己的 LeanbackDetailsFragment 以显示所选媒体的详细信息;在您的应用中,启动立即播放媒体的 activity 可以让用户再点击一两次。

搜索行为

在 Android TV 中,您可以从主屏幕和应用内部使用搜索功能。这两种情况下的搜索结果有所不同。

在主屏幕上搜索

当用户从主屏幕进行搜索时,第一条结果会显示在实体卡片中。如果存在能够播放相应内容的应用,卡片底部会显示各个应用的链接:

TV 搜索结果播放

您无法以编程方式将应用放入实体卡中。若要以播放选项的形式包含在内,应用的搜索结果必须与搜索内容的标题、年份和时长相匹配。

卡片下方可能会显示更多搜索结果。如需查看它们,用户必须按下遥控器并向下滚动。每个应用的结果会显示在单独的一行中。您无法控制行排序。首先列出支持观看操作的应用。

电视搜索结果

在应用中搜索

用户还可以通过从遥控器或游戏手柄控制器启动麦克风,从应用中启动搜索。搜索结果会显示在应用内容顶部的一行中。您的应用会使用自己的全局搜索提供程序生成搜索结果。

TV 应用内搜索结果

了解详情

如需详细了解如何搜索 TV 应用,请阅读将 Android 搜索功能集成到您的应用中添加搜索功能

如需详细了解如何使用 SearchFragment 自定义应用内搜索体验,请阅读在 TV 应用内搜索