Home | History | Annotate | Download | only in discovery
      1 page.title=Making TV Apps Searchable
      2 page.tags="search","searchable"
      3 
      4 trainingnavtop=true
      5 
      6 @jd:body
      7 
      8 <div id="tb-wrapper">
      9 <div id="tb">
     10   <h2>This lesson teaches you to</h2>
     11   <ol>
     12     <li><a href="#columns">Identify Columns</a></li>
     13     <li><a href="#provide">Provide Search Suggestion Data</a></li>
     14     <li><a href="#suggestions">Handle Search Suggestions</a></li>
     15     <li><a href="#terms">Handle Search Terms</a></li>
     16     <li><a href="#details">Deep Link to Your App in the Details Screen</a></li>
     17   </ol>
     18   <h2>You should also read</h2>
     19   <ul>
     20     <li><a href="{@docRoot}guide/topics/search/index.html">Search</a></li>
     21     <li><a href="{@docRoot}training/search/index.html">Adding Search Functionality</a></li>
     22   </ul>
     23   <h2>Try it out</h2>
     24   <ul>
     25     <li><a class="external-link" href="https://github.com/googlesamples/androidtv-Leanback">Android Leanback sample app</a></li>
     26   </ul>
     27 </div>
     28 </div>
     29 
     30 <p>Android TV uses the Android <a href="{@docRoot}guide/topics/search/index.html">search interface</a>
     31 to retrieve content data from installed apps and deliver search results to the user. Your app's
     32 content data can be included with these results, to give the user instant access to the content in
     33 your app.</p>
     34 
     35 <p>Your app must provide Android TV with the data fields from which it generates suggested search
     36 results as the user enters characters in the search dialog. To do that, your app must implement a
     37 <a href="{@docRoot}guide/topics/providers/content-providers.html">Content Provider</a> that serves
     38 up the suggestions along with a <a href="{@docRoot}guide/topics/search/searchable-config.html">
     39 {@code searchable.xml}</a> configuration file that describes the content
     40 provider and other vital information for Android TV. You also need an activity that handles the
     41 intent that fires when the user selects a suggested search result. All of this is described in
     42 more detail in <a href="{@docRoot}guide/topics/search/adding-custom-suggestions.html">Adding Custom
     43 Suggestions</a>. Here are described the main points for Android TV apps.</p>
     44 
     45 <p>This lesson builds on your knowledge of using search in Android to show you how to make your app
     46 searchable in Android TV. Be sure you are familiar with the concepts explained in the
     47 <a href="{@docRoot}guide/topics/search/index.html">Search API guide</a> before following this lesson.
     48 See also the training <a href="{@docRoot}training/search/index.html">Adding Search Functionality</a>.</p>
     49 
     50 <p>This discussion describes some code from the
     51 <a class="external-link" href="https://github.com/googlesamples/androidtv-Leanback">Android Leanback sample app</a>,
     52 available on GitHub.</p>
     53 
     54 <h2 id="columns">Identify Columns</h2>
     55 
     56 <p>The {@link android.app.SearchManager} describes the data fields it expects by representing them as
     57 columns of an SQLite database. Regardless of your data's format, you must map your data fields to
     58 these columns, usually in the class that accessess your content data. For information about building
     59 a class that maps your existing data to the required fields, see
     60 <a href="{@docRoot}guide/topics/search/adding-custom-suggestions.html#SuggestionTable">
     61 Building a suggestion table</a>.</p>
     62 
     63 <p>The {@link android.app.SearchManager} class includes several columns for Android TV. Some of the
     64 more important columns are described below.</p>
     65 
     66 <table>
     67 <tr>
     68    <th>Value</th>
     69    <th>Description</th>
     70 </tr><tr>
     71    <td>{@code SUGGEST_COLUMN_TEXT_1}</td>
     72    <td>The name of your content <strong>(required)</strong></td>
     73 </tr><tr>
     74    <td>{@code SUGGEST_COLUMN_TEXT_2}</td>
     75    <td>A text description of your content</td>
     76 </tr><tr>
     77    <td>{@code SUGGEST_COLUMN_RESULT_CARD_IMAGE}</td>
     78    <td>An image/poster/cover for your content</td>
     79 </tr><tr>
     80    <td>{@code SUGGEST_COLUMN_CONTENT_TYPE}</td>
     81    <td>The MIME type of your media <strong>(required)</strong></td>
     82 </tr><tr>
     83    <td>{@code SUGGEST_COLUMN_VIDEO_WIDTH}</td>
     84    <td>The resolution width of your media</td>
     85 </tr><tr>
     86    <td>{@code SUGGEST_COLUMN_VIDEO_HEIGHT}</td>
     87    <td>The resolution height of your media</td>
     88 </tr><tr>
     89    <td>{@code SUGGEST_COLUMN_PRODUCTION_YEAR}</td>
     90    <td>The production year of your content <strong>(required)</strong></td>
     91 </tr><tr>
     92    <td>{@code SUGGEST_COLUMN_DURATION}</td>
     93    <td>The duration in milliseconds of your media <strong>(required)</strong></td>
     94 </tr>
     95 </table>
     96 
     97 <p>The search framework requires the following columns:</p>
     98 <ul>
     99   <li>{@link android.app.SearchManager#SUGGEST_COLUMN_TEXT_1}</li>
    100   <li>{@link android.app.SearchManager#SUGGEST_COLUMN_CONTENT_TYPE}</li>
    101   <li>{@link android.app.SearchManager#SUGGEST_COLUMN_PRODUCTION_YEAR}</li>
    102   <li>{@link android.app.SearchManager#SUGGEST_COLUMN_DURATION}</li>
    103 </ul>
    104 
    105 <p>When the values of these columns for your content match the values for the same content from other
    106 providers found by Google servers, the system provides a
    107 <a href="{@docRoot}training/app-indexing/deep-linking.html">deep link</a> to your app in the details
    108 view for the content, along with links to the apps of other providers. This is discussed more in
    109 <a href="#details">Display Content in the Details Screen</a>, below.</p>
    110 
    111 <p>Your application's database class might define the columns as follows:</p>
    112 
    113 <pre>
    114 public class VideoDatabase {
    115   //The columns we'll include in the video database table
    116   public static final String KEY_NAME = SearchManager.SUGGEST_COLUMN_TEXT_1;
    117   public static final String KEY_DESCRIPTION = SearchManager.SUGGEST_COLUMN_TEXT_2;
    118   public static final String KEY_ICON = SearchManager.SUGGEST_COLUMN_RESULT_CARD_IMAGE;
    119   public static final String KEY_DATA_TYPE = SearchManager.SUGGEST_COLUMN_CONTENT_TYPE;
    120   public static final String KEY_IS_LIVE = SearchManager.SUGGEST_COLUMN_IS_LIVE;
    121   public static final String KEY_VIDEO_WIDTH = SearchManager.SUGGEST_COLUMN_VIDEO_WIDTH;
    122   public static final String KEY_VIDEO_HEIGHT = SearchManager.SUGGEST_COLUMN_VIDEO_HEIGHT;
    123   public static final String KEY_AUDIO_CHANNEL_CONFIG =
    124           SearchManager.SUGGEST_COLUMN_AUDIO_CHANNEL_CONFIG;
    125   public static final String KEY_PURCHASE_PRICE = SearchManager.SUGGEST_COLUMN_PURCHASE_PRICE;
    126   public static final String KEY_RENTAL_PRICE = SearchManager.SUGGEST_COLUMN_RENTAL_PRICE;
    127   public static final String KEY_RATING_STYLE = SearchManager.SUGGEST_COLUMN_RATING_STYLE;
    128   public static final String KEY_RATING_SCORE = SearchManager.SUGGEST_COLUMN_RATING_SCORE;
    129   public static final String KEY_PRODUCTION_YEAR = SearchManager.SUGGEST_COLUMN_PRODUCTION_YEAR;
    130   public static final String KEY_COLUMN_DURATION = SearchManager.SUGGEST_COLUMN_DURATION;
    131   public static final String KEY_ACTION = SearchManager.SUGGEST_COLUMN_INTENT_ACTION;
    132 ...
    133 </pre>
    134 
    135 <p>When you build the map from the {@link android.app.SearchManager} columns to your data fields, you
    136 must also specify the {@link android.provider.BaseColumns#_ID} to give each row a unique ID.</p>
    137 
    138 <pre>
    139 ...
    140   private static HashMap<String, String> buildColumnMap() {
    141     HashMap<String, String> map = new HashMap<String, String>();
    142     map.put(KEY_NAME, KEY_NAME);
    143     map.put(KEY_DESCRIPTION, KEY_DESCRIPTION);
    144     map.put(KEY_ICON, KEY_ICON);
    145     map.put(KEY_DATA_TYPE, KEY_DATA_TYPE);
    146     map.put(KEY_IS_LIVE, KEY_IS_LIVE);
    147     map.put(KEY_VIDEO_WIDTH, KEY_VIDEO_WIDTH);
    148     map.put(KEY_VIDEO_HEIGHT, KEY_VIDEO_HEIGHT);
    149     map.put(KEY_AUDIO_CHANNEL_CONFIG, KEY_AUDIO_CHANNEL_CONFIG);
    150     map.put(KEY_PURCHASE_PRICE, KEY_PURCHASE_PRICE);
    151     map.put(KEY_RENTAL_PRICE, KEY_RENTAL_PRICE);
    152     map.put(KEY_RATING_STYLE, KEY_RATING_STYLE);
    153     map.put(KEY_RATING_SCORE, KEY_RATING_SCORE);
    154     map.put(KEY_PRODUCTION_YEAR, KEY_PRODUCTION_YEAR);
    155     map.put(KEY_COLUMN_DURATION, KEY_COLUMN_DURATION);
    156     map.put(KEY_ACTION, KEY_ACTION);
    157     map.put(BaseColumns._ID, "rowid AS " +
    158             BaseColumns._ID);
    159     map.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID, "rowid AS " +
    160             SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID);
    161     map.put(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID, "rowid AS " +
    162             SearchManager.SUGGEST_COLUMN_SHORTCUT_ID);
    163     return map;
    164   }
    165 ...
    166 </pre>
    167 
    168 <p>In the example above, notice the mapping to the {@link android.app.SearchManager#SUGGEST_COLUMN_INTENT_DATA_ID}
    169 field. This is the portion of the URI that points to the content unique to the data in this row &mdash;
    170 that is, the last part of the URI describing where the content is stored. The first part of the URI,
    171 when it is common to all of the rows in the table, is set in the
    172 <a href="{@docRoot}guide/topics/search/searchable-config.html"> {@code searchable.xml}</a> file as the
    173 <a href="{@docRoot}guide/topics/search/searchable-config.html#searchSuggestIntentData">
    174 {@code android:searchSuggestIntentData}</a> attribute, as described in
    175 <a href="#suggestions">Handle Search Suggestions</a>, below.
    176 
    177 <p>If the first part of the URI is different for each row in the
    178 table, you map that value with the {@link android.app.SearchManager#SUGGEST_COLUMN_INTENT_DATA} field.
    179 When the user selects this content, the intent that fires provides the intent data from the
    180 combination of the {@link android.app.SearchManager#SUGGEST_COLUMN_INTENT_DATA_ID}
    181 and either the {@code android:searchSuggestIntentData} attribute or the
    182 {@link android.app.SearchManager#SUGGEST_COLUMN_INTENT_DATA} field value.</p>
    183 
    184 <h2 id="provide">Provide Search Suggestion Data</h2>
    185 
    186 <p>Implement a <a href="{@docRoot}guide/topics/providers/content-providers.html">Content Provider</a>
    187 to return search term suggestions to the Android TV search dialog. The system queries your content
    188 provider for suggestions by calling the {@link android.content.ContentProvider#query(android.net.Uri,
    189 java.lang.String[], java.lang.String, java.lang.String[], java.lang.String) query()} method each time
    190 a letter is typed. In your implementation of {@link android.content.ContentProvider#query(android.net.Uri,
    191 java.lang.String[], java.lang.String, java.lang.String[], java.lang.String) query()}, your content
    192 provider searches your suggestion data and returns a {@link android.database.Cursor} that points to
    193 the rows you have designated for suggestions.</p>
    194 
    195 <pre>
    196 &#64;Override
    197   public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
    198                       String sortOrder) {
    199     // Use the UriMatcher to see what kind of query we have and format the db query accordingly
    200     switch (URI_MATCHER.match(uri)) {
    201       case SEARCH_SUGGEST:
    202           Log.d(TAG, "search suggest: " + selectionArgs[0] + " URI: " + uri);
    203           if (selectionArgs == null) {
    204               throw new IllegalArgumentException(
    205                       "selectionArgs must be provided for the Uri: " + uri);
    206           }
    207           return getSuggestions(selectionArgs[0]);
    208       default:
    209           throw new IllegalArgumentException("Unknown Uri: " + uri);
    210     }
    211   }
    212 
    213   private Cursor getSuggestions(String query) {
    214     query = query.toLowerCase();
    215     String[] columns = new String[]{
    216       BaseColumns._ID,
    217       VideoDatabase.KEY_NAME,
    218       VideoDatabase.KEY_DESCRIPTION,
    219       VideoDatabase.KEY_ICON,
    220       VideoDatabase.KEY_DATA_TYPE,
    221       VideoDatabase.KEY_IS_LIVE,
    222       VideoDatabase.KEY_VIDEO_WIDTH,
    223       VideoDatabase.KEY_VIDEO_HEIGHT,
    224       VideoDatabase.KEY_AUDIO_CHANNEL_CONFIG,
    225       VideoDatabase.KEY_PURCHASE_PRICE,
    226       VideoDatabase.KEY_RENTAL_PRICE,
    227       VideoDatabase.KEY_RATING_STYLE,
    228       VideoDatabase.KEY_RATING_SCORE,
    229       VideoDatabase.KEY_PRODUCTION_YEAR,
    230       VideoDatabase.KEY_COLUMN_DURATION,
    231       VideoDatabase.KEY_ACTION,
    232       SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID
    233     };
    234     return mVideoDatabase.getWordMatch(query, columns);
    235   }
    236 ...
    237 </pre>
    238 
    239 <p>In your manifest file, the content provider receives special treatment. Rather than getting
    240 tagged as an activity, it is described as a
    241 <a href="{@docRoot}guide/topics/manifest/provider-element.html">{@code &lt;provider&gt;}</a>. The
    242 provider includes the {@code android:searchSuggestAuthority} attribute to tell the system the
    243 namespace of your content provider. Also, you must set its {@code android:exported} attribute to
    244 {@code "true"} so that the Android global search can use the results returned from it.</p>
    245 
    246 <pre>
    247 &lt;provider android:name="com.example.android.tvleanback.VideoContentProvider"
    248     android:authorities="com.example.android.tvleanback"
    249     android:exported="true" /&gt;
    250 </pre>
    251 
    252 <h2 id="suggestions">Handle Search Suggestions</h2>
    253 
    254 <p>Your app must include a <a href="{@docRoot}guide/topics/search/searchable-config.html">
    255 {@code res/xml/searchable.xml}</a> file to configure the search suggestions settings. It inlcudes
    256 the <a href="{@docRoot}guide/topics/search/searchable-config.html#searchSuggestAuthority">
    257 {@code android:searchSuggestAuthority}</a> attribute to tell the system the namespace of your
    258 content provider. This must match the string value you specify in the
    259 <a href="{@docRoot}guide/topics/manifest/provider-element.html#auth">{@code android:authorities}</a>
    260 attribute of the <a href="{@docRoot}guide/topics/manifest/provider-element.html">{@code &lt;provider&gt;}
    261 </a> element in your {@code AndroidManifest.xml} file.</p>
    262 
    263 The <a href="{@docRoot}guide/topics/search/searchable-config.html">{@code searchable.xml}</a> file
    264 must also include the <a href="{@docRoot}guide/topics/search/searchable-config.html#searchSuggestIntentAction">
    265 {@code android:searchSuggestIntentAction}</a> with the value {@code "android.intent.action.VIEW"}
    266 to define the intent action for providing a custom suggestion. This is different from the intent
    267 action for providing a search term, explained below. See also,
    268 <a href="{@docRoot}guide/topics/search/adding-custom-suggestions.html#IntentAction">Declaring the
    269 intent action</a> for other ways to declare the intent action for suggestions.</p>
    270 
    271 <p>Along with the intent action, your app must provide the intent data, which you specify with the
    272 <a href="{@docRoot}guide/topics/search/searchable-config.html#searchSuggestIntentData">
    273 {@code android:searchSuggestIntentData}</a> attribute. This is the first part of the URI that points
    274 to the content. It describes the portion of the URI common to all rows in the mapping table for that
    275 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,
    276 as described above in <a href="#columns">Identify Columns</a>. See also,
    277 <a href="{@docRoot}guide/topics/search/adding-custom-suggestions.html#IntentData">
    278 Declaring the intent data</a> for other ways to declare the intent data for suggestions.</p>
    279 
    280 <p>Also, note the {@code android:searchSuggestSelection=" ?"} attribute which specifies the value passed
    281 as the {@code selection} parameter of the {@link android.content.ContentProvider#query(android.net.Uri,
    282 java.lang.String[], java.lang.String, java.lang.String[], java.lang.String) query()} method where the
    283 question mark ({@code ?}) value is replaced with the query text.</p>
    284 
    285 <p>Finally, you must also include the <a href="{@docRoot}guide/topics/search/searchable-config.html#includeInGlobalSearch">
    286 {@code android:includeInGlobalSearch}</a> attribute with the value {@code "true"}. Here is an example
    287 <a href="{@docRoot}guide/topics/search/searchable-config.html">{@code searchable.xml}</a>
    288 file:</p>
    289 
    290 <pre>
    291 &lt;searchable xmlns:android="http://schemas.android.com/apk/res/android"
    292     android:label="@string/search_label"
    293         android:hint="@string/search_hint"
    294         android:searchSettingsDescription="@string/settings_description"
    295         android:searchSuggestAuthority="com.example.android.tvleanback"
    296         android:searchSuggestIntentAction="android.intent.action.VIEW"
    297         android:searchSuggestIntentData="content://com.example.android.tvleanback/video_database_leanback"
    298         android:searchSuggestSelection=" ?"
    299         android:searchSuggestThreshold="1"
    300         android:includeInGlobalSearch="true"
    301     &gt;
    302 &lt;/searchable&gt;
    303 </pre>
    304 
    305 <h2 id="terms">Handle Search Terms</h2>
    306 
    307 <p>As soon as the search dialog has a word which matches the value in one of your app's columns
    308 (described in <a href="#identifying">Identifying Columns</a>, above), the system fires the
    309 {@link android.content.Intent#ACTION_SEARCH} intent. The activity in your app which handles that
    310 intent searches the repository for columns with the given word in their values, and returns a list
    311 of content items with those columns. In your {@code AndroidManifest.xml} file, you designate the
    312 activity which handles the {@link android.content.Intent#ACTION_SEARCH} intent like this:
    313 
    314 <pre>
    315 ...
    316   &lt;activity
    317       android:name="com.example.android.tvleanback.DetailsActivity"
    318       android:exported="true"&gt;
    319 
    320       &lt;!-- Receives the search request. --&gt;
    321       &lt;intent-filter&gt;
    322           &lt;action android:name="android.intent.action.SEARCH" /&gt;
    323           &lt;!-- No category needed, because the Intent will specify this class component --&gt;
    324       &lt;/intent-filter&gt;
    325 
    326       &lt;!-- Points to searchable meta data. --&gt;
    327       &lt;meta-data android:name="android.app.searchable"
    328           android:resource="@xml/searchable" /&gt;
    329   &lt;/activity&gt;
    330 ...
    331   &lt;!-- Provides search suggestions for keywords against video meta data. --&gt;
    332   &lt;provider android:name="com.example.android.tvleanback.VideoContentProvider"
    333       android:authorities="com.example.android.tvleanback"
    334       android:exported="true" /&gt;
    335 ...
    336 </pre>
    337 
    338 <p>The activity must also describe the searchable configuration with a reference to the
    339 <a href="{@docRoot}guide/topics/search/searchable-config.html">{@code searchable.xml}</a> file.
    340 To <a href="{@docRoot}guide/topics/search/search-dialog.html">use the global search dialog</a>,
    341 the manifest must describe which activity should receive search queries. The manifest must also
    342 describe the <a href="{@docRoot}guide/topics/manifest/provider-element.html">{@code &lt;provider&gt;}
    343 </a>element, exactly as it is described in the <a href="{@docRoot}guide/topics/search/searchable-config.html">
    344 {@code searchable.xml}</a> file.</p>
    345 
    346 <h2 id="details">Deep Link to Your App in the Details Screen</h2>
    347 
    348 <p>If you have set up the search configuration as described in <a href="#suggestions">Handle Search
    349 Suggestions</a> and mapped the {@link android.app.SearchManager#SUGGEST_COLUMN_TEXT_1},
    350 {@link android.app.SearchManager#SUGGEST_COLUMN_CONTENT_TYPE}, and
    351 {@link android.app.SearchManager#SUGGEST_COLUMN_PRODUCTION_YEAR} fields as described in
    352 <a href="#columns">Identify Columns</a>, a <a href="{@docRoot}training/app-indexing/deep-linking.html">
    353 deep link</a> to a watch action for your content appears in the details screen that launches when
    354 the user selects a search result, as shown in figure 1.</p>
    355 
    356 <img itemprop="image" src="{@docRoot}images/tv/deep-link.png" alt="Deep link in the details screen"/>
    357 <p class="img-caption"><b>Figure 1.</b> The details screen displays a deep link for the
    358 Videos by Google (Leanback) sample app. Sintel: &copy; copyright Blender Foundation, www.sintel.org.</p>
    359 
    360 <p>When the user selects the link for your app, identified by the "Available On" button in the
    361 details screen, the system launches the activity which handles the {@link android.content.Intent#ACTION_VIEW}
    362 (set as <a href="{@docRoot}guide/topics/search/searchable-config.html#searchSuggestIntentAction">
    363 {@code android:searchSuggestIntentAction}</a> with the value {@code "android.intent.action.VIEW"} in
    364 the <a href="{@docRoot}guide/topics/search/searchable-config.html">{@code searchable.xml}</a> file).</p>
    365 
    366 <p>You can also set up a custom intent to launch your activity, and this is demonstrated in the
    367 <a class="external-link" href="https://github.com/googlesamples/androidtv-Leanback">Android Leanback
    368 sample app</a>. Note that the sample app launches its own <code>LeanbackDetailsFragment</code> to
    369 show the details for the selected media, but you should launch the activity that plays the media
    370 immediately to save the user another click or two.</p>
    371 
    372 
    373 
    374 
    375 
    376 
    377