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