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