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