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.widget;
15 
16 import android.Manifest;
17 import android.content.Context;
18 import android.content.Intent;
19 import android.content.pm.PackageManager;
20 import android.content.res.Resources;
21 import android.graphics.Color;
22 import android.graphics.drawable.Drawable;
23 import android.media.AudioManager;
24 import android.media.SoundPool;
25 import android.os.Build;
26 import android.os.Bundle;
27 import android.os.Handler;
28 import android.os.SystemClock;
29 import android.speech.RecognitionListener;
30 import android.speech.RecognizerIntent;
31 import android.speech.SpeechRecognizer;
32 import android.text.Editable;
33 import android.text.TextUtils;
34 import android.text.TextWatcher;
35 import android.util.AttributeSet;
36 import android.util.Log;
37 import android.util.SparseIntArray;
38 import android.view.KeyEvent;
39 import android.view.LayoutInflater;
40 import android.view.MotionEvent;
41 import android.view.View;
42 import android.view.ViewGroup;
43 import android.view.inputmethod.CompletionInfo;
44 import android.view.inputmethod.EditorInfo;
45 import android.view.inputmethod.InputMethodManager;
46 import android.widget.ImageView;
47 import android.widget.RelativeLayout;
48 import android.widget.TextView;
49 
50 import androidx.leanback.R;
51 
52 import java.util.ArrayList;
53 import java.util.List;
54 
55 /**
56  * A search widget containing a search orb and a text entry view.
57  *
58  * <p>
59  * Note: When {@link SpeechRecognitionCallback} is not used, i.e. using {@link SpeechRecognizer},
60  * your application will need to declare android.permission.RECORD_AUDIO in manifest file.
61  * If your application target >= 23 and the device is running >= 23, it needs implement
62  * {@link SearchBarPermissionListener} where requests runtime permission.
63  * </p>
64  */
65 public class SearchBar extends RelativeLayout {
66     static final String TAG = SearchBar.class.getSimpleName();
67     static final boolean DEBUG = false;
68 
69     static final float FULL_LEFT_VOLUME = 1.0f;
70     static final float FULL_RIGHT_VOLUME = 1.0f;
71     static final int DEFAULT_PRIORITY = 1;
72     static final int DO_NOT_LOOP = 0;
73     static final float DEFAULT_RATE = 1.0f;
74 
75     /**
76      * Interface for receiving notification of search query changes.
77      */
78     public interface SearchBarListener {
79 
80         /**
81          * Method invoked when the search bar detects a change in the query.
82          *
83          * @param query The current full query.
84          */
onSearchQueryChange(String query)85         public void onSearchQueryChange(String query);
86 
87         /**
88          * <p>Method invoked when the search query is submitted.</p>
89          *
90          * <p>This method can be called without a preceeding onSearchQueryChange,
91          * in particular in the case of a voice input.</p>
92          *
93          * @param query The query being submitted.
94          */
onSearchQuerySubmit(String query)95         public void onSearchQuerySubmit(String query);
96 
97         /**
98          * Method invoked when the IME is being dismissed.
99          *
100          * @param query The query set in the search bar at the time the IME is being dismissed.
101          */
onKeyboardDismiss(String query)102         public void onKeyboardDismiss(String query);
103 
104     }
105 
106     /**
107      * Interface that handles runtime permissions requests. App sets listener on SearchBar via
108      * {@link #setPermissionListener(SearchBarPermissionListener)}.
109      */
110     public interface SearchBarPermissionListener {
111 
112         /**
113          * Method invoked when SearchBar asks for "android.permission.RECORD_AUDIO" runtime
114          * permission.
115          */
requestAudioPermission()116         void requestAudioPermission();
117 
118     }
119 
120     SearchBarListener mSearchBarListener;
121     SearchEditText mSearchTextEditor;
122     SpeechOrbView mSpeechOrbView;
123     private ImageView mBadgeView;
124     String mSearchQuery;
125     private String mHint;
126     private String mTitle;
127     private Drawable mBadgeDrawable;
128     final Handler mHandler = new Handler();
129     private final InputMethodManager mInputMethodManager;
130     boolean mAutoStartRecognition = false;
131     private Drawable mBarBackground;
132 
133     private final int mTextColor;
134     private final int mTextColorSpeechMode;
135     private final int mTextHintColor;
136     private final int mTextHintColorSpeechMode;
137     private int mBackgroundAlpha;
138     private int mBackgroundSpeechAlpha;
139     private int mBarHeight;
140     private SpeechRecognizer mSpeechRecognizer;
141     private SpeechRecognitionCallback mSpeechRecognitionCallback;
142     private boolean mListening;
143     SoundPool mSoundPool;
144     SparseIntArray mSoundMap = new SparseIntArray();
145     boolean mRecognizing = false;
146     private final Context mContext;
147     private AudioManager mAudioManager;
148     private SearchBarPermissionListener mPermissionListener;
149 
SearchBar(Context context)150     public SearchBar(Context context) {
151         this(context, null);
152     }
153 
SearchBar(Context context, AttributeSet attrs)154     public SearchBar(Context context, AttributeSet attrs) {
155         this(context, attrs, 0);
156     }
157 
SearchBar(Context context, AttributeSet attrs, int defStyle)158     public SearchBar(Context context, AttributeSet attrs, int defStyle) {
159         super(context, attrs, defStyle);
160         mContext = context;
161 
162         Resources r = getResources();
163 
164         LayoutInflater inflater = LayoutInflater.from(getContext());
165         inflater.inflate(R.layout.lb_search_bar, this, true);
166 
167         mBarHeight = getResources().getDimensionPixelSize(R.dimen.lb_search_bar_height);
168         RelativeLayout.LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
169                 mBarHeight);
170         params.addRule(ALIGN_PARENT_TOP, RelativeLayout.TRUE);
171         setLayoutParams(params);
172         setBackgroundColor(Color.TRANSPARENT);
173         setClipChildren(false);
174 
175         mSearchQuery = "";
176         mInputMethodManager =
177                 (InputMethodManager)context.getSystemService(Context.INPUT_METHOD_SERVICE);
178 
179         mTextColorSpeechMode = r.getColor(R.color.lb_search_bar_text_speech_mode);
180         mTextColor = r.getColor(R.color.lb_search_bar_text);
181 
182         mBackgroundSpeechAlpha = r.getInteger(R.integer.lb_search_bar_speech_mode_background_alpha);
183         mBackgroundAlpha = r.getInteger(R.integer.lb_search_bar_text_mode_background_alpha);
184 
185         mTextHintColorSpeechMode = r.getColor(R.color.lb_search_bar_hint_speech_mode);
186         mTextHintColor = r.getColor(R.color.lb_search_bar_hint);
187 
188         mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
189     }
190 
191     @Override
onFinishInflate()192     protected void onFinishInflate() {
193         super.onFinishInflate();
194 
195         RelativeLayout items = (RelativeLayout)findViewById(R.id.lb_search_bar_items);
196         mBarBackground = items.getBackground();
197 
198         mSearchTextEditor = (SearchEditText)findViewById(R.id.lb_search_text_editor);
199         mBadgeView = (ImageView)findViewById(R.id.lb_search_bar_badge);
200         if (null != mBadgeDrawable) {
201             mBadgeView.setImageDrawable(mBadgeDrawable);
202         }
203 
204         mSearchTextEditor.setOnFocusChangeListener(new OnFocusChangeListener() {
205             @Override
206             public void onFocusChange(View view, boolean hasFocus) {
207                 if (DEBUG) Log.v(TAG, "EditText.onFocusChange " + hasFocus);
208                 if (hasFocus) {
209                     showNativeKeyboard();
210                 } else {
211                     hideNativeKeyboard();
212                 }
213                 updateUi(hasFocus);
214             }
215         });
216         final Runnable mOnTextChangedRunnable = new Runnable() {
217             @Override
218             public void run() {
219                 setSearchQueryInternal(mSearchTextEditor.getText().toString());
220             }
221         };
222         mSearchTextEditor.addTextChangedListener(new TextWatcher() {
223             @Override
224             public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) {
225             }
226 
227             @Override
228             public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) {
229                 // don't propagate event during speech recognition.
230                 if (mRecognizing) {
231                     return;
232                 }
233                 // while IME opens,  text editor becomes "" then restores to current value
234                 mHandler.removeCallbacks(mOnTextChangedRunnable);
235                 mHandler.post(mOnTextChangedRunnable);
236             }
237 
238             @Override
239             public void afterTextChanged(Editable editable) {
240 
241             }
242         });
243         mSearchTextEditor.setOnKeyboardDismissListener(
244                 new SearchEditText.OnKeyboardDismissListener() {
245                     @Override
246                     public void onKeyboardDismiss() {
247                         if (null != mSearchBarListener) {
248                             mSearchBarListener.onKeyboardDismiss(mSearchQuery);
249                         }
250                     }
251                 });
252 
253         mSearchTextEditor.setOnEditorActionListener(new TextView.OnEditorActionListener() {
254             @Override
255             public boolean onEditorAction(TextView textView, int action, KeyEvent keyEvent) {
256                 if (DEBUG) Log.v(TAG, "onEditorAction: " + action + " event: " + keyEvent);
257                 boolean handled = true;
258                 if ((EditorInfo.IME_ACTION_SEARCH == action
259                         || EditorInfo.IME_NULL == action) && null != mSearchBarListener) {
260                     if (DEBUG) Log.v(TAG, "Action or enter pressed");
261                     hideNativeKeyboard();
262                     mHandler.postDelayed(new Runnable() {
263                         @Override
264                         public void run() {
265                             if (DEBUG) Log.v(TAG, "Delayed action handling (search)");
266                             submitQuery();
267                         }
268                     }, 500);
269 
270                 } else if (EditorInfo.IME_ACTION_NONE == action && null != mSearchBarListener) {
271                     if (DEBUG) Log.v(TAG, "Escaped North");
272                     hideNativeKeyboard();
273                     mHandler.postDelayed(new Runnable() {
274                         @Override
275                         public void run() {
276                             if (DEBUG) Log.v(TAG, "Delayed action handling (escape_north)");
277                             mSearchBarListener.onKeyboardDismiss(mSearchQuery);
278                         }
279                     }, 500);
280                 } else if (EditorInfo.IME_ACTION_GO == action) {
281                     if (DEBUG) Log.v(TAG, "Voice Clicked");
282                         hideNativeKeyboard();
283                         mHandler.postDelayed(new Runnable() {
284                             @Override
285                             public void run() {
286                                 if (DEBUG) Log.v(TAG, "Delayed action handling (voice_mode)");
287                                 mAutoStartRecognition = true;
288                                 mSpeechOrbView.requestFocus();
289                             }
290                         }, 500);
291                 } else {
292                     handled = false;
293                 }
294 
295                 return handled;
296             }
297         });
298 
299         mSearchTextEditor.setPrivateImeOptions("EscapeNorth=1;VoiceDismiss=1;");
300 
301         mSpeechOrbView = (SpeechOrbView)findViewById(R.id.lb_search_bar_speech_orb);
302         mSpeechOrbView.setOnOrbClickedListener(new OnClickListener() {
303             @Override
304             public void onClick(View view) {
305                 toggleRecognition();
306             }
307         });
308         mSpeechOrbView.setOnFocusChangeListener(new OnFocusChangeListener() {
309             @Override
310             public void onFocusChange(View view, boolean hasFocus) {
311                 if (DEBUG) Log.v(TAG, "SpeechOrb.onFocusChange " + hasFocus);
312                 if (hasFocus) {
313                     hideNativeKeyboard();
314                     if (mAutoStartRecognition) {
315                         startRecognition();
316                         mAutoStartRecognition = false;
317                     }
318                 } else {
319                     stopRecognition();
320                 }
321                 updateUi(hasFocus);
322             }
323         });
324 
325         updateUi(hasFocus());
326         updateHint();
327     }
328 
329     @Override
onAttachedToWindow()330     protected void onAttachedToWindow() {
331         super.onAttachedToWindow();
332         if (DEBUG) Log.v(TAG, "Loading soundPool");
333         mSoundPool = new SoundPool(2, AudioManager.STREAM_SYSTEM, 0);
334         loadSounds(mContext);
335     }
336 
337     @Override
onDetachedFromWindow()338     protected void onDetachedFromWindow() {
339         stopRecognition();
340         if (DEBUG) Log.v(TAG, "Releasing SoundPool");
341         mSoundPool.release();
342         super.onDetachedFromWindow();
343     }
344 
345     /**
346      * Sets a listener for when the term search changes
347      * @param listener
348      */
setSearchBarListener(SearchBarListener listener)349     public void setSearchBarListener(SearchBarListener listener) {
350         mSearchBarListener = listener;
351     }
352 
353     /**
354      * Sets the search query
355      * @param query the search query to use
356      */
setSearchQuery(String query)357     public void setSearchQuery(String query) {
358         stopRecognition();
359         mSearchTextEditor.setText(query);
360         setSearchQueryInternal(query);
361     }
362 
setSearchQueryInternal(String query)363     void setSearchQueryInternal(String query) {
364         if (DEBUG) Log.v(TAG, "setSearchQueryInternal " + query);
365         if (TextUtils.equals(mSearchQuery, query)) {
366             return;
367         }
368         mSearchQuery = query;
369 
370         if (null != mSearchBarListener) {
371             mSearchBarListener.onSearchQueryChange(mSearchQuery);
372         }
373     }
374 
375     /**
376      * Sets the title text used in the hint shown in the search bar.
377      * @param title The hint to use.
378      */
setTitle(String title)379     public void setTitle(String title) {
380         mTitle = title;
381         updateHint();
382     }
383 
384     /**
385      * Sets background color of not-listening state search orb.
386      *
387      * @param colors SearchOrbView.Colors.
388      */
setSearchAffordanceColors(SearchOrbView.Colors colors)389     public void setSearchAffordanceColors(SearchOrbView.Colors colors) {
390         if (mSpeechOrbView != null) {
391             mSpeechOrbView.setNotListeningOrbColors(colors);
392         }
393     }
394 
395     /**
396      * Sets background color of listening state search orb.
397      *
398      * @param colors SearchOrbView.Colors.
399      */
setSearchAffordanceColorsInListening(SearchOrbView.Colors colors)400     public void setSearchAffordanceColorsInListening(SearchOrbView.Colors colors) {
401         if (mSpeechOrbView != null) {
402             mSpeechOrbView.setListeningOrbColors(colors);
403         }
404     }
405 
406     /**
407      * Returns the current title
408      */
getTitle()409     public String getTitle() {
410         return mTitle;
411     }
412 
413     /**
414      * Returns the current search bar hint text.
415      */
getHint()416     public CharSequence getHint() {
417         return mHint;
418     }
419 
420     /**
421      * Sets the badge drawable showing inside the search bar.
422      * @param drawable The drawable to be used in the search bar.
423      */
setBadgeDrawable(Drawable drawable)424     public void setBadgeDrawable(Drawable drawable) {
425         mBadgeDrawable = drawable;
426         if (null != mBadgeView) {
427             mBadgeView.setImageDrawable(drawable);
428             if (null != drawable) {
429                 mBadgeView.setVisibility(View.VISIBLE);
430             } else {
431                 mBadgeView.setVisibility(View.GONE);
432             }
433         }
434     }
435 
436     /**
437      * Returns the badge drawable
438      */
getBadgeDrawable()439     public Drawable getBadgeDrawable() {
440         return mBadgeDrawable;
441     }
442 
443     /**
444      * Updates the completion list shown by the IME
445      *
446      * @param completions list of completions shown in the IME, can be null or empty to clear them
447      */
displayCompletions(List<String> completions)448     public void displayCompletions(List<String> completions) {
449         List<CompletionInfo> infos = new ArrayList<>();
450         if (null != completions) {
451             for (String completion : completions) {
452                 infos.add(new CompletionInfo(infos.size(), infos.size(), completion));
453             }
454         }
455         CompletionInfo[] array = new CompletionInfo[infos.size()];
456         displayCompletions(infos.toArray(array));
457     }
458 
459     /**
460      * Updates the completion list shown by the IME
461      *
462      * @param completions list of completions shown in the IME, can be null or empty to clear them
463      */
displayCompletions(CompletionInfo[] completions)464     public void displayCompletions(CompletionInfo[] completions) {
465         mInputMethodManager.displayCompletions(mSearchTextEditor, completions);
466     }
467 
468     /**
469      * Sets the speech recognizer to be used when doing voice search. The Activity/Fragment is in
470      * charge of creating and destroying the recognizer with its own lifecycle.
471      *
472      * @param recognizer a SpeechRecognizer
473      */
setSpeechRecognizer(SpeechRecognizer recognizer)474     public void setSpeechRecognizer(SpeechRecognizer recognizer) {
475         stopRecognition();
476         if (null != mSpeechRecognizer) {
477             mSpeechRecognizer.setRecognitionListener(null);
478             if (mListening) {
479                 mSpeechRecognizer.cancel();
480                 mListening = false;
481             }
482         }
483         mSpeechRecognizer = recognizer;
484         if (mSpeechRecognitionCallback != null && mSpeechRecognizer != null) {
485             throw new IllegalStateException("Can't have speech recognizer and request");
486         }
487     }
488 
489     /**
490      * Sets the speech recognition callback.
491      *
492      * @deprecated Launching voice recognition activity is no longer supported. App should declare
493      *             android.permission.RECORD_AUDIO in AndroidManifest file. See details in
494      *             {@link androidx.leanback.app.SearchSupportFragment}.
495      */
496     @Deprecated
setSpeechRecognitionCallback(SpeechRecognitionCallback request)497     public void setSpeechRecognitionCallback(SpeechRecognitionCallback request) {
498         mSpeechRecognitionCallback = request;
499         if (mSpeechRecognitionCallback != null && mSpeechRecognizer != null) {
500             throw new IllegalStateException("Can't have speech recognizer and request");
501         }
502     }
503 
hideNativeKeyboard()504     void hideNativeKeyboard() {
505         mInputMethodManager.hideSoftInputFromWindow(mSearchTextEditor.getWindowToken(),
506                 InputMethodManager.RESULT_UNCHANGED_SHOWN);
507     }
508 
showNativeKeyboard()509     void showNativeKeyboard() {
510         mHandler.post(new Runnable() {
511             @Override
512             public void run() {
513                 mSearchTextEditor.requestFocusFromTouch();
514                 mSearchTextEditor.dispatchTouchEvent(MotionEvent.obtain(SystemClock.uptimeMillis(),
515                         SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN,
516                         mSearchTextEditor.getWidth(), mSearchTextEditor.getHeight(), 0));
517                 mSearchTextEditor.dispatchTouchEvent(MotionEvent.obtain(SystemClock.uptimeMillis(),
518                         SystemClock.uptimeMillis(), MotionEvent.ACTION_UP,
519                         mSearchTextEditor.getWidth(), mSearchTextEditor.getHeight(), 0));
520             }
521         });
522     }
523 
524     /**
525      * This will update the hint for the search bar properly depending on state and provided title
526      */
updateHint()527     private void updateHint() {
528         String title = getResources().getString(R.string.lb_search_bar_hint);
529         if (!TextUtils.isEmpty(mTitle)) {
530             if (isVoiceMode()) {
531                 title = getResources().getString(R.string.lb_search_bar_hint_with_title_speech, mTitle);
532             } else {
533                 title = getResources().getString(R.string.lb_search_bar_hint_with_title, mTitle);
534             }
535         } else if (isVoiceMode()) {
536             title = getResources().getString(R.string.lb_search_bar_hint_speech);
537         }
538         mHint = title;
539         if (mSearchTextEditor != null) {
540             mSearchTextEditor.setHint(mHint);
541         }
542     }
543 
toggleRecognition()544     void toggleRecognition() {
545         if (mRecognizing) {
546             stopRecognition();
547         } else {
548             startRecognition();
549         }
550     }
551 
552     /**
553      * Returns true if is not running Recognizer, false otherwise.
554      * @return True if is not running Recognizer, false otherwise.
555      */
isRecognizing()556     public boolean isRecognizing() {
557         return mRecognizing;
558     }
559 
560     /**
561      * Stops the speech recognition, if already started.
562      */
stopRecognition()563     public void stopRecognition() {
564         if (DEBUG) Log.v(TAG, String.format("stopRecognition (listening: %s, recognizing: %s)",
565                 mListening, mRecognizing));
566 
567         if (!mRecognizing) return;
568 
569         // Edit text content was cleared when starting recognition; ensure the content is restored
570         // in error cases
571         mSearchTextEditor.setText(mSearchQuery);
572         mSearchTextEditor.setHint(mHint);
573 
574         mRecognizing = false;
575 
576         if (mSpeechRecognitionCallback != null || null == mSpeechRecognizer) return;
577 
578         mSpeechOrbView.showNotListening();
579 
580         if (mListening) {
581             mSpeechRecognizer.cancel();
582             mListening = false;
583         }
584 
585         mSpeechRecognizer.setRecognitionListener(null);
586     }
587 
588     /**
589      * Sets listener that handles runtime permission requests.
590      * @param listener Listener that handles runtime permission requests.
591      */
setPermissionListener(SearchBarPermissionListener listener)592     public void setPermissionListener(SearchBarPermissionListener listener) {
593         mPermissionListener = listener;
594     }
595 
startRecognition()596     public void startRecognition() {
597         if (DEBUG) Log.v(TAG, String.format("startRecognition (listening: %s, recognizing: %s)",
598                 mListening, mRecognizing));
599 
600         if (mRecognizing) return;
601         if (!hasFocus()) {
602             requestFocus();
603         }
604         if (mSpeechRecognitionCallback != null) {
605             mSearchTextEditor.setText("");
606             mSearchTextEditor.setHint("");
607             mSpeechRecognitionCallback.recognizeSpeech();
608             mRecognizing = true;
609             return;
610         }
611         if (null == mSpeechRecognizer) return;
612         int res = getContext().checkCallingOrSelfPermission(Manifest.permission.RECORD_AUDIO);
613         if (PackageManager.PERMISSION_GRANTED != res) {
614             if (Build.VERSION.SDK_INT >= 23 && mPermissionListener != null) {
615                 mPermissionListener.requestAudioPermission();
616                 return;
617             } else {
618                 throw new IllegalStateException(Manifest.permission.RECORD_AUDIO
619                         + " required for search");
620             }
621         }
622 
623         mRecognizing = true;
624 
625         mSearchTextEditor.setText("");
626 
627         Intent recognizerIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
628 
629         recognizerIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
630                 RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
631         recognizerIntent.putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, true);
632 
633         mSpeechRecognizer.setRecognitionListener(new RecognitionListener() {
634             @Override
635             public void onReadyForSpeech(Bundle bundle) {
636                 if (DEBUG) Log.v(TAG, "onReadyForSpeech");
637                 mSpeechOrbView.showListening();
638                 playSearchOpen();
639             }
640 
641             @Override
642             public void onBeginningOfSpeech() {
643                 if (DEBUG) Log.v(TAG, "onBeginningOfSpeech");
644             }
645 
646             @Override
647             public void onRmsChanged(float rmsdB) {
648                 if (DEBUG) Log.v(TAG, "onRmsChanged " + rmsdB);
649                 int level = rmsdB < 0 ? 0 : (int)(10 * rmsdB);
650                 mSpeechOrbView.setSoundLevel(level);
651             }
652 
653             @Override
654             public void onBufferReceived(byte[] bytes) {
655                 if (DEBUG) Log.v(TAG, "onBufferReceived " + bytes.length);
656             }
657 
658             @Override
659             public void onEndOfSpeech() {
660                 if (DEBUG) Log.v(TAG, "onEndOfSpeech");
661             }
662 
663             @Override
664             public void onError(int error) {
665                 if (DEBUG) Log.v(TAG, "onError " + error);
666                 switch (error) {
667                     case SpeechRecognizer.ERROR_NETWORK_TIMEOUT:
668                         Log.w(TAG, "recognizer network timeout");
669                         break;
670                     case SpeechRecognizer.ERROR_NETWORK:
671                         Log.w(TAG, "recognizer network error");
672                         break;
673                     case SpeechRecognizer.ERROR_AUDIO:
674                         Log.w(TAG, "recognizer audio error");
675                         break;
676                     case SpeechRecognizer.ERROR_SERVER:
677                         Log.w(TAG, "recognizer server error");
678                         break;
679                     case SpeechRecognizer.ERROR_CLIENT:
680                         Log.w(TAG, "recognizer client error");
681                         break;
682                     case SpeechRecognizer.ERROR_SPEECH_TIMEOUT:
683                         Log.w(TAG, "recognizer speech timeout");
684                         break;
685                     case SpeechRecognizer.ERROR_NO_MATCH:
686                         Log.w(TAG, "recognizer no match");
687                         break;
688                     case SpeechRecognizer.ERROR_RECOGNIZER_BUSY:
689                         Log.w(TAG, "recognizer busy");
690                         break;
691                     case SpeechRecognizer.ERROR_INSUFFICIENT_PERMISSIONS:
692                         Log.w(TAG, "recognizer insufficient permissions");
693                         break;
694                     default:
695                         Log.d(TAG, "recognizer other error");
696                         break;
697                 }
698 
699                 stopRecognition();
700                 playSearchFailure();
701             }
702 
703             @Override
704             public void onResults(Bundle bundle) {
705                 if (DEBUG) Log.v(TAG, "onResults");
706                 final ArrayList<String> matches =
707                         bundle.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION);
708                 if (matches != null) {
709                     if (DEBUG) Log.v(TAG, "Got results" + matches);
710 
711                     mSearchQuery = matches.get(0);
712                     mSearchTextEditor.setText(mSearchQuery);
713                     submitQuery();
714                 }
715 
716                 stopRecognition();
717                 playSearchSuccess();
718             }
719 
720             @Override
721             public void onPartialResults(Bundle bundle) {
722                 ArrayList<String> results = bundle.getStringArrayList(
723                         SpeechRecognizer.RESULTS_RECOGNITION);
724                 if (DEBUG) {
725                     Log.v(TAG, "onPartialResults " + bundle + " results "
726                             + (results == null ? results : results.size()));
727                 }
728                 if (results == null || results.size() == 0) {
729                     return;
730                 }
731 
732                 // stableText: high confidence text from PartialResults, if any.
733                 // Otherwise, existing stable text.
734                 final String stableText = results.get(0);
735                 if (DEBUG) Log.v(TAG, "onPartialResults stableText " + stableText);
736 
737                 // pendingText: low confidence text from PartialResults, if any.
738                 // Otherwise, empty string.
739                 final String pendingText = results.size() > 1 ? results.get(1) : null;
740                 if (DEBUG) Log.v(TAG, "onPartialResults pendingText " + pendingText);
741 
742                 mSearchTextEditor.updateRecognizedText(stableText, pendingText);
743             }
744 
745             @Override
746             public void onEvent(int i, Bundle bundle) {
747 
748             }
749         });
750 
751         mListening = true;
752         mSpeechRecognizer.startListening(recognizerIntent);
753     }
754 
updateUi(boolean hasFocus)755     void updateUi(boolean hasFocus) {
756         if (hasFocus) {
757             mBarBackground.setAlpha(mBackgroundSpeechAlpha);
758             if (isVoiceMode()) {
759                 mSearchTextEditor.setTextColor(mTextHintColorSpeechMode);
760                 mSearchTextEditor.setHintTextColor(mTextHintColorSpeechMode);
761             } else {
762                 mSearchTextEditor.setTextColor(mTextColorSpeechMode);
763                 mSearchTextEditor.setHintTextColor(mTextHintColorSpeechMode);
764             }
765         } else {
766             mBarBackground.setAlpha(mBackgroundAlpha);
767             mSearchTextEditor.setTextColor(mTextColor);
768             mSearchTextEditor.setHintTextColor(mTextHintColor);
769         }
770 
771         updateHint();
772     }
773 
isVoiceMode()774     private boolean isVoiceMode() {
775         return mSpeechOrbView.isFocused();
776     }
777 
submitQuery()778     void submitQuery() {
779         if (!TextUtils.isEmpty(mSearchQuery) && null != mSearchBarListener) {
780             mSearchBarListener.onSearchQuerySubmit(mSearchQuery);
781         }
782     }
783 
loadSounds(Context context)784     private void loadSounds(Context context) {
785         int[] sounds = {
786                 R.raw.lb_voice_failure,
787                 R.raw.lb_voice_open,
788                 R.raw.lb_voice_no_input,
789                 R.raw.lb_voice_success,
790         };
791         for (int sound : sounds) {
792             mSoundMap.put(sound, mSoundPool.load(context, sound, 1));
793         }
794     }
795 
play(final int resId)796     private void play(final int resId) {
797         mHandler.post(new Runnable() {
798             @Override
799             public void run() {
800                 int sound = mSoundMap.get(resId);
801                 mSoundPool.play(sound, FULL_LEFT_VOLUME, FULL_RIGHT_VOLUME, DEFAULT_PRIORITY,
802                         DO_NOT_LOOP, DEFAULT_RATE);
803             }
804         });
805     }
806 
playSearchOpen()807     void playSearchOpen() {
808         play(R.raw.lb_voice_open);
809     }
810 
playSearchFailure()811     void playSearchFailure() {
812         play(R.raw.lb_voice_failure);
813     }
814 
playSearchNoInput()815     private void playSearchNoInput() {
816         play(R.raw.lb_voice_no_input);
817     }
818 
playSearchSuccess()819     void playSearchSuccess() {
820         play(R.raw.lb_voice_success);
821     }
822 
823     @Override
setNextFocusDownId(int viewId)824     public void setNextFocusDownId(int viewId) {
825         mSpeechOrbView.setNextFocusDownId(viewId);
826         mSearchTextEditor.setNextFocusDownId(viewId);
827     }
828 
829 }
830