胡凯 · 更新于 2018-09-26 09:00:42

使TV应用是可被搜索的

编写:awong1900 - 原文:http://developer.android.com/training/tv/discovery/searchable.html

Android TV使用Android搜索接口从安装的应用中检索内容数据并且释放搜索结果给用户。我们的应用内容数据能被包含在这些结果中,去给用户即时访问应用程序中的内容。

我们的应用必须提供Android TV数据字段,它是用户在搜索框中输入字符生成的建议搜索结果。去做这个,我们的应用必须实现Content Provider,在searchable.xml配置文件描述content provider和其他必要的Android TV信息。我们也需要一个activity在用户选择一个建议的搜索结果时处理intent的触发。所有的这些被描述在Adding Custom Suggestions。本文描述Android TV应用搜索的关键点。

这节课展示Android中搜索的知识,展示如何使我们的应用在Android TV里是可被搜索的。确信我们熟悉Search API guide的解释。在下面的这节课程之前,查看Adding Search Functionality训练课程。

这个讨论描述的一些代码,从Android Leanback示例代码摘出。代码可以在Github上找到。

识别列

SearchManager描述了数据字段,它被代表为SQLite数据库的列。不管我们的数据格式,我们必须把我们的数据字段填到那些列,通常用存取我们的内容数据的类。更多信息,查看Building a suggestion table()

SearchManager类为AndroidTV包含了几个列。下面是重要的一些列:

描述
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 媒体的时间长度

搜索framework需要以下的列:

当这些内容的列的值匹配Google服务的providers提供的的值时,系统提供一个深链接到我们的应用,用于详情查看,以及指向应用的其他Providers的链接。更多讨论在在详情页显示内容

我们的应用的数据库类可能定义以下的列:

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。

...
  private static HashMap buildColumnMap() {
    HashMap map = new HashMap();
    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属性。属性被描述在Handle Search Suggestions

如果URI的第一部分是不同于表格的每一列,我们填充SUGGEST_COLUMN_INTENT_DATA字段的值。当用户选择这个内容时,这个intent被启动依据SUGGEST_COLUMN_INTENT_DATA_ID的混合intent数据或者android:searchSuggestIntentData属性和SUGGEST_COLUMN_INTENT_DATA字段值之一。

提供搜索建议数据

实现一个Content Provider去返回搜索术语建议到AndroidTV搜索框。系统需要我们的内容容器提供建议,通过调用每次一个字母类型[query()](http://developer.android.com/reference/android/content/ContentProvider.html#query(android.net.Uri, java.lang.String[], java.lang.String, java.lang.String[], java.lang.String))方法。在[query()](http://developer.android.com/reference/android/content/ContentProvider.html#query(android.net.Uri, java.lang.String[], java.lang.String, java.lang.String[], java.lang.String))的实现中,我们的内容容器搜索我们的建议数据并且返回一个光标指向我们已经指定的建议列。

@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 mVideoDatabase.getWordMatch(query, columns);
  }
...

在我们的manifest文件中,内容容器接受特殊处理。相比被标记为一个activity,它是被描述为<provider>。provider包括android:searchSuggestAuthority属性去告诉系统我们的内容容器的名字空间。并且,我们必须设置它的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 action。这与提供一个搜索术语的intent action不同,下面解释。查看Declaring the intent action 用另一种方式去定义建议的intent action。

同intent action一起,我们的应用必须提供我们定义的android:searchSuggestIntentData属性的intent数据。这是指向内容的URI的第一部分。它描述在填充的内容表格中URI所有共同列的部分。URI的独一无二的部分用 SUGGEST_COLUMN_INTENT_DATA_ID字段建立每一列,以上被描述在识别列。查看Declaring the intent data用另一种方式去定义建议的intent数据。

并且,注意android:searchSuggestSelection="?"属性为特定的值。这个值作为[query()](http://developer.android.com/reference/android/content/ContentProvider.html#query(android.net.Uri, java.lang.String[], java.lang.String, java.lang.String[], java.lang.String))方法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。我们应用的activity处理intent搜索列的给定的字段资源,并且返回一个那些内容项的列表。在我们的AndroidManifest.xml文件中,我们指定的activity处理ACTION_SEARCH intent,像这样:

...
  <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文件描述可搜索的设置。用全局搜索框,manifest必须描述activity应该收到的搜索请求。manifest必须描述<provider>元素,详细被描述在searchable.xml文件。

深链接到应用的详情页

如果我们有设置处理搜索建议描述的搜索配置和填充 SUGGEST_COLUMN_TEXT_1SUGGEST_COLUMN_CONTENT_TYPESUGGEST_COLUMN_PRODUCTION_YEAR字段到识别列,一个深链接去查看详情页的内容。当用户选择一个搜索结果时,详情页将打开。如图1。

deep-link

图1 详情页显示一个深链接为Google(Leanback)的视频代码。Sintel: © copyright Blender Foundation, www.sintel.org.

当用户选择我们的应用链接,“Available On”按钮被标识在详情页,系统启动activity处理ACTION_VIEW(在searchable.xml文件设置android:searchSuggestIntentAction值为"android.intent.action.VIEW")。

我们也能设置用户intent去启动我们的activity,这个在在AndroidLeanback示例代码应用中演示。注意示例应用启动它自己的LeanbackDetailsFragment去显示被选择媒体的详情,但是我们应该启动activity去播放媒体。立即去保存用户的另一次或两次点击。


下一节: 使TV应用是可被搜索的 >