376 lines
19 KiB
Plaintext
376 lines
19 KiB
Plaintext
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>
|
|
|
|
<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>
|
|
|
|
<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>
|
|
|
|
<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>
|
|
|
|
<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>
|
|
|
|
<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:
|
|
|
|
<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 a watch action for your content appears in the details screen that launches when
|
|
the user selects a search result, as shown in figure 1.</p>
|
|
|
|
<img itemprop="image" src="{@docRoot}images/tv/deep-link.png" alt="Deep link in the details screen"/>
|
|
<p class="img-caption"><b>Figure 1.</b> The details screen displays a deep link for the
|
|
Videos by Google (Leanback) sample app. Sintel: © copyright Blender Foundation, www.sintel.org.</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>
|
|
|
|
|
|
|
|
|
|
|
|
|