使 TV 应用可供搜索

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

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

本课程的前提是您已掌握在 Android 中进行搜索的知识,以向您介绍如何让应用在 Android TV 中可供搜索。在开始学习本课程之前,请确保您熟悉搜索 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() 方法以在内容提供程序中查询建议。在 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);
    }
    ...
    

在您的清单文件中,内容提供程序会受到特殊对待。它不会被标记为 Activity,而是被描述为 <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 文件,用于配置搜索建议设置。它包含 android:searchSuggestAuthority 属性,用于将内容提供程序的命名空间告知系统。这必须与您在 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 字段,在用户选择搜索结果时启动的详情屏幕中,将出现一个指向内容的观看操作的深层链接,如图 1 所示。

详情屏幕中的深层链接

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

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

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

搜索行为

可以在 Android TV 的主屏幕上和应用内部进行搜索。这两种情况的搜索结果不同。

在主屏幕上搜索

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

TV 搜索结果播放

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

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

TV 搜索结果

在应用中搜索

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

TV 应用内搜索结果

了解详情

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

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