SearchFragment.java revision 7e599cd800f063eb6c7f965d5f13c7ae0556be1d
1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 * in compliance with the License. You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software distributed under the License
10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 * or implied. See the License for the specific language governing permissions and limitations under
12 * the License.
13 */
14package android.support.v17.leanback.app;
15
16import android.app.Fragment;
17import android.content.Intent;
18import android.graphics.drawable.Drawable;
19import android.os.Bundle;
20import android.os.Handler;
21import android.os.Message;
22import android.speech.SpeechRecognizer;
23import android.speech.RecognizerIntent;
24import android.support.v17.leanback.widget.ObjectAdapter;
25import android.support.v17.leanback.widget.ObjectAdapter.DataObserver;
26import android.support.v17.leanback.widget.OnItemClickedListener;
27import android.support.v17.leanback.widget.OnItemSelectedListener;
28import android.support.v17.leanback.widget.OnItemViewClickedListener;
29import android.support.v17.leanback.widget.OnItemViewSelectedListener;
30import android.support.v17.leanback.widget.Row;
31import android.support.v17.leanback.widget.RowPresenter;
32import android.support.v17.leanback.widget.SearchBar;
33import android.support.v17.leanback.widget.VerticalGridView;
34import android.support.v17.leanback.widget.Presenter.ViewHolder;
35import android.support.v17.leanback.widget.SpeechRecognitionCallback;
36import android.util.Log;
37import android.view.LayoutInflater;
38import android.view.View;
39import android.view.ViewGroup;
40import android.widget.FrameLayout;
41import android.support.v17.leanback.R;
42
43import java.util.ArrayList;
44import java.util.List;
45
46/**
47 * A fragment to handle searches. An application will supply an implementation
48 * of the {@link SearchResultProvider} interface to handle the search and return
49 * an {@link ObjectAdapter} containing the results. The results are rendered
50 * into a {@link RowsFragment}, in the same way that they are in a {@link
51 * BrowseFragment}.
52 *
53 * <p>If you do not supply a callback via
54 * {@link #setSpeechRecognitionCallback(SpeechRecognitionCallback)}, an internal speech
55 * recognizer will be used for which your application will need to request
56 * android.permission.RECORD_AUDIO.
57 */
58public class SearchFragment extends Fragment {
59    private static final String TAG = SearchFragment.class.getSimpleName();
60    private static final boolean DEBUG = false;
61
62    private static final String ARG_PREFIX = SearchFragment.class.getCanonicalName();
63    private static final String ARG_QUERY =  ARG_PREFIX + ".query";
64    private static final String ARG_TITLE = ARG_PREFIX  + ".title";
65
66    private static final int MSG_DESTROY_RECOGNIZER = 1;
67
68    /**
69     * Search API to be provided by the application.
70     */
71    public static interface SearchResultProvider {
72        /**
73         * <p>Method invoked some time prior to the first call to onQueryTextChange to retrieve
74         * an ObjectAdapter that will contain the results to future updates of the search query.</p>
75         *
76         * <p>As results are retrieved, the application should use the data set notification methods
77         * on the ObjectAdapter to instruct the SearchFragment to update the results.</p>
78         *
79         * @return ObjectAdapter The result object adapter.
80         */
81        public ObjectAdapter getResultsAdapter();
82
83        /**
84         * <p>Method invoked when the search query is updated.</p>
85         *
86         * <p>This is called as soon as the query changes; it is up to the application to add a
87         * delay before actually executing the queries if needed.
88         *
89         * <p>This method might not always be called before onQueryTextSubmit gets called, in
90         * particular for voice input.
91         *
92         * @param newQuery The current search query.
93         * @return whether the results changed as a result of the new query.
94         */
95        public boolean onQueryTextChange(String newQuery);
96
97        /**
98         * Method invoked when the search query is submitted, either by dismissing the keyboard,
99         * pressing search or next on the keyboard or when voice has detected the end of the query.
100         *
101         * @param query The query entered.
102         * @return whether the results changed as a result of the query.
103         */
104        public boolean onQueryTextSubmit(String query);
105    }
106
107    private final DataObserver mAdapterObserver = new DataObserver() {
108        public void onChanged() {
109            resultsChanged();
110        }
111    };
112
113    private RowsFragment mRowsFragment;
114    private final Handler mHandler = new Handler() {
115        @Override
116        public void handleMessage (Message msg) {
117            switch (msg.what) {
118                case MSG_DESTROY_RECOGNIZER:
119                    if (null != mSpeechRecognizer) {
120                        if (DEBUG) Log.v(TAG, "Destroy recognizer");
121                        mSpeechRecognizer.destroy();
122                        mSpeechRecognizer = null;
123                    }
124            }
125        }
126    };
127
128    private SearchBar mSearchBar;
129    private SearchResultProvider mProvider;
130    private String mPendingQuery = null;
131
132    private OnItemSelectedListener mOnItemSelectedListener;
133    private OnItemClickedListener mOnItemClickedListener;
134    private OnItemViewSelectedListener mOnItemViewSelectedListener;
135    private OnItemViewClickedListener mOnItemViewClickedListener;
136    private ObjectAdapter mResultAdapter;
137    private SpeechRecognitionCallback mSpeechRecognitionCallback;
138
139    private String mTitle;
140    private Drawable mBadgeDrawable;
141
142    private SpeechRecognizer mSpeechRecognizer;
143
144    private final int RESULTS_CHANGED = 0x1;
145    private final int QUERY_COMPLETE = 0x2;
146
147    private int mStatus;
148
149    /**
150     * @param args Bundle to use for the arguments, if null a new Bundle will be created.
151     */
152    public static Bundle createArgs(Bundle args, String query) {
153        return createArgs(args, query, null);
154    }
155
156    public static Bundle createArgs(Bundle args, String query, String title)  {
157        if (args == null) {
158            args = new Bundle();
159        }
160        args.putString(ARG_QUERY, query);
161        args.putString(ARG_TITLE, title);
162        return args;
163    }
164
165    /**
166     * Create a search fragment with a given search query.
167     *
168     * <p>You should only use this if you need to start the search fragment with a
169     * pre-filled query.
170     *
171     * @param query The search query to begin with.
172     * @return A new SearchFragment.
173     */
174    public static SearchFragment newInstance(String query) {
175        SearchFragment fragment = new SearchFragment();
176        Bundle args = createArgs(null, query);
177        fragment.setArguments(args);
178        return fragment;
179    }
180
181    @Override
182    public void onCreate(Bundle savedInstanceState) {
183        super.onCreate(savedInstanceState);
184    }
185
186    @Override
187    public View onCreateView(LayoutInflater inflater, ViewGroup container,
188                             Bundle savedInstanceState) {
189        View root = inflater.inflate(R.layout.lb_search_fragment, container, false);
190
191        FrameLayout searchFrame = (FrameLayout) root.findViewById(R.id.lb_search_frame);
192        mSearchBar = (SearchBar) searchFrame.findViewById(R.id.lb_search_bar);
193        mSearchBar.setSearchBarListener(new SearchBar.SearchBarListener() {
194            @Override
195            public void onSearchQueryChange(String query) {
196                if (DEBUG) Log.v(TAG, String.format("onSearchQueryChange %s", query));
197                if (null != mProvider) {
198                    retrieveResults(query);
199                } else {
200                    mPendingQuery = query;
201                }
202            }
203
204            @Override
205            public void onSearchQuerySubmit(String query) {
206                if (DEBUG) Log.v(TAG, String.format("onSearchQuerySubmit %s", query));
207                queryComplete();
208                if (null != mProvider) {
209                    mProvider.onQueryTextSubmit(query);
210                }
211            }
212
213            @Override
214            public void onKeyboardDismiss(String query) {
215                if (DEBUG) Log.v(TAG, String.format("onKeyboardDismiss %s", query));
216                queryComplete();
217            }
218        });
219        mSearchBar.setSpeechRecognitionCallback(mSpeechRecognitionCallback);
220
221        readArguments(getArguments());
222        if (null != mBadgeDrawable) {
223            setBadgeDrawable(mBadgeDrawable);
224        }
225        if (null != mTitle) {
226            setTitle(mTitle);
227        }
228
229        // Inject the RowsFragment in the results container
230        if (getChildFragmentManager().findFragmentById(R.id.browse_container_dock) == null) {
231            mRowsFragment = new RowsFragment();
232            getChildFragmentManager().beginTransaction()
233                    .replace(R.id.lb_results_frame, mRowsFragment).commit();
234        } else {
235            mRowsFragment = (RowsFragment) getChildFragmentManager()
236                    .findFragmentById(R.id.browse_container_dock);
237        }
238        mRowsFragment.setOnItemViewSelectedListener(new OnItemViewSelectedListener() {
239            @Override
240            public void onItemSelected(ViewHolder itemViewHolder, Object item,
241                    RowPresenter.ViewHolder rowViewHolder, Row row) {
242                int position = mRowsFragment.getVerticalGridView().getSelectedPosition();
243                if (DEBUG) Log.v(TAG, String.format("onItemSelected %d", position));
244                mSearchBar.setVisibility(0 >= position ? View.VISIBLE : View.GONE);
245                if (null != mOnItemSelectedListener) {
246                    mOnItemSelectedListener.onItemSelected(item, row);
247                }
248                if (null != mOnItemViewSelectedListener) {
249                    mOnItemViewSelectedListener.onItemSelected(itemViewHolder, item,
250                            rowViewHolder, row);
251                }
252            }
253        });
254        mRowsFragment.setOnItemViewClickedListener(new OnItemViewClickedListener() {
255            @Override
256            public void onItemClicked(ViewHolder itemViewHolder, Object item,
257                    RowPresenter.ViewHolder rowViewHolder, Row row) {
258                int position = mRowsFragment.getVerticalGridView().getSelectedPosition();
259                if (DEBUG) Log.v(TAG, String.format("onItemClicked %d", position));
260                if (null != mOnItemClickedListener) {
261                    mOnItemClickedListener.onItemClicked(item, row);
262                }
263                if (null != mOnItemViewClickedListener) {
264                    mOnItemViewClickedListener.onItemClicked(itemViewHolder, item,
265                            rowViewHolder, row);
266                }
267            }
268        });
269        mRowsFragment.setExpand(true);
270        if (null != mProvider) {
271            onSetSearchResultProvider();
272        }
273        updateSearchBar();
274        return root;
275    }
276
277    @Override
278    public void onStart() {
279        super.onStart();
280
281        VerticalGridView list = mRowsFragment.getVerticalGridView();
282        int mContainerListAlignTop =
283                getResources().getDimensionPixelSize(R.dimen.lb_search_browse_rows_align_top);
284        list.setItemAlignmentOffset(0);
285        list.setItemAlignmentOffsetPercent(VerticalGridView.ITEM_ALIGN_OFFSET_PERCENT_DISABLED);
286        list.setWindowAlignmentOffset(mContainerListAlignTop);
287        list.setWindowAlignmentOffsetPercent(VerticalGridView.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED);
288        list.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE);
289    }
290
291    @Override
292    public void onResume() {
293        super.onResume();
294        if (mSpeechRecognitionCallback == null && null == mSpeechRecognizer) {
295            mHandler.removeMessages(MSG_DESTROY_RECOGNIZER);
296            mSpeechRecognizer = SpeechRecognizer.createSpeechRecognizer(getActivity());
297            mSearchBar.setSpeechRecognizer(mSpeechRecognizer);
298        }
299    }
300
301    @Override
302    public void onPause() {
303        releaseRecognizer();
304        super.onPause();
305    }
306
307    @Override
308    public void onDestroy() {
309        releaseAdapter();
310        super.onDestroy();
311    }
312
313    private void releaseRecognizer() {
314        if (null != mSpeechRecognizer) {
315            mSearchBar.setSpeechRecognizer(null);
316            mSpeechRecognizer.stopListening();
317            mSpeechRecognizer.cancel();
318            mHandler.sendEmptyMessageDelayed(MSG_DESTROY_RECOGNIZER, 1000);
319        }
320    }
321
322    /**
323     * Set the search provider that is responsible for returning results for the
324     * search query.
325     */
326    public void setSearchResultProvider(SearchResultProvider searchResultProvider) {
327        mProvider = searchResultProvider;
328        onSetSearchResultProvider();
329    }
330
331    /**
332     * Sets an item selection listener for the results.
333     *
334     * @param listener The item selection listener to be invoked when an item in
335     *        the search results is selected.
336     * @deprecated Use {@link #setOnItemViewSelectedListener(OnItemViewSelectedListener)}
337     */
338    public void setOnItemSelectedListener(OnItemSelectedListener listener) {
339        mOnItemSelectedListener = listener;
340    }
341
342    /**
343     * Sets an item clicked listener for the results.
344     *
345     * @param listener The item clicked listener to be invoked when an item in
346     *        the search results is clicked.
347     * @deprecated Use {@link #setOnItemViewClickedListener(OnItemViewClickedListener)}
348     */
349    public void setOnItemClickedListener(OnItemClickedListener listener) {
350        mOnItemClickedListener = listener;
351    }
352
353    /**
354     * Sets an item selection listener for the results.
355     *
356     * @param listener The item selection listener to be invoked when an item in
357     *        the search results is selected.
358     */
359    public void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) {
360        mOnItemViewSelectedListener = listener;
361    }
362
363    /**
364     * Sets an item clicked listener for the results.
365     *
366     * @param listener The item clicked listener to be invoked when an item in
367     *        the search results is clicked.
368     */
369    public void setOnItemViewClickedListener(OnItemViewClickedListener listener) {
370        mOnItemViewClickedListener = listener;
371    }
372
373    /**
374     * Sets the title string to be be shown in an empty search bar. The title
375     * may be placed in a call-to-action, such as "Search <i>title</i>" or
376     * "Speak to search <i>title</i>".
377     */
378    public void setTitle(String title) {
379        mTitle = title;
380        if (null != mSearchBar) {
381            mSearchBar.setTitle(title);
382        }
383    }
384
385    /**
386     * Returns the title set in the search bar.
387     */
388    public String getTitle() {
389        if (null != mSearchBar) {
390            return mSearchBar.getTitle();
391        }
392        return null;
393    }
394
395    /**
396     * Sets the badge drawable that will be shown inside the search bar next to
397     * the title.
398     */
399    public void setBadgeDrawable(Drawable drawable) {
400        mBadgeDrawable = drawable;
401        if (null != mSearchBar) {
402            mSearchBar.setBadgeDrawable(drawable);
403        }
404    }
405
406    /**
407     * Returns the badge drawable in the search bar.
408     */
409    public Drawable getBadgeDrawable() {
410        if (null != mSearchBar) {
411            return mSearchBar.getBadgeDrawable();
412        }
413        return null;
414    }
415
416    /**
417     * Display the completions shown by the IME. An application may provide
418     * a list of query completions that the system will show in the IME.
419     *
420     * @param completions A list of completions to show in the IME. Setting to
421     *        null or empty will clear the list.
422     */
423    public void displayCompletions(List<String> completions) {
424        mSearchBar.displayCompletions(completions);
425    }
426
427    /**
428     * Set this callback to have the fragment pass speech recognition requests
429     * to the activity rather than using an internal recognizer.
430     */
431    public void setSpeechRecognitionCallback(SpeechRecognitionCallback callback) {
432        mSpeechRecognitionCallback = callback;
433        if (mSearchBar != null) {
434            mSearchBar.setSpeechRecognitionCallback(mSpeechRecognitionCallback);
435        }
436        if (callback != null) {
437            releaseRecognizer();
438        }
439    }
440
441    /**
442     * Sets the text of the search query and optionally submits the query. Either
443     * {@link SearchResultProvider#onQueryTextChange onQueryTextChange} or
444     * {@link SearchResultProvider#onQueryTextSubmit onQueryTextSubmit} will be
445     * called on the provider if it is set.
446     *
447     * @param query The search query to set.
448     * @param submit Whether to submit the query.
449     */
450    public void setSearchQuery(String query, boolean submit) {
451        // setSearchQuery will call onQueryTextChange
452        mSearchBar.setSearchQuery(query);
453        if (submit) {
454            mProvider.onQueryTextSubmit(query);
455        }
456    }
457
458    /**
459     * Sets the text of the search query based on the {@link RecognizerIntent#EXTRA_RESULTS} in
460     * the given intent, and optionally submit the query.  If more than one result is present
461     * in the results list, the first will be used.
462     *
463     * @param intent Intent received from a speech recognition service.
464     * @param submit Whether to submit the query.
465     */
466    public void setSearchQuery(Intent intent, boolean submit) {
467        ArrayList<String> matches = intent.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS);
468        if (matches != null && matches.size() > 0) {
469            setSearchQuery(matches.get(0), submit);
470        }
471    }
472
473    /**
474     * Returns an intent that can be used to request speech recognition.
475     * Built from the base {@link RecognizerIntent#ACTION_RECOGNIZE_SPEECH} plus
476     * extras:
477     *
478     * <ul>
479     * <li>{@link RecognizerIntent#EXTRA_LANGUAGE_MODEL} set to
480     * {@link RecognizerIntent#LANGUAGE_MODEL_FREE_FORM}</li>
481     * <li>{@link RecognizerIntent#EXTRA_PARTIAL_RESULTS} set to true</li>
482     * <li>{@link RecognizerIntent#EXTRA_PROMPT} set to the search bar hint text</li>
483     * </ul>
484     *
485     * For handling the intent returned from the service, see
486     * {@link #setSearchQuery(Intent, boolean)}.
487     */
488    public Intent getRecognizerIntent() {
489        Intent recognizerIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
490        recognizerIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
491                RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
492        recognizerIntent.putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, true);
493        if (mSearchBar != null && mSearchBar.getHint() != null) {
494            recognizerIntent.putExtra(RecognizerIntent.EXTRA_PROMPT, mSearchBar.getHint());
495        }
496        return recognizerIntent;
497    }
498
499    private void retrieveResults(String searchQuery) {
500        if (DEBUG) Log.v(TAG, String.format("retrieveResults %s", searchQuery));
501        mProvider.onQueryTextChange(searchQuery);
502        mStatus &= ~QUERY_COMPLETE;
503    }
504
505    private void queryComplete() {
506        mStatus |= QUERY_COMPLETE;
507        focusOnResults();
508    }
509
510    private void resultsChanged() {
511        if (DEBUG) Log.v(TAG, "adapter size " + mResultAdapter.size());
512        mStatus |= RESULTS_CHANGED;
513        if ((mStatus & QUERY_COMPLETE) != 0) {
514            focusOnResults();
515        }
516        updateSearchBar();
517    }
518
519    private void updateSearchBar() {
520        if (mSearchBar == null || mResultAdapter == null) {
521            return;
522        }
523        final int viewId = (mResultAdapter.size() == 0 || mRowsFragment == null ||
524                mRowsFragment.getVerticalGridView() == null) ? 0 :
525                mRowsFragment.getVerticalGridView().getId();
526        mSearchBar.setNextFocusDownId(viewId);
527    }
528
529    private void focusOnResults() {
530        if (mRowsFragment == null ||
531                mRowsFragment.getVerticalGridView() == null ||
532                mResultAdapter.size() == 0) {
533            return;
534        }
535        mRowsFragment.setSelectedPosition(0);
536        if (mRowsFragment.getVerticalGridView().requestFocus()) {
537            mStatus &= ~RESULTS_CHANGED;
538        }
539    }
540
541    private void onSetSearchResultProvider() {
542        mHandler.post(new Runnable() {
543            @Override
544            public void run() {
545                // Retrieve the result adapter
546                ObjectAdapter adapter = mProvider.getResultsAdapter();
547                if (adapter != mResultAdapter) {
548                    releaseAdapter();
549                    mResultAdapter = adapter;
550                    if (mResultAdapter != null) {
551                        mResultAdapter.registerObserver(mAdapterObserver);
552                    }
553                }
554                if (null != mRowsFragment) {
555                    mRowsFragment.setAdapter(mResultAdapter);
556                    executePendingQuery();
557                }
558                updateSearchBar();
559            }
560        });
561    }
562
563    private void releaseAdapter() {
564        if (mResultAdapter != null) {
565            mResultAdapter.unregisterObserver(mAdapterObserver);
566            mResultAdapter = null;
567        }
568    }
569
570    private void executePendingQuery() {
571        if (null != mPendingQuery && null != mResultAdapter) {
572            String query = mPendingQuery;
573            mPendingQuery = null;
574            retrieveResults(query);
575        }
576    }
577
578    private void readArguments(Bundle args) {
579        if (null == args) {
580            return;
581        }
582        if (args.containsKey(ARG_QUERY)) {
583            setSearchQuery(args.getString(ARG_QUERY));
584        }
585
586        if (args.containsKey(ARG_TITLE)) {
587            setTitle(args.getString(ARG_TITLE));
588        }
589    }
590
591    private void setSearchQuery(String query) {
592        mSearchBar.setSearchQuery(query);
593    }
594}
595