使电视应用可搜索

Android TV 使用 Android 搜索界面 从已安装的应用中检索内容数据,并将搜索结果交付给用户。您的应用的内容数据可以包含在这些结果中,以便用户可以立即访问您应用中的内容。

您的应用必须向 Android TV 提供数据字段,Android TV 可以使用这些字段在用户在搜索对话框中输入字符时生成建议的搜索结果。为此,您的应用必须实现一个 内容提供程序 来提供建议,以及一个 searchable.xml 配置文件,该配置文件描述内容提供程序和其他对 Android TV 至关重要的信息。您还需要一个活动来处理当用户选择建议的搜索结果时触发的意图。有关更多详细信息,请参阅 添加自定义搜索建议。本指南涵盖了 Android TV 应用特有的主要要点。

在阅读本指南之前,请确保您熟悉 搜索 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 字段映射该值。当用户选择此内容时,触发的意图将提供来自 SUGGEST_COLUMN_INTENT_DATA_IDandroid:searchSuggestIntentData 属性或 SUGGEST_COLUMN_INTENT_DATA 字段值的组合的意图数据。

提供搜索建议数据

实现一个内容提供商,将搜索词建议返回给 Android TV 搜索对话框。系统每次键入一个字母时都会调用 query() 方法,查询您的内容提供商以获取建议。在您的 query() 实现中,您的内容提供商将搜索您的建议数据并返回一个指向您为建议指定行的 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);
}
...

在您的清单文件中,内容提供商将获得特殊处理。它不是被标记为活动,而是被描述为一个<provider>。提供商包括 android:authorities 属性来告诉系统您的内容提供商的命名空间。此外,您必须将其 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 属性来告诉系统您的内容提供商的命名空间。这必须与您在 AndroidManifest.xml 文件的 <provider> 元素的 android:authorities 属性中指定的字符串值匹配。

还包括一个label,它是应用程序的名称。当系统搜索设置枚举可搜索应用程序时,它将使用此标签。

searchable.xml 文件还必须包含 android:searchSuggestIntentAction,其值为 "android.intent.action.VIEW",用于定义提供自定义建议的意图操作。这与提供搜索词的意图操作不同,如下一节所述。有关声明建议意图操作的其他方法,请参见声明意图操作

除了意图操作外,您的应用还必须提供意图数据,您使用 android:searchSuggestIntentData 属性指定它。这是指向内容的 URI 的第一部分,它描述了映射表中所有行通用的 URI 部分。唯一标识每行的 URI 部分通过 SUGGEST_COLUMN_INTENT_DATA_ID 字段建立,如标识列部分所述。有关声明建议意图数据的其他方法,请参见声明意图数据

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

最后,您还必须包含 android:includeInGlobalSearch 属性,其值为 "true"。这是一个示例 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 意图。您应用中处理该意图的活动将搜索存储库以查找包含给定词的列的值,并返回包含这些列的内容项列表。在您的 AndroidManifest.xml 文件中,您指定处理 ACTION_SEARCH 意图的活动,如以下示例所示

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

该活动还必须使用对 searchable.xml 文件的引用描述可搜索配置。要使用全局搜索对话框,清单必须描述哪个活动应接收搜索查询。清单还必须描述 <provider> 元素,与在 searchable.xml 文件中描述的一样。

详细信息屏幕中指向您的应用的深层链接

如果您已按照处理搜索建议部分的说明设置了搜索配置,并按照标识列部分的说明映射了 SUGGEST_COLUMN_TEXT_1SUGGEST_COLUMN_PRODUCTION_YEARSUGGEST_COLUMN_DURATION 字段,则在用户选择搜索结果时启动的详细信息屏幕中将显示一个指向您内容的观看操作的深层链接

Deep link in the details screen

当用户选择您的应用的链接时,该链接由详细信息屏幕中的“可用”按钮标识,系统将启动处理 ACTION_VIEW 的活动的,该活动被设置为 android:searchSuggestIntentAction,其值为 "android.intent.action.VIEW"searchable.xml 文件中。

您也可以设置一个自定义意图来启动您的活动。这在 Leanback 示例应用中有所体现。请注意,示例应用程序启动它自己的 LeanbackDetailsFragment 来显示所选媒体的详细信息;在您的应用程序中,立即启动播放媒体的活动,以节省用户一次或两次点击。

搜索行为

搜索可在 Android TV 的主屏幕和应用程序内部使用。这两种情况下,搜索结果会有所不同。

从主屏幕搜索

当用户从主屏幕搜索时,第一个结果将出现在实体卡片中。如果有可以播放该内容的应用程序,则卡片底部会出现指向每个应用程序的链接

TV Search Result Playback

您无法以编程方式将应用程序放置到实体卡片中。要作为播放选项包含在内,应用程序的搜索结果必须与搜索内容的标题、年份和持续时间匹配。

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

TV Search Results

从您的应用程序搜索

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

TV In-app Search Results

了解更多

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

有关如何使用 SearchFragment 自定义应用程序内搜索体验的更多信息,请阅读TV 应用内的搜索