Home | History | Annotate | Download | only in app
      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  */
     14 package androidx.leanback.app;
     15 
     16 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
     17 
     18 import android.Manifest;
     19 import android.content.Intent;
     20 import android.graphics.drawable.Drawable;
     21 import android.os.Bundle;
     22 import android.os.Handler;
     23 import android.speech.RecognizerIntent;
     24 import android.speech.SpeechRecognizer;
     25 import android.util.Log;
     26 import android.view.LayoutInflater;
     27 import android.view.View;
     28 import android.view.ViewGroup;
     29 import android.view.inputmethod.CompletionInfo;
     30 import android.widget.FrameLayout;
     31 
     32 import androidx.fragment.app.Fragment;
     33 import androidx.leanback.R;
     34 import androidx.leanback.widget.ObjectAdapter;
     35 import androidx.leanback.widget.ObjectAdapter.DataObserver;
     36 import androidx.leanback.widget.OnItemViewClickedListener;
     37 import androidx.leanback.widget.OnItemViewSelectedListener;
     38 import androidx.leanback.widget.Presenter.ViewHolder;
     39 import androidx.leanback.widget.Row;
     40 import androidx.leanback.widget.RowPresenter;
     41 import androidx.leanback.widget.SearchBar;
     42 import androidx.leanback.widget.SearchOrbView;
     43 import androidx.leanback.widget.SpeechRecognitionCallback;
     44 import androidx.leanback.widget.VerticalGridView;
     45 
     46 import java.util.ArrayList;
     47 import 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  */
     68 public 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             requestPermissions(new String[]{Manifest.permission.RECORD_AUDIO},
    236                     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