Android TV は、Android 検索インターフェース を使用して、インストール済みのアプリからコンテンツデータを取得し、検索結果をユーザーに配信します。この結果にアプリのコンテンツ データを含めると、ユーザーはアプリ内のコンテンツにすぐにアクセスできます。
アプリは、ユーザーが検索ダイアログに文字を入力したときに検索結果候補を生成するデータ フィールドを Android TV
に提供する必要があります。そのためには、候補を提供する
コンテンツ プロバイダとともに、Android TV 用のコンテンツ
プロバイダおよびその他の重要な情報を含む
searchable.xml 構成ファイルをアプリに実装する必要があります。また、ユーザーが検索結果候補を選択したときに起動されるインテントを処理するアクティビティも必要です。詳しくは、カスタム検索候補を追加するをご覧ください。このガイドでは、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 属性を含めて、コンテンツ プロバイダの名前空間をシステムに知らせます。この値は、
android:authorities
属性が<provider>
要素のAndroidManifest.xmlファイルで指定した文字列値と一致する必要があります。
また、アプリの名前であるラベル を含めます。システムの検索設定では、検索可能なアプリを列挙するときに、このラベルが使用されます。
searchable.xml ファイル
は、値 "android.intent.action.VIEW"
を含む
android:searchSuggestIntentActionもインクルードする必要があります。これにより、カスタム候補を提供するためのインテントのアクションが定義されます。これは、次のセクションで説明するように、検索キーワードを提供するためのインテント
アクションとは異なります。
候補のためのインテントのアクションを宣言するその他の方法については、
インテント アクションの宣言をご覧ください。
アプリは、インテントのアクションとともに、
android:searchSuggestIntentData 属性で指定するインテント データを提供する必要があります。これは、コンテンツを指す URI の最初の部分であり、そのコンテンツのマッピング
テーブルのすべての行に共通する URI の部分を記述します。各行に固有の URI の部分は、SUGGEST_COLUMN_INTENT_DATA_ID フィールドで確定されます。
列を特定するで説明されているように。
候補のためのインテント データを宣言するその他の方法については、
インテント データの宣言
をご覧ください。
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インテントを起動します。
このインテントを処理するアプリ内のアクティビティは、値に特定の単語を含む列をリポジトリで検索し、それらの列を含むコンテンツ
アイテムのリストを返します。次の例に示すように、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**] ボタンで識別されます)をクリックすると、システムは
として設定されたACTION_VIEW
android:searchSuggestIntentActionを処理するアクティビティを値"android.intent.action.VIEW"により
searchable.xmlファイルで起動します。
アクティビティを起動するカスタム インテントを設定することもできます。以下がその例です。
Leanback サンプルアプリ
.このサンプルアプリは、選択されたメディアの詳細を表示するために独自の LeanbackDetailsFragment
を起動します。ただし、メディアをすぐに再生するアクティビティを起動して、ユーザーが 1 ~ 2 回余計にクリックする手間を省く必要があります。
検索行動
Android TV では、ホーム画面とアプリ内から検索を実行できます。この 2 つのケースでは、検索結果の表示方法が異なります。
ホーム画面から検索する
ユーザーがホーム画面から検索すると、最初の結果がエンティティ カードに表示されます。コンテンツを再生できるアプリがあれば、各アプリへのリンクがカードの下部に表示されます。
プログラムでエンティティ カードにアプリを配置することはできません。アプリの検索結果が再生オプションとして含まれるためには、検索されたコンテンツのタイトル、制作年、再生時間と一致する必要があります。
場合によっては、カードの下にさらに検索結果が表示されます。ユーザーがそれらを表示するには、リモコンを押してスクロールする必要があります。各アプリの結果は別々の行に表示されます。行の順序を制御することはできません。 ウォッチアクションをサポートするアプリが最初に表示されます。
アプリから検索する
ユーザーは、リモコンまたはゲームパッド コントローラからマイクを起動して、アプリ内から検索を開始することもできます。検索結果は、アプリのコンテンツの上部に 1 つの行で表示されます。 アプリは、固有のグローバル検索プロバイダを使用して、検索結果を生成します。
詳細
TV アプリの検索の詳細については、 アプリに Android 検索機能を統合するおよび 検索機能を追加するをご覧ください。
SearchFragment を使用してアプリ内検索のエクスペリエンスをカスタマイズする方法については、
TV アプリ内で検索するをご覧ください。