讓 TV 應用程式可供搜尋

Android TV 會使用 Android 搜尋介面從已安裝的應用程式擷取內容資料,並將搜尋結果提供給使用者。這些結果可以納入應用程式內容資料,讓使用者可以立即存取應用程式內容。

您的應用程式必須在搜尋對話方塊中輸入字元時,為 Android TV 提供資料欄位,讓系統產生建議搜尋結果。為此,您的應用程式必須實作內容供應器,除了提供建議,也會實作 searchable.xml 設定檔,用於說明內容供應器和其他 Android TV 重要資訊。您也必須提供活動,用於處理使用者選取建議搜尋結果時觸發的意圖。如需上述所有說明,請參閱「新增自訂建議」。以下說明 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 欄位對應該值。使用者選取這項內容時,觸發的意圖會提供 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 檔案,才能設定搜尋建議設定。其中包含 android:searchSuggestAuthority 屬性,讓系統瞭解內容供應器的命名空間。這必須與您在 AndroidManifest.xml 檔案中 <provider> 元素的 android:authorities 屬性中指定的字串值相符。

您的應用程式必須包含應用程式名稱標籤。系統搜尋設定會在列舉可搜尋的應用程式時使用這個標籤。

searchable.xml 檔案也必須包含具有 "android.intent.action.VIEW" 值的 android:searchSuggestIntentAction,以定義提供自訂建議的意圖動作。這與提供搜尋字詞的意圖動作不同,如下所述。另請參閱「宣告意圖動作」,瞭解為建議宣告意圖動作的其他方式。

除了意圖動作之外,應用程式必須提供您透過 android:searchSuggestIntentData 屬性指定的意圖資料。此為指向內容的 URI 第一個部分。也會說明該內容對應資料表中所有資料列共用的 URI 部分。每個資料列專屬的 URI 部分,是使用 SUGGEST_COLUMN_INTENT_DATA_ID 欄位建立而成,如上方「識別資料欄」一節所述。另請參閱「 宣告意圖資料」,瞭解宣告意圖資料以獲得建議的其他方式。

另請注意,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 意圖。應用程式內的活動會處理該意圖,並在存放區中搜尋含有指定字詞的資料欄,然後傳回含有這些資料欄的內容項目清單。在 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 檔案來說明可供搜尋的設定。如要使用全域搜尋對話方塊,資訊清單必須說明應接收搜尋查詢的活動。資訊清單也必須按照 searchable.xml 檔案中所描述的 <provider> 元素進行說明。

詳細資料畫面中應用程式的深層連結

如果您已按照「處理搜尋建議」一節的說明完成搜尋設定,並已對應 SUGGEST_COLUMN_TEXT_1SUGGEST_COLUMN_PRODUCTION_YEARSUGGEST_COLUMN_DURATION 欄位 (如「識別資料欄」一節所述),則當使用者選取搜尋結果時,詳細資料畫面會顯示內容觀察動作的 深層連結,如圖 1 所示。

詳細資料畫面中的深層連結

圖 1. 詳細資料畫面會顯示 Google Videos by Google (Leanback) 範例應用程式的深層連結。Sintel:© copyright Blender Foundation, www.sintel.org。

當使用者選取應用程式連結 (如詳細資料畫面中的「Available On」按鈕) 時,系統就會啟動處理 ACTION_VIEW 的活動 (設為 android:searchSuggestIntentActionsearchable.xml 檔案中值為 "android.intent.action.VIEW")。

或者,您也可以設定自訂意圖來啟動活動,詳情請參閱 Android TV GitHub 存放區中的 Android Leanback 範例應用程式。請注意,範例應用程式會啟動自己的 LeanbackDetailsFragment,以顯示所選媒體的詳細資料,但您應立即啟動播放媒體的活動,讓使用者再按一或兩次。

搜尋行為

Android TV 可在主畫面和應用程式內搜尋。這兩種情況的搜尋結果有不同。

在主畫面上搜尋

從主畫面進行搜尋時,第一筆搜尋結果會顯示在實體資訊卡中。如果應用程式可播放此內容,資訊卡底部會顯示每個應用程式的連結。

電視搜尋結果播放

您無法透過程式輔助方式將應用程式置入實體資訊卡。應用程式的搜尋結果必須與內容的標題、年份和時間長度相符,才能納入播放選項。

資訊卡下方可能會顯示更多搜尋結果。如要查看這些圖示,使用者必須按下遙控器並向下捲動。每個應用程式的結果會自成一列。您無法控制資料列排序。系統會優先列出支援觀看操作的應用程式。

電視搜尋結果

透過應用程式進行搜尋

使用者可以透過遙控器或遊戲手把控制器啟動麥克風,在應用程式中開始搜尋。搜尋結果會顯示在應用程式內容頂端的單列。您的應用程式會使用自家的全域搜尋引擎產生搜尋結果。

電視應用程式內搜尋結果

瞭解詳情

如要進一步瞭解如何搜尋 TV 應用程式,請參閱「搜尋總覽」和「新增搜尋功能」。

如要進一步瞭解如何使用「SearchFragment」自訂應用程式內搜尋體驗,請參閱「在電視應用程式中搜尋」。