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