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