SearchBar.java revision a00bada00bff4a58436a39472ab14ccb7a8f619d
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 * 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 */
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     * 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         */
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                    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
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        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     */
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     */
339    public void setSearchQuery(String query) {
340        stopRecognition();
341        mSearchTextEditor.setText(query);
342        setSearchQueryInternal(query);
343    }
344
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     */
361    public void setTitle(String title) {
362        mTitle = title;
363        updateHint();
364    }
365
366    /**
367     * Returns the current title
368     */
369    public String getTitle() {
370        return mTitle;
371    }
372
373    /**
374     * Returns the current search bar hint text.
375     */
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     */
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     */
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     */
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     */
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     */
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
454    private void hideNativeKeyboard() {
455        mInputMethodManager.hideSoftInputFromWindow(mSearchTextEditor.getWindowToken(),
456                InputMethodManager.RESULT_UNCHANGED_SHOWN);
457    }
458
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     */
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
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     */
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     */
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
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
710    private boolean isVoiceMode() {
711        return mSpeechOrbView.isFocused();
712    }
713
714    private void submitQuery() {
715        if (!TextUtils.isEmpty(mSearchQuery) && null != mSearchBarListener) {
716            mSearchBarListener.onSearchQuerySubmit(mSearchQuery);
717        }
718    }
719
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
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
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
751    private void playSearchOpen() {
752        play(R.raw.lb_voice_open);
753    }
754
755    private void playSearchFailure() {
756        play(R.raw.lb_voice_failure);
757    }
758
759    private void playSearchNoInput() {
760        play(R.raw.lb_voice_no_input);
761    }
762
763    private void playSearchSuccess() {
764        play(R.raw.lb_voice_success);
765    }
766
767    @Override
768    public void setNextFocusDownId(int viewId) {
769        mSpeechOrbView.setNextFocusDownId(viewId);
770        mSearchTextEditor.setNextFocusDownId(viewId);
771    }
772
773}
774