| page.title=Making TV Apps Searchable |
| page.tags="search","searchable" |
| |
| trainingnavtop=true |
| |
| @jd:body |
| |
| <div id="tb-wrapper"> |
| <div id="tb"> |
| <h2>This lesson teaches you to</h2> |
| <ol> |
| <li><a href="#columns">Identify Columns</a></li> |
| <li><a href="#provide">Provide Search Suggestion Data</a></li> |
| <li><a href="#suggestions">Handle Search Suggestions</a></li> |
| <li><a href="#terms">Handle Search Terms</a></li> |
| <li><a href="#details">Deep Link to Your App in the Details Screen</a></li> |
| </ol> |
| <h2>You should also read</h2> |
| <ul> |
| <li><a href="{@docRoot}guide/topics/search/index.html">Search</a></li> |
| <li><a href="{@docRoot}training/search/index.html">Adding Search Functionality</a></li> |
| </ul> |
| <h2>Try it out</h2> |
| <ul> |
| <li><a class="external-link" href="https://github.com/googlesamples/androidtv-Leanback">Android Leanback sample app</a></li> |
| </ul> |
| </div> |
| </div> |
| |
| <p>Android TV uses the Android <a href="{@docRoot}guide/topics/search/index.html">search interface</a> |
| 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.</p> |
| |
| <p>Your app must provide Android TV with the data fields from which it generates suggested search |
| results as the user enters characters in the search dialog. To do that, your app must implement a |
| <a href="{@docRoot}guide/topics/providers/content-providers.html">Content Provider</a> that serves |
| up the suggestions along with a <a href="{@docRoot}guide/topics/search/searchable-config.html"> |
| {@code searchable.xml}</a> 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. All of this is described in |
| more detail in <a href="{@docRoot}guide/topics/search/adding-custom-suggestions.html">Adding Custom |
| Suggestions</a>. Here are described the main points for Android TV apps.</p> |
| |
| <p>This lesson builds on your knowledge of using search in Android to show you how to make your app |
| searchable in Android TV. Be sure you are familiar with the concepts explained in the |
| <a href="{@docRoot}guide/topics/search/index.html">Search API guide</a> before following this lesson. |
| See also the training <a href="{@docRoot}training/search/index.html">Adding Search Functionality</a>.</p> |
| |
| <p>This discussion describes some code from the |
| <a class="external-link" href="https://github.com/googlesamples/androidtv-Leanback">Android Leanback sample app</a>, |
| available on GitHub.</p> |
| |
| <h2 id="columns">Identify Columns</h2> |
| |
| <p>The {@link android.app.SearchManager} describes the data fields it expects by representing them as |
| columns of an SQLite 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 |
| <a href="{@docRoot}guide/topics/search/adding-custom-suggestions.html#SuggestionTable"> |
| Building a suggestion table</a>.</p> |
| |
| <p>The {@link android.app.SearchManager} class includes several columns for Android TV. Some of the |
| more important columns are described below.</p> |
| |
| <table> |
| <tr> |
| <th>Value</th> |
| <th>Description</th> |
| </tr><tr> |
| <td>{@code SUGGEST_COLUMN_TEXT_1}</td> |
| <td>The name of your content <strong>(required)</strong></td> |
| </tr><tr> |
| <td>{@code SUGGEST_COLUMN_TEXT_2}</td> |
| <td>A text description of your content</td> |
| </tr><tr> |
| <td>{@code SUGGEST_COLUMN_RESULT_CARD_IMAGE}</td> |
| <td>An image/poster/cover for your content</td> |
| </tr><tr> |
| <td>{@code SUGGEST_COLUMN_CONTENT_TYPE}</td> |
| <td>The MIME type of your media <strong>(required)</strong></td> |
| </tr><tr> |
| <td>{@code SUGGEST_COLUMN_VIDEO_WIDTH}</td> |
| <td>The resolution width of your media</td> |
| </tr><tr> |
| <td>{@code SUGGEST_COLUMN_VIDEO_HEIGHT}</td> |
| <td>The resolution height of your media</td> |
| </tr><tr> |
| <td>{@code SUGGEST_COLUMN_PRODUCTION_YEAR}</td> |
| <td>The production year of your content <strong>(required)</strong></td> |
| </tr><tr> |
| <td>{@code SUGGEST_COLUMN_DURATION}</td> |
| <td>The duration in milliseconds of your media</td> |
| </tr> |
| </table> |
| |
| <p>The search framework requires the following columns:</p> |
| <ul> |
| <li>{@link android.app.SearchManager#SUGGEST_COLUMN_TEXT_1}</li> |
| <li>{@link android.app.SearchManager#SUGGEST_COLUMN_CONTENT_TYPE}</li> |
| <li>{@link android.app.SearchManager#SUGGEST_COLUMN_PRODUCTION_YEAR}</li> |
| </ul> |
| |
| <p>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 |
| <a href="{@docRoot}training/app-indexing/deep-linking.html">deep link</a> to your app in the details |
| view for the content, along with links to the apps of other providers. This is discussed more in |
| <a href="#details">Display Content in the Details Screen</a>, below.</p> |
| |
| <p>Your application's database class might define the columns as follows:</p> |
| |
| <p class="code-caption"><a href="https://github.com/googlesamples/androidtv-Leanback/blob/master/app/src/main/java/com/example/android/tvleanback/VideoDatabase.java#L41" target="_blank"> |
| VideoDatabase.java</a></p> |
| <pre> |
| 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; |
| ... |
| </pre> |
| |
| <p>When you build the map from the {@link android.app.SearchManager} columns to your data fields, you |
| must also specify the {@link android.provider.BaseColumns#_ID} to give each row a unique ID.</p> |
| |
| <p class="code-caption"><a href="https://github.com/googlesamples/androidtv-Leanback/blob/master/app/src/main/java/com/example/android/tvleanback/VideoDatabase.java#L83" target="_blank"> |
| VideoDatabase.java</a></p> |
| <pre> |
| ... |
| 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; |
| } |
| ... |
| </pre> |
| |
| <p>In the example above, notice the mapping to the {@link android.app.SearchManager#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 — |
| that is, the last part of the URI describing where the content is stored. The first part of the URI, |
| when it is common to all of the rows in the table, is set in the |
| <a href="{@docRoot}guide/topics/search/searchable-config.html"> {@code searchable.xml}</a> file as the |
| <a href="{@docRoot}guide/topics/search/searchable-config.html#searchSuggestIntentData"> |
| {@code android:searchSuggestIntentData}</a> attribute, as described in |
| <a href="#suggestions">Handle Search Suggestions</a>, below. |
| |
| <p>If the first part of the URI is different for each row in the |
| table, you map that value with the {@link android.app.SearchManager#SUGGEST_COLUMN_INTENT_DATA} field. |
| When the user selects this content, the intent that fires provides the intent data from the |
| combination of the {@link android.app.SearchManager#SUGGEST_COLUMN_INTENT_DATA_ID} |
| and either the {@code android:searchSuggestIntentData} attribute or the |
| {@link android.app.SearchManager#SUGGEST_COLUMN_INTENT_DATA} field value.</p> |
| |
| <h2 id="provide">Provide Search Suggestion Data</h2> |
| |
| <p>Implement a <a href="{@docRoot}guide/topics/providers/content-providers.html">Content Provider</a> |
| to return search term suggestions to the Android TV search dialog. The system queries your content |
| provider for suggestions by calling the {@link android.content.ContentProvider#query(android.net.Uri, |
| java.lang.String[], java.lang.String, java.lang.String[], java.lang.String) query()} method each time |
| a letter is typed. In your implementation of {@link android.content.ContentProvider#query(android.net.Uri, |
| java.lang.String[], java.lang.String, java.lang.String[], java.lang.String) query()}, your content |
| provider searches your suggestion data and returns a {@link android.database.Cursor} that points to |
| the rows you have designated for suggestions.</p> |
| |
| <p class="code-caption"><a href="https://github.com/googlesamples/androidtv-Leanback/blob/master/app/src/main/java/com/example/android/tvleanback/VideoContentProvider.java" target="_blank"> |
| VideoContentProvider.java</a></p> |
| <pre> |
| @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); |
| } |
| ... |
| </pre> |
| |
| <p>In your manifest file, the content provider receives special treatment. Rather than getting |
| tagged as an activity, it is described as a |
| <a href="{@docRoot}guide/topics/manifest/provider-element.html">{@code <provider>}</a>. The |
| provider includes the {@code android:searchSuggestAuthority} attribute to tell the system the |
| namespace of your content provider. Also, you must set its {@code android:exported} attribute to |
| {@code "true"} so that the Android global search can use the results returned from it.</p> |
| |
| <p class="code-caption"><a href="https://github.com/googlesamples/androidtv-Leanback/blob/master/app/src/main/AndroidManifest.xml" target="_blank"> |
| AndroidManifest.xml</a></p> |
| <pre> |
| <provider android:name="com.example.android.tvleanback.VideoContentProvider" |
| android:authorities="com.example.android.tvleanback" |
| android:exported="true" /> |
| </pre> |
| |
| <h2 id="suggestions">Handle Search Suggestions</h2> |
| |
| <p>Your app must include a <a href="{@docRoot}guide/topics/search/searchable-config.html"> |
| {@code res/xml/searchable.xml}</a> file to configure the search suggestions settings. It inlcudes |
| the <a href="{@docRoot}guide/topics/search/searchable-config.html#searchSuggestAuthority"> |
| {@code android:searchSuggestAuthority}</a> attribute to tell the system the namespace of your |
| content provider. This must match the string value you specify in the |
| <a href="{@docRoot}guide/topics/manifest/provider-element.html#auth">{@code android:authorities}</a> |
| attribute of the <a href="{@docRoot}guide/topics/manifest/provider-element.html">{@code <provider>} |
| </a> element in your {@code AndroidManifest.xml} file.</p> |
| |
| The <a href="{@docRoot}guide/topics/search/searchable-config.html">{@code searchable.xml}</a> file |
| must also include the <a href="{@docRoot}guide/topics/search/searchable-config.html#searchSuggestIntentAction"> |
| {@code android:searchSuggestIntentAction}</a> with the value {@code "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, explained below. See also, |
| <a href="{@docRoot}guide/topics/search/adding-custom-suggestions.html#IntentAction">Declaring the |
| intent action</a> for other ways to declare the intent action for suggestions.</p> |
| |
| <p>Along with the intent action, your app must provide the intent data, which you specify with the |
| <a href="{@docRoot}guide/topics/search/searchable-config.html#searchSuggestIntentData"> |
| {@code android:searchSuggestIntentData}</a> attribute. This is the first part of the URI that points |
| to the content. It 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 {@link android.app.SearchManager#SUGGEST_COLUMN_INTENT_DATA_ID} field, |
| as described above in <a href="#columns">Identify Columns</a>. See also, |
| <a href="{@docRoot}guide/topics/search/adding-custom-suggestions.html#IntentData"> |
| Declaring the intent data</a> for other ways to declare the intent data for suggestions.</p> |
| |
| <p>Also, note the {@code android:searchSuggestSelection=" ?"} attribute which specifies the value passed |
| as the {@code selection} parameter of the {@link android.content.ContentProvider#query(android.net.Uri, |
| java.lang.String[], java.lang.String, java.lang.String[], java.lang.String) query()} method where the |
| question mark ({@code ?}) value is replaced with the query text.</p> |
| |
| <p>Finally, you must also include the <a href="{@docRoot}guide/topics/search/searchable-config.html#includeInGlobalSearch"> |
| {@code android:includeInGlobalSearch}</a> attribute with the value {@code "true"}. Here is an example |
| <a href="{@docRoot}guide/topics/search/searchable-config.html">{@code searchable.xml}</a> |
| file:</p> |
| |
| <p class="code-caption"><a href="https://github.com/googlesamples/androidtv-Leanback/blob/master/app/src/main/res/xml/searchable.xml" target="_blank"> |
| Searchable.xml</a></p> |
| <pre> |
| <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> |
| </pre> |
| |
| <h2 id="terms">Handle Search Terms</h2> |
| |
| <p>As soon as the search dialog has a word which matches the value in one of your app's columns |
| (described in <a href="#identifying">Identifying Columns</a>, above), the system fires the |
| {@link android.content.Intent#ACTION_SEARCH} intent. The activity in your app which 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 {@code AndroidManifest.xml} file, you designate the |
| activity which handles the {@link android.content.Intent#ACTION_SEARCH} intent like this: |
| |
| <p class="code-caption"><a href="https://github.com/googlesamples/androidtv-Leanback/blob/master/app/src/main/AndroidManifest.xml" target="_blank"> |
| AndroidManifest.xml</a></p> |
| <pre> |
| ... |
| <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" /> |
| ... |
| </pre> |
| |
| <p>The activity must also describe the searchable configuration with a reference to the |
| <a href="{@docRoot}guide/topics/search/searchable-config.html">{@code searchable.xml}</a> file. |
| To <a href="{@docRoot}guide/topics/search/search-dialog.html">use the global search dialog</a>, |
| the manifest must describe which activity should receive search queries. The manifest must also |
| describe the <a href="{@docRoot}guide/topics/manifest/provider-element.html">{@code <provider>} |
| </a>element, exactly as it is described in the <a href="{@docRoot}guide/topics/search/searchable-config.html"> |
| {@code searchable.xml}</a> file.</p> |
| |
| <h2 id="details">Deep Link to Your App in the Details Screen</h2> |
| |
| <p>If you have set up the search configuration as described in <a href="#suggestions">Handle Search |
| Suggestions</a> and mapped the {@link android.app.SearchManager#SUGGEST_COLUMN_TEXT_1}, |
| {@link android.app.SearchManager#SUGGEST_COLUMN_CONTENT_TYPE}, and |
| {@link android.app.SearchManager#SUGGEST_COLUMN_PRODUCTION_YEAR} fields as described in |
| <a href="#columns">Identify Columns</a>, a <a href="{@docRoot}training/app-indexing/deep-linking.html"> |
| deep link</a> to your content appears in the details screen that launches when the user selects a |
| search result.</p> |
| |
| <p>When the user selects the link for your app, identified by the "Available On" button in the |
| details screen, the system launches the activity which handles the {@link android.content.Intent#ACTION_VIEW} |
| (set as <a href="{@docRoot}guide/topics/search/searchable-config.html#searchSuggestIntentAction"> |
| {@code android:searchSuggestIntentAction}</a> with the value {@code "android.intent.action.VIEW"} in |
| the <a href="{@docRoot}guide/topics/search/searchable-config.html">{@code searchable.xml}</a> file).</p> |
| |
| <p>You can also set up a custom intent to launch your activity, and this is demonstrated in the |
| <a class="external-link" href="https://github.com/googlesamples/androidtv-Leanback">Android Leanback |
| sample app</a>. Note that the sample app launches its own <code>LeanbackDetailsFragment</code> to |
| show the details for the selected media, but you should launch the activity that plays the media |
| immediately to save the user another click or two.</p> |
| |
| |
| |
| |
| |
| |