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