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