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 */
14package android.support.v17.leanback.widget;
15
16import android.content.Context;
17import android.content.Intent;
18import android.content.pm.PackageManager;
19import android.content.res.Resources;
20import android.graphics.Color;
21import android.graphics.drawable.Drawable;
22import android.media.AudioManager;
23import android.media.SoundPool;
24import android.os.Bundle;
25import android.os.Handler;
26import android.os.SystemClock;
27import android.speech.RecognitionListener;
28import android.speech.RecognizerIntent;
29import android.speech.SpeechRecognizer;
30import android.text.Editable;
31import android.text.TextUtils;
32import android.text.TextWatcher;
33import android.util.AttributeSet;
34import android.util.Log;
35import android.util.SparseIntArray;
36import android.view.LayoutInflater;
37import android.view.ViewGroup;
38import android.view.inputmethod.CompletionInfo;
39import android.view.inputmethod.EditorInfo;
40import android.view.KeyEvent;
41import android.view.MotionEvent;
42import android.view.View;
43import android.widget.ImageView;
44import android.view.inputmethod.InputMethodManager;
45import android.widget.RelativeLayout;
46import android.support.v17.leanback.R;
47import android.widget.TextView;
48
49import java.util.ArrayList;
50import java.util.List;
51
52/**
53 * <p>SearchBar is a search widget.</p>
54 *
55 * <p>Note: Your application will need to request android.permission.RECORD_AUDIO</p>
56 */
57public 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     * Listener for 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         */
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         */
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         */
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                    // Do nothing.
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
134    public SearchBar(Context context) {
135        this(context, null);
136    }
137
138    public SearchBar(Context context, AttributeSet attrs) {
139        this(context, attrs, 0);
140    }
141
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
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
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
320    protected void onDetachedFromWindow() {
321        if (DEBUG) Log.v(TAG, "Releasing SoundPool");
322        mSoundPool.release();
323        super.onDetachedFromWindow();
324    }
325
326    /**
327     * Set a listener for when the term search changes
328     * @param listener
329     */
330    public void setSearchBarListener(SearchBarListener listener) {
331        mSearchBarListener = listener;
332    }
333
334    /**
335     * Set the search query
336     * @param query the search query to use
337     */
338    public void setSearchQuery(String query) {
339        stopRecognition();
340        mSearchTextEditor.setText(query);
341        setSearchQueryInternal(query);
342    }
343
344    private void setSearchQueryInternal(String query) {
345        if (DEBUG) Log.v(TAG, "setSearchQueryInternal " + query);
346        if (TextUtils.equals(mSearchQuery, query)) {
347            return;
348        }
349        mSearchQuery = query;
350
351        if (null != mSearchBarListener) {
352            mSearchBarListener.onSearchQueryChange(mSearchQuery);
353        }
354    }
355
356    /**
357     * Set the title text used in the hint shown in the search bar.
358     * @param title The hint to use.
359     */
360    public void setTitle(String title) {
361        mTitle = title;
362        updateHint();
363    }
364
365    /**
366     * Returns the current title
367     */
368    public String getTitle() {
369        return mTitle;
370    }
371
372    /**
373     * Returns the current search bar hint text.
374     */
375    public CharSequence getHint() {
376        return mHint;
377    }
378
379    /**
380     * Set the badge drawable showing inside the search bar.
381     * @param drawable The drawable to be used in the search bar.
382     */
383    public void setBadgeDrawable(Drawable drawable) {
384        mBadgeDrawable = drawable;
385        if (null != mBadgeView) {
386            mBadgeView.setImageDrawable(drawable);
387            if (null != drawable) {
388                mBadgeView.setVisibility(View.VISIBLE);
389            } else {
390                mBadgeView.setVisibility(View.GONE);
391            }
392        }
393    }
394
395    /**
396     * Returns the badge drawable
397     */
398    public Drawable getBadgeDrawable() {
399        return mBadgeDrawable;
400    }
401
402    /**
403     * Update the completion list shown by the IME
404     *
405     * @param completions list of completions shown in the IME, can be null or empty to clear them
406     */
407    public void displayCompletions(List<String> completions) {
408        List<CompletionInfo> infos = new ArrayList<CompletionInfo>();
409        if (null != completions) {
410            for (String completion : completions) {
411                infos.add(new CompletionInfo(infos.size(), infos.size(), completion));
412            }
413        }
414
415        mInputMethodManager.displayCompletions(mSearchTextEditor,
416                infos.toArray(new CompletionInfo[] {}));
417    }
418
419    /**
420     * Set the speech recognizer to be used when doing voice search. The Activity/Fragment is in
421     * charge of creating and destroying the recognizer with its own lifecycle.
422     *
423     * @param recognizer a SpeechRecognizer
424     */
425    public void setSpeechRecognizer(SpeechRecognizer recognizer) {
426        if (null != mSpeechRecognizer) {
427            mSpeechRecognizer.setRecognitionListener(null);
428            if (mListening) {
429                mSpeechRecognizer.cancel();
430                mListening = false;
431            }
432        }
433        mSpeechRecognizer = recognizer;
434        if (mSpeechRecognizer != null) {
435            enforceAudioRecordPermission();
436        }
437        if (mSpeechRecognitionCallback != null && mSpeechRecognizer != null) {
438            throw new IllegalStateException("Can't have speech recognizer and request");
439        }
440    }
441
442    public void setSpeechRecognitionCallback(SpeechRecognitionCallback request) {
443        mSpeechRecognitionCallback = request;
444        if (mSpeechRecognitionCallback != null && mSpeechRecognizer != null) {
445            throw new IllegalStateException("Can't have speech recognizer and request");
446        }
447    }
448
449    private void hideNativeKeyboard() {
450        mInputMethodManager.hideSoftInputFromWindow(mSearchTextEditor.getWindowToken(),
451                InputMethodManager.RESULT_UNCHANGED_SHOWN);
452    }
453
454    private void showNativeKeyboard() {
455        mHandler.post(new Runnable() {
456            @Override
457            public void run() {
458                mSearchTextEditor.requestFocusFromTouch();
459                mSearchTextEditor.dispatchTouchEvent(MotionEvent.obtain(SystemClock.uptimeMillis(),
460                        SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN,
461                        mSearchTextEditor.getWidth(), mSearchTextEditor.getHeight(), 0));
462                mSearchTextEditor.dispatchTouchEvent(MotionEvent.obtain(SystemClock.uptimeMillis(),
463                        SystemClock.uptimeMillis(), MotionEvent.ACTION_UP,
464                        mSearchTextEditor.getWidth(), mSearchTextEditor.getHeight(), 0));
465            }
466        });
467    }
468
469    /**
470     * This will update the hint for the search bar properly depending on state and provided title
471     */
472    private void updateHint() {
473        String title = getResources().getString(R.string.lb_search_bar_hint);
474        if (!TextUtils.isEmpty(mTitle)) {
475            if (isVoiceMode()) {
476                title = getResources().getString(R.string.lb_search_bar_hint_with_title_speech, mTitle);
477            } else {
478                title = getResources().getString(R.string.lb_search_bar_hint_with_title, mTitle);
479            }
480        } else if (isVoiceMode()) {
481            title = getResources().getString(R.string.lb_search_bar_hint_speech);
482        }
483        mHint = title;
484        if (mSearchTextEditor != null) {
485            mSearchTextEditor.setHint(mHint);
486        }
487    }
488
489    private void toggleRecognition() {
490        if (mRecognizing) {
491            stopRecognition();
492        } else {
493            startRecognition();
494        }
495    }
496
497    /**
498     * Stop the recognition if already started
499     */
500    public void stopRecognition() {
501        if (DEBUG) Log.v(TAG, String.format("stopRecognition (listening: %s, recognizing: %s)",
502                mListening, mRecognizing));
503
504        if (!mRecognizing) return;
505
506        // Edit text content was cleared when starting recogition; ensure the content is restored
507        // in error cases
508        mSearchTextEditor.setText(mSearchQuery);
509        mSearchTextEditor.setHint(mHint);
510
511        mRecognizing = false;
512
513        if (mSpeechRecognitionCallback != null || null == mSpeechRecognizer) return;
514
515        mSpeechOrbView.showNotListening();
516
517        if (mListening) {
518            mSpeechRecognizer.cancel();
519            mListening = false;
520            mAudioManager.abandonAudioFocus(mAudioFocusChangeListener);
521        }
522
523        mSpeechRecognizer.setRecognitionListener(null);
524    }
525
526    /**
527     * Start the voice recognition
528     */
529    public void startRecognition() {
530        if (DEBUG) Log.v(TAG, String.format("startRecognition (listening: %s, recognizing: %s)",
531                mListening, mRecognizing));
532
533        if (mRecognizing) return;
534        mRecognizing = true;
535        if (!hasFocus()) {
536            requestFocus();
537        }
538        if (mSpeechRecognitionCallback != null) {
539            mSearchTextEditor.setText("");
540            mSearchTextEditor.setHint("");
541            mSpeechRecognitionCallback.recognizeSpeech();
542            return;
543        }
544        if (null == mSpeechRecognizer) return;
545
546        // Request audio focus
547        int result = mAudioManager.requestAudioFocus(mAudioFocusChangeListener,
548                // Use the music stream.
549                AudioManager.STREAM_MUSIC,
550                // Request exclusive transient focus.
551                AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK);
552
553
554        if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
555            Log.w(TAG, "Could not get audio focus");
556        }
557
558        mSearchTextEditor.setText("");
559
560        Intent recognizerIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
561
562        recognizerIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
563                RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
564        recognizerIntent.putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, true);
565
566        mSpeechRecognizer.setRecognitionListener(new RecognitionListener() {
567            @Override
568            public void onReadyForSpeech(Bundle bundle) {
569                if (DEBUG) Log.v(TAG, "onReadyForSpeech");
570                mSpeechOrbView.showListening();
571                playSearchOpen();
572            }
573
574            @Override
575            public void onBeginningOfSpeech() {
576                if (DEBUG) Log.v(TAG, "onBeginningOfSpeech");
577            }
578
579            @Override
580            public void onRmsChanged(float rmsdB) {
581                if (DEBUG) Log.v(TAG, "onRmsChanged " + rmsdB);
582                int level = rmsdB < 0 ? 0 : (int)(10 * rmsdB);
583                mSpeechOrbView.setSoundLevel(level);
584            }
585
586            @Override
587            public void onBufferReceived(byte[] bytes) {
588                if (DEBUG) Log.v(TAG, "onBufferReceived " + bytes.length);
589            }
590
591            @Override
592            public void onEndOfSpeech() {
593                if (DEBUG) Log.v(TAG, "onEndOfSpeech");
594            }
595
596            @Override
597            public void onError(int error) {
598                if (DEBUG) Log.v(TAG, "onError " + error);
599                switch (error) {
600                    case SpeechRecognizer.ERROR_NETWORK_TIMEOUT:
601                        Log.w(TAG, "recognizer network timeout");
602                        break;
603                    case SpeechRecognizer.ERROR_NETWORK:
604                        Log.w(TAG, "recognizer network error");
605                        break;
606                    case SpeechRecognizer.ERROR_AUDIO:
607                        Log.w(TAG, "recognizer audio error");
608                        break;
609                    case SpeechRecognizer.ERROR_SERVER:
610                        Log.w(TAG, "recognizer server error");
611                        break;
612                    case SpeechRecognizer.ERROR_CLIENT:
613                        Log.w(TAG, "recognizer client error");
614                        break;
615                    case SpeechRecognizer.ERROR_SPEECH_TIMEOUT:
616                        Log.w(TAG, "recognizer speech timeout");
617                        break;
618                    case SpeechRecognizer.ERROR_NO_MATCH:
619                        Log.w(TAG, "recognizer no match");
620                        break;
621                    case SpeechRecognizer.ERROR_RECOGNIZER_BUSY:
622                        Log.w(TAG, "recognizer busy");
623                        break;
624                    case SpeechRecognizer.ERROR_INSUFFICIENT_PERMISSIONS:
625                        Log.w(TAG, "recognizer insufficient permissions");
626                        break;
627                    default:
628                        Log.d(TAG, "recognizer other error");
629                        break;
630                }
631
632                stopRecognition();
633                playSearchFailure();
634            }
635
636            @Override
637            public void onResults(Bundle bundle) {
638                if (DEBUG) Log.v(TAG, "onResults");
639                final ArrayList<String> matches =
640                        bundle.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION);
641                if (matches != null) {
642                    if (DEBUG) Log.v(TAG, "Got results" + matches);
643
644                    mSearchQuery = matches.get(0);
645                    mSearchTextEditor.setText(mSearchQuery);
646                    submitQuery();
647                }
648
649                stopRecognition();
650                playSearchSuccess();
651            }
652
653            @Override
654            public void onPartialResults(Bundle bundle) {
655                ArrayList<String> results = bundle.getStringArrayList(
656                        SpeechRecognizer.RESULTS_RECOGNITION);
657                if (DEBUG) Log.v(TAG, "onPartialResults " + bundle + " results " +
658                        (results == null ? results : results.size()));
659                if (results == null || results.size() == 0) {
660                    return;
661                }
662
663                // stableText: high confidence text from PartialResults, if any.
664                // Otherwise, existing stable text.
665                final String stableText = results.get(0);
666                if (DEBUG) Log.v(TAG, "onPartialResults stableText " + stableText);
667
668                // pendingText: low confidence text from PartialResults, if any.
669                // Otherwise, empty string.
670                final String pendingText = results.size() > 1 ? results.get(1) : null;
671                if (DEBUG) Log.v(TAG, "onPartialResults pendingText " + pendingText);
672
673                mSearchTextEditor.updateRecognizedText(stableText, pendingText);
674            }
675
676            @Override
677            public void onEvent(int i, Bundle bundle) {
678
679            }
680        });
681
682        mListening = true;
683        mSpeechRecognizer.startListening(recognizerIntent);
684    }
685
686    private void updateUi(boolean hasFocus) {
687        if (hasFocus) {
688            mBarBackground.setAlpha(mBackgroundSpeechAlpha);
689            if (isVoiceMode()) {
690                mSearchTextEditor.setTextColor(mTextHintColorSpeechMode);
691                mSearchTextEditor.setHintTextColor(mTextHintColorSpeechMode);
692            } else {
693                mSearchTextEditor.setTextColor(mTextColorSpeechMode);
694                mSearchTextEditor.setHintTextColor(mTextHintColorSpeechMode);
695            }
696        } else {
697            mBarBackground.setAlpha(mBackgroundAlpha);
698            mSearchTextEditor.setTextColor(mTextColor);
699            mSearchTextEditor.setHintTextColor(mTextHintColor);
700        }
701
702        updateHint();
703    }
704
705    private boolean isVoiceMode() {
706        return mSpeechOrbView.isFocused();
707    }
708
709    private void submitQuery() {
710        if (!TextUtils.isEmpty(mSearchQuery) && null != mSearchBarListener) {
711            mSearchBarListener.onSearchQuerySubmit(mSearchQuery);
712        }
713    }
714
715    private void enforceAudioRecordPermission() {
716        String permission = "android.permission.RECORD_AUDIO";
717        int res = getContext().checkCallingOrSelfPermission(permission);
718        if (PackageManager.PERMISSION_GRANTED != res) {
719            throw new IllegalStateException("android.permission.RECORD_AUDIO required for search");
720        }
721    }
722
723    private void loadSounds(Context context) {
724        int[] sounds = {
725                R.raw.lb_voice_failure,
726                R.raw.lb_voice_open,
727                R.raw.lb_voice_no_input,
728                R.raw.lb_voice_success,
729        };
730        for (int sound : sounds) {
731            mSoundMap.put(sound, mSoundPool.load(context, sound, 1));
732        }
733    }
734
735    private void play(final int resId) {
736        mHandler.post(new Runnable() {
737            @Override
738            public void run() {
739                int sound = mSoundMap.get(resId);
740                mSoundPool.play(sound, FULL_LEFT_VOLUME, FULL_RIGHT_VOLUME, DEFAULT_PRIORITY,
741                        DO_NOT_LOOP, DEFAULT_RATE);
742            }
743        });
744    }
745
746    private void playSearchOpen() {
747        play(R.raw.lb_voice_open);
748    }
749
750    private void playSearchFailure() {
751        play(R.raw.lb_voice_failure);
752    }
753
754    private void playSearchNoInput() {
755        play(R.raw.lb_voice_no_input);
756    }
757
758    private void playSearchSuccess() {
759        play(R.raw.lb_voice_success);
760    }
761
762    @Override
763    public void setNextFocusDownId(int viewId) {
764        mSpeechOrbView.setNextFocusDownId(viewId);
765        mSearchTextEditor.setNextFocusDownId(viewId);
766    }
767
768}
769