Make TV apps searchable
Stay organized with collections
Save and categorize content based on your preferences.
Android TV uses the Android search interface to retrieve content data from installed apps and deliver search results to the user. Your app's content data can be included with these results to give the user instant access to the content in your app.
Your app must provide Android TV with the data fields from which Android TV can generate suggested search
results as the user enters characters in the search dialog. To do that, your app must implement a
Content Provider that serves
up the suggestions along with a
searchable.xml configuration file that describes the content
provider and other vital information for Android TV. You also need an activity that handles the
intent that fires when the user selects a suggested search result. For
more detail, see Add
custom search suggestions. This guide covers the main points specific to Android TV apps.
Before reading this guide, make sure you are familiar with the concepts explained in the Search API guide. Also, review Add search functionality.
The sample code in this guide comes from the Leanback sample app .
Identify columns
The SearchManager describes the data fields it expects by representing them as
columns of a local database. Regardless of your data's format, you must map your data fields to
these columns, usually in the class that accessess your content data. For information about building
a class that maps your existing data to the required fields, see
Building a suggestion table.
The SearchManager class includes several columns for Android TV. Some of the
more important columns are described in the following table.
| Value | Description |
|---|---|
SUGGEST_COLUMN_TEXT_1 |
The name of your content (required) |
SUGGEST_COLUMN_TEXT_2 |
A text description of your content |
SUGGEST_COLUMN_RESULT_CARD_IMAGE |
An image, poster, or cover for your content |
SUGGEST_COLUMN_CONTENT_TYPE |
The MIME type of your media |
SUGGEST_COLUMN_VIDEO_WIDTH |
The resolution width of your media |
SUGGEST_COLUMN_VIDEO_HEIGHT |
The resolution height of your media |
SUGGEST_COLUMN_PRODUCTION_YEAR |
The production year of your content (required) |
SUGGEST_COLUMN_DURATION |
The duration in milliseconds of your media (required) |
The search framework requires the following columns:
When the values of these columns for your content match the values for the same content from other providers found by Google servers, the system provides a deep link to your app in the details view for the content, along with links to the apps of other providers. This is discussed more in the Deep link to your app in the details screen section.
Your application's database class might define the columns as follows:
Kotlin
classVideoDatabase{ companionobject{ // The columns we'll include in the video database table valKEY_NAME=SearchManager.SUGGEST_COLUMN_TEXT_1 valKEY_DESCRIPTION=SearchManager.SUGGEST_COLUMN_TEXT_2 valKEY_ICON=SearchManager.SUGGEST_COLUMN_RESULT_CARD_IMAGE valKEY_DATA_TYPE=SearchManager.SUGGEST_COLUMN_CONTENT_TYPE valKEY_IS_LIVE=SearchManager.SUGGEST_COLUMN_IS_LIVE valKEY_VIDEO_WIDTH=SearchManager.SUGGEST_COLUMN_VIDEO_WIDTH valKEY_VIDEO_HEIGHT=SearchManager.SUGGEST_COLUMN_VIDEO_HEIGHT valKEY_AUDIO_CHANNEL_CONFIG=SearchManager.SUGGEST_COLUMN_AUDIO_CHANNEL_CONFIG valKEY_PURCHASE_PRICE=SearchManager.SUGGEST_COLUMN_PURCHASE_PRICE valKEY_RENTAL_PRICE=SearchManager.SUGGEST_COLUMN_RENTAL_PRICE valKEY_RATING_STYLE=SearchManager.SUGGEST_COLUMN_RATING_STYLE valKEY_RATING_SCORE=SearchManager.SUGGEST_COLUMN_RATING_SCORE valKEY_PRODUCTION_YEAR=SearchManager.SUGGEST_COLUMN_PRODUCTION_YEAR valKEY_COLUMN_DURATION=SearchManager.SUGGEST_COLUMN_DURATION valKEY_ACTION=SearchManager.SUGGEST_COLUMN_INTENT_ACTION ... } ... }
Java
publicclass VideoDatabase{ // The columns we'll include in the video database table publicstaticfinalStringKEY_NAME=SearchManager.SUGGEST_COLUMN_TEXT_1; publicstaticfinalStringKEY_DESCRIPTION=SearchManager.SUGGEST_COLUMN_TEXT_2; publicstaticfinalStringKEY_ICON=SearchManager.SUGGEST_COLUMN_RESULT_CARD_IMAGE; publicstaticfinalStringKEY_DATA_TYPE=SearchManager.SUGGEST_COLUMN_CONTENT_TYPE; publicstaticfinalStringKEY_IS_LIVE=SearchManager.SUGGEST_COLUMN_IS_LIVE; publicstaticfinalStringKEY_VIDEO_WIDTH=SearchManager.SUGGEST_COLUMN_VIDEO_WIDTH; publicstaticfinalStringKEY_VIDEO_HEIGHT=SearchManager.SUGGEST_COLUMN_VIDEO_HEIGHT; publicstaticfinalStringKEY_AUDIO_CHANNEL_CONFIG= SearchManager.SUGGEST_COLUMN_AUDIO_CHANNEL_CONFIG; publicstaticfinalStringKEY_PURCHASE_PRICE=SearchManager.SUGGEST_COLUMN_PURCHASE_PRICE; publicstaticfinalStringKEY_RENTAL_PRICE=SearchManager.SUGGEST_COLUMN_RENTAL_PRICE; publicstaticfinalStringKEY_RATING_STYLE=SearchManager.SUGGEST_COLUMN_RATING_STYLE; publicstaticfinalStringKEY_RATING_SCORE=SearchManager.SUGGEST_COLUMN_RATING_SCORE; publicstaticfinalStringKEY_PRODUCTION_YEAR=SearchManager.SUGGEST_COLUMN_PRODUCTION_YEAR; publicstaticfinalStringKEY_COLUMN_DURATION=SearchManager.SUGGEST_COLUMN_DURATION; publicstaticfinalStringKEY_ACTION=SearchManager.SUGGEST_COLUMN_INTENT_ACTION; ...
When you build the map from the SearchManager columns to your data fields, you
must also specify the _ID to give each row a unique ID.
Kotlin
companionobject{ .... privatefunbuildColumnMap():Map<String,String>{ returnmapOf( KEY_NAMEtoKEY_NAME, KEY_DESCRIPTIONtoKEY_DESCRIPTION, KEY_ICONtoKEY_ICON, KEY_DATA_TYPEtoKEY_DATA_TYPE, KEY_IS_LIVEtoKEY_IS_LIVE, KEY_VIDEO_WIDTHtoKEY_VIDEO_WIDTH, KEY_VIDEO_HEIGHTtoKEY_VIDEO_HEIGHT, KEY_AUDIO_CHANNEL_CONFIGtoKEY_AUDIO_CHANNEL_CONFIG, KEY_PURCHASE_PRICEtoKEY_PURCHASE_PRICE, KEY_RENTAL_PRICEtoKEY_RENTAL_PRICE, KEY_RATING_STYLEtoKEY_RATING_STYLE, KEY_RATING_SCOREtoKEY_RATING_SCORE, KEY_PRODUCTION_YEARtoKEY_PRODUCTION_YEAR, KEY_COLUMN_DURATIONtoKEY_COLUMN_DURATION, KEY_ACTIONtoKEY_ACTION, BaseColumns._IDto("rowid AS "+BaseColumns._ID), SearchManager.SUGGEST_COLUMN_INTENT_DATA_IDto("rowid AS "+SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID), SearchManager.SUGGEST_COLUMN_SHORTCUT_IDto("rowid AS "+SearchManager.SUGGEST_COLUMN_SHORTCUT_ID) ) } }
Java
... privatestaticHashMap<String,String>buildColumnMap(){ HashMap<String,String>map=newHashMap<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); returnmap; } ...
In the previous example, notice the mapping to the SUGGEST_COLUMN_INTENT_DATA_ID
field. This is the portion of the URI that points to the content unique to the data in this
row—the last part of the URI, describing where the content is stored. The first part of the URI,
when it is common to all the rows in the table, is set in the
searchable.xml file as the
android:searchSuggestIntentData attribute, as described in the
Handle search suggestions section.
If the first part of the URI is different for each row in the
table, map that value with the SUGGEST_COLUMN_INTENT_DATA field.
When the user selects this content, the intent that fires provides the intent data from the
combination of the SUGGEST_COLUMN_INTENT_DATA_ID
and either the android:searchSuggestIntentData attribute or the
SUGGEST_COLUMN_INTENT_DATA field value.
Provide search suggestion data
Implement a Content Provider
to return search term suggestions to the Android TV search dialog. The system queries your content
provider for suggestions by calling the query() method each time
a letter is typed. In your implementation of query(), your content
provider searches your suggestion data and returns a Cursor that points to
the rows you have designated for suggestions.
Kotlin
funquery(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){ throwIllegalArgumentException( "selectionArgs must be provided for the Uri: $uri") } returngetSuggestions(selectionArgs[0]) } else->throwIllegalArgumentException("Unknown Uri: $uri") } } privatefungetSuggestions(query:String):Cursor{ valcolumns=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 ) returnvideoDatabase.getWordMatch(query.toLowerCase(),columns) }
Java
@Override publicCursorquery(Uriuri,String[]projection,Stringselection,String[]selectionArgs, StringsortOrder){ // Use the UriMatcher to see what kind of query we have and format the db query accordingly switch(URI_MATCHER.match(uri)){ caseSEARCH_SUGGEST: Log.d(TAG,"search suggest: "+selectionArgs[0]+" URI: "+uri); if(selectionArgs==null){ thrownewIllegalArgumentException( "selectionArgs must be provided for the Uri: "+uri); } returngetSuggestions(selectionArgs[0]); default: thrownewIllegalArgumentException("Unknown Uri: "+uri); } } privateCursorgetSuggestions(Stringquery){ query=query.toLowerCase(); String[]columns=newString[]{ 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 }; returnvideoDatabase.getWordMatch(query,columns); } ...
In your manifest file, the content provider receives special treatment. Rather than being
tagged as an activity, it is described as a
<provider>. The
provider includes the android:authorities attribute to tell the system the
namespace of your content provider. Also, you must set its android:exported attribute to
"true" so that the Android global search can use the results returned from it.
<providerandroid:name="com.example.android.tvleanback.VideoContentProvider" android:authorities="com.example.android.tvleanback" android:exported="true"/>
Handle search suggestions
Your app must include a
res/xml/searchable.xml file to configure the search suggestions settings.
In the res/xml/searchable.xml file, include
the
android:searchSuggestAuthority attribute to tell the system the namespace of your
content provider. This must match the string value you specify in the
android:authorities
attribute of the <provider>
element in your AndroidManifest.xml file.
Also include a label, which is the name of the application. The system search settings use this label when enumerating searchable apps.
The searchable.xml file
must also include the
android:searchSuggestIntentAction with the value "android.intent.action.VIEW"
to define the intent action for providing a custom suggestion. This is different from the intent
action for providing a search term, as described in the following section.
For other ways to declare the intent action for suggestions,
see Declaring the
intent action.
Along with the intent action, your app must provide the intent data, which you specify with the
android:searchSuggestIntentData attribute. This is the first part of the URI that points
to the content, which describes the portion of the URI common to all rows in the mapping table for that
content. The portion of the URI that is unique to each row is established with the SUGGEST_COLUMN_INTENT_DATA_ID field,
as described in the Identify columns section.
For other ways to declare the intent data for suggestions, see
Declaring
the intent data.
The android:searchSuggestSelection=" ?" attribute specifies the value passed
as the selection parameter of the query()
method. The question mark (?) value is replaced with the query text.
Finally, you must also include the
android:includeInGlobalSearch attribute with the value "true". Here is an example
searchable.xml file:
<searchablexmlns: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>
Handle search terms
As soon as the search dialog has a word that matches the value in one of your app's columns, as
described in the Identify columns section, the system fires the
ACTION_SEARCH intent.
The activity in your app that handles that
intent searches the repository for columns with the given word in their values and returns a list
of content items with those columns. In your AndroidManifest.xml file, you designate the
activity which handles the ACTION_SEARCH
intent as shown in the following example:
... <activity android:name="com.example.android.tvleanback.DetailsActivity" android:exported="true"> <!--Receivesthesearchrequest.--> <intent-filter> <actionandroid:name="android.intent.action.SEARCH"/> <!--Nocategoryneeded,becausetheIntentwillspecifythisclasscomponent--> </intent-filter> <!--Pointstosearchablemetadata.--> <meta-dataandroid:name="android.app.searchable" android:resource="@xml/searchable"/> </activity> ... <!--Providessearchsuggestionsforkeywordsagainstvideometadata.--> <providerandroid:name="com.example.android.tvleanback.VideoContentProvider" android:authorities="com.example.android.tvleanback" android:exported="true"/> ...
The activity must also describe the searchable configuration with a reference to the
searchable.xml file.
To use the global search dialog,
the manifest must describe which activity should receive search queries. The manifest must also
describe the <provider>
element, exactly as it is described in the searchable.xml file.
Deep link to your app in the details screen
If you have set up the search configuration as described in the Handle search
suggestions section and mapped the SUGGEST_COLUMN_TEXT_1 ,
SUGGEST_COLUMN_PRODUCTION_YEAR , and
SUGGEST_COLUMN_DURATION fields as described in
the Identify columns section, a
deep link to a watch action for your content appears in the details screen that launches when
the user selects a search result:
When the user selects the link for your app, identified by the **Available On** button in the
details screen, the system launches the activity that handles the ACTION_VIEW
set as
android:searchSuggestIntentAction with the value "android.intent.action.VIEW" in
the searchable.xml file.
You can also set up a custom intent to launch your activity. This is demonstrated in the
Leanback sample app
. Note that the sample app launches its own LeanbackDetailsFragment to
show the details for the selected media; in your apps, launch the activity that plays the media
immediately to save the user another click or two.
Search behavior
Search is available in Android TV from the home screen and from inside your app. Search results are different for these two cases.
Search from the home screen
When the user searches from the home screen, the first result appears in an entity card. If there are apps that can play the content, a link to each one appears at the bottom of the card:
You can't programmatically place an app into the entity card. To be included as a playback option, an app's search results must match the title, year, and duration of the searched content.
More search results might be available below the card. To see them, the user must press down on the remote and scroll. The results for each app appear in a separate row. You can't control the row ordering. Apps that support watch actions are listed first.
Search from your app
The user can also start a search from within your app by initiating the microphone from the remote or game pad controller. The search results are displayed in a single row on top of the app's content. Your app generates search results using its own global search provider.
Learn more
To learn more about searching a TV app, read Integrate Android search features into your app and Add search functionality.
For more information on how to customize the in-app search experience with a SearchFragment, read
Search within TV apps.