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