使 TV 应用可供搜索

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

您的应用必须为 Android TV 提供相应的数据字段,当用户在搜索对话框中输入字符时,应用会根据这些字段生成建议的搜索结果。为此,您的应用必须实现一个用于提供建议的内容提供程序,以及一个描述 Android TV 的 content provider 和其他重要信息的 searchable.xml 配置文件。您还需要一个 activity 来处理用户选择建议的搜索结果时触发的 intent。所有这些内容在添加自定义建议中进行了更详细的介绍。本文介绍了 Android TV 应用的要点。

本课以您已掌握的 Android 搜索功能知识为基础,向您介绍如何让您的应用可在 Android TV 中搜索到。在开始学习本课之前,请确保您熟悉 Search API 指南中介绍的概念。 另请参阅有关添加搜索功能的培训。

这里讨论的是 Android TV GitHub 代码库中 Android 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>。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 文件来配置搜索建议设置。它包含 android:searchSuggestAuthority 属性,用于将 content provider 的命名空间告知系统。此字符串必须与您在 AndroidManifest.xml 文件中 <provider> 元素的 android:authorities 属性中指定的字符串值匹配。

您的应用必须包含一个标签,这是应用的名称。系统搜索设置在枚举可搜索应用时会使用此标签。

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

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

另请注意,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 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 字段,那么当用户选择搜索结果时,系统会启动详情屏幕中会显示一条指向您内容的观看操作的 深层链接,如图 1 所示。

详情屏幕中的深层链接

图 1. 详情屏幕显示了 Videos by Google (Leanback) 示例应用的深层链接。Sintel: © copyright Blender Foundation, www.sintel.org.

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

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

搜索行为

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

在主屏幕上搜索

从主屏幕搜索时,第一个结果出现在实体卡中。如果有可以播放内容的应用,卡片底部会显示每个应用的链接。

TV 搜索结果播放

您无法通过编程方式将应用放入实体卡内。应用的搜索结果必须与内容的标题、年份和时长匹配,才能作为播放选项包含在内。

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

电视搜索结果

在应用中搜索

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

TV 应用内搜索结果

了解详情

如需详细了解如何搜索 TV 应用,请阅读搜索概览添加搜索功能

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