In-app Search on Android TV application
One of the biggest difference between Android phone & Android TV is their input method. Since TV does not support touchpad, and we shouldn’t expect users to use keyboard for TV, inputting words for TV is troublesome.
Google suggests to use voice input for in Searching within TV Apps
In-app Search icon on BrowseFragment
BrowseFragment contains a design layout for search function as well, and showing in-app icon on your application is very easy. Just implement setOnSearchClickedListener, that’s all.
// Existence of this method make In-app search icon visible setOnSearchClickedListener(new View.OnClickListener() { @Override public void onClick(View view) { } });
setSearchAffordanceColor method can be used for specifying search icon color.
// set search icon color setSearchAffordanceColor(getResources().getColor(R.color.search_opaque));
SearchFragment
Search function need to be implemented to answer user’s search query. To show the search query input UI and search result UI, LeanbackLibrary provides SearchFragment. This time, when request search (by either voice search button or explicitly press search button icon), we invoke this SearchFragment from SearchActivity.
private void setupEventListeners() { setOnItemViewSelectedListener(new ItemViewSelectedListener()); setOnItemViewClickedListener(new ItemViewClickedListener()); // Existence of this method make In-app search icon visible setOnSearchClickedListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(getActivity(), SearchActivity.class); startActivity(intent); } }); }
Start creating SearchActivity by right click on Package name, Create → New Activity → Blank Activity → type “SearchActivity”. After SearchActivity class and activity_search.xml layout have created, modify res/acitivity_search.xml as follows.
<?xml version="1.0" encoding="utf-8"?> <fragment xmlns:android="http://schemas.android.com/apk/res/android" android:name="com.corochann.androidtvapptutorial.SearchFragment" android:id="@+id/search_fragment" android:layout_width="match_parent" android:layout_height="match_parent" />
It attaches SearchFragment. Again, create SearchFragment by Create → New → Java class → SearchFragment, and make it a subclass of android.support.v17.leanback.app.SearchFragment.
If you build and run application here, application will crash because SearchFragment automatically start to get search query from internal speech recognizer.
Implement voice input/voice search – setSpeechRecognitionCallback
Official doc says,
If you do not supply a callback via
setSpeechRecognitionCallback(SpeechRecognitionCallback)
, an internal speech recognizer will be used for which your application will need to request android.permission.RECORD_AUDIO.
So you need to do either
- Implement setSpeechRecognitionCallback
- Request android.permission.RECORD_AUDIO on AndroidManifest.xml
as follwing.
public class SearchFragment extends android.support.v17.leanback.app.SearchFragment { private static final String TAG = SearchFragment.class.getSimpleName(); private static final int REQUEST_SPEECH = 0x00000010; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (!Utils.hasPermission(getActivity(), Manifest.permission.RECORD_AUDIO)) { // SpeechRecognitionCallback is not required and if not provided recognition will be handled // using internal speech recognizer, in which case you must have RECORD_AUDIO permission setSpeechRecognitionCallback(new SpeechRecognitionCallback() { @Override public void recognizeSpeech() { Log.v(TAG, "recognizeSpeech"); try { startActivityForResult(getRecognizerIntent(), REQUEST_SPEECH); } catch (ActivityNotFoundException e) { Log.e(TAG, "Cannot find activity for speech recognizer", e); } } }); } }
public static boolean hasPermission(final Context context, final String permission) { return PackageManager.PERMISSION_GRANTED == context.getPackageManager().checkPermission( permission, context.getPackageName()); }
Overriding onSearchRequeseted to activate in-app search
When user tries voice input search, onSearchRequested callback is executed and Google’s global contents search will be launched as default.
It is necessary to override this method if you want to activate in-app application search.
<Ref> It is written in the description of startSearch method that
It is typically called from onSearchRequested(), either directly from Activity.onSearchRequested() or from an overridden version in any given Activity. If your goal is simply to activate search, it is preferred to call onSearchRequested(), which may have been overridden elsewhere in your Activity. If your goal is to inject specific data such as context data, it is preferred to override onSearchRequested(), so that any callers to it will benefit from the override.
We override onSearchRequested method for both MainActivity & SearchActivity.
@Override public boolean onSearchRequested() { startActivity(new Intent(this, SearchActivity.class)); return true; }
Customize in-app search – SearchResultProvider
SearchResultProvider intereface is an intereface of Leanback library, used to listen search related event. We need to override 3 methods.
- getResultsAdapter – returns the adapter which includes the search results, to show search results on SearchFragment.
- onQueryTextChange – event listener which is called when user changes search query text.
- onQueryTextSubmit – event listener which is called when user submitted search query text.
We need to register this SearchResultProvider by using setSearchResultProvider method, minimum implementation is like this,
public class SearchFragment extends android.support.v17.leanback.app.SearchFragment implements android.support.v17.leanback.app.SearchFragment.SearchResultProvider { private static final String TAG = SearchFragment.class.getSimpleName(); private static final int REQUEST_SPEECH = 0x00000010; private ArrayObjectAdapter mRowsAdapter; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mRowsAdapter = new ArrayObjectAdapter(new ListRowPresenter()); setSearchResultProvider(this); ... } ... @Override public ObjectAdapter getResultsAdapter() { Log.d(TAG, "getResultsAdapter"); Log.d(TAG, mRowsAdapter.toString()); // It should return search result here, // but static Movie Item list will be returned here now for practice. ArrayList<Movie> mItems = MovieProvider.getMovieItems(); ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(new CardPresenter()); listRowAdapter.addAll(0, mItems); HeaderItem header = new HeaderItem("Search results"); mRowsAdapter.add(new ListRow(header, listRowAdapter)); return mRowsAdapter; } @Override public boolean onQueryTextChange(String newQuery){ Log.i(TAG, String.format("Search Query Text Change %s", newQuery)); return true; } @Override public boolean onQueryTextSubmit(String query) { Log.i(TAG, String.format("Search Query Text Submit %s", query)); return true; }
Build and run
SearchActivity can be launched in 2 ways,
- Explicitly click search icon of BrowseFragment.
- When user starts voice input search from specific controller *1 (onSearchRequested will be called.)
*1 It is depending on the Android TV devices. For example, SONY BRAVIA provides touchpad remote controller and voice search can be done from this remote controller.
SearchFragment shows the mock search results now.
Source code is on github.
[Update on 1. 8. 2016]: I’m sorry that it seems the implementation is insufficient. We should implement OnItemViewClickedListener
to define the action of clicking these search results.This OnItemViewClickedListener
can be set it by
setOnItemViewClickedListener(new ItemViewClickedListener());
(in this case) in onCreate
of BrowseFragment
. Please refer Kim Johnsson’s helpful comment below.
Google’s AOSP implementation is explained in the next chapter.