Android TV 會使用 Android 搜尋介面從已安裝的應用程式中擷取內容資料,並向使用者提供搜尋結果。只要將應用程式的內容資料納入這些結果,即可讓使用者立即存取應用程式中的內容。
您的應用程式必須為 Android TV 提供資料欄位,當使用者在搜尋對話方塊中輸入字元時,Android TV 就可以產生建議搜尋結果。為此,應用程式必須實作提供建議的內容供應器,以及
searchable.xml
設定檔,用於說明內容供應器和其他有關 Android TV 的重要資訊。您還需要一個活動,處理使用者選取建議搜尋結果時觸發的意圖。詳情請參閱「新增自訂搜尋建議」。本指南涵蓋 Android TV 應用程式特有的重點。
閱讀本指南之前,請務必熟悉 Search 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_ID
與 android: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
屬性中指定的字串值。
也請加入標籤,也就是應用程式的名稱。系統搜尋設定會在列舉可搜尋的應用程式時使用這個標籤。
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
檔案的參照。如要使用全域搜尋對話方塊,資訊清單必須說明應接收搜尋查詢的活動。資訊清單也必須描述 <provider>
元素,與 searchable.xml
檔案中描述的內容完全一致。
詳細資料畫面中的應用程式深層連結
如果您已按照「處理搜尋建議」一節的說明完成搜尋設定,並對應「識別資料欄」一節所述的 SUGGEST_COLUMN_TEXT_1
、SUGGEST_COLUMN_PRODUCTION_YEAR
和 SUGGEST_COLUMN_DURATION
欄位,則當使用者選取搜尋結果時,詳細資料畫面就會顯示觀看動作的
深層連結:
當使用者選取應用程式連結 (也就是詳細資料畫面中的「Available On」按鈕) 時,系統會啟動處理 searchable.xml
檔案中值為
android:searchSuggestIntentAction
的 ACTION_VIEW
活動。"android.intent.action.VIEW"
您也可以設定自訂意圖來啟動活動。如
Leanback 範例應用程式所示。請注意,範例應用程式會啟動自己的 LeanbackDetailsFragment
,顯示所選媒體的詳細資料;在應用程式中,立即啟動播放媒體的活動,讓使用者再按一兩次點擊。
搜尋行為
在 Android TV 的主畫面和應用程式中皆可使用搜尋功能。這兩種情況的搜尋結果有所不同。
在主畫面上搜尋
當使用者從主畫面進行搜尋時,第一筆搜尋結果會顯示在實體資訊卡中。如果有應用程式可以播放內容,資訊卡底部就會顯示這些應用程式的連結:
無法透過程式輔助方式將應用程式放入實體資訊卡。應用程式的搜尋結果必須與搜尋內容的標題、年份和時間長度一致,才能納入播放選項。
資訊卡下方可能會提供更多搜尋結果。如要查看這些通知,使用者必須按下遙控器並向下捲動。每個應用程式的結果會自成一列。您無法控制資料列的排序。先列出支援手錶動作的應用程式。
在應用程式中搜尋
使用者也可以透過遙控器或遊戲手把控制器啟動麥克風,從應用程式內開始搜尋。搜尋結果會顯示在應用程式內容頂端,並顯示為一列。您的應用程式是運用自家的全域搜尋引擎產生搜尋結果。
瞭解詳情
如要進一步瞭解如何搜尋 TV 應用程式,請參閱「將 Android 搜尋功能整合至您的應用程式」和「新增搜尋功能」。
如要進一步瞭解如何使用 SearchFragment
自訂應用程式內搜尋體驗,請參閱「在 TV 應用程式中搜尋」。