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