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