Home | History | Annotate | Download | only in widget
      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  */
     14 package androidx.leanback.widget;
     15 
     16 import android.Manifest;
     17 import android.content.Context;
     18 import android.content.Intent;
     19 import android.content.pm.PackageManager;
     20 import android.content.res.Resources;
     21 import android.graphics.Color;
     22 import android.graphics.drawable.Drawable;
     23 import android.media.AudioManager;
     24 import android.media.SoundPool;
     25 import android.os.Build;
     26 import android.os.Bundle;
     27 import android.os.Handler;
     28 import android.os.SystemClock;
     29 import android.speech.RecognitionListener;
     30 import android.speech.RecognizerIntent;
     31 import android.speech.SpeechRecognizer;
     32 import android.text.Editable;
     33 import android.text.TextUtils;
     34 import android.text.TextWatcher;
     35 import android.util.AttributeSet;
     36 import android.util.Log;
     37 import android.util.SparseIntArray;
     38 import android.view.KeyEvent;
     39 import android.view.LayoutInflater;
     40 import android.view.MotionEvent;
     41 import android.view.View;
     42 import android.view.ViewGroup;
     43 import android.view.inputmethod.CompletionInfo;
     44 import android.view.inputmethod.EditorInfo;
     45 import android.view.inputmethod.InputMethodManager;
     46 import android.widget.ImageView;
     47 import android.widget.RelativeLayout;
     48 import android.widget.TextView;
     49 
     50 import androidx.leanback.R;
     51 
     52 import java.util.ArrayList;
     53 import 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  */
     65 public class SearchBar extends RelativeLayout {
     66     static final String TAG = SearchBar.class.getSimpleName();
     67     static final boolean DEBUG = false;
     68 
     69     static final float FULL_LEFT_VOLUME = 1.0f;
     70     static final float FULL_RIGHT_VOLUME = 1.0f;
     71     static final int DEFAULT_PRIORITY = 1;
     72     static final int DO_NOT_LOOP = 0;
     73     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     SearchBarListener mSearchBarListener;
    121     SearchEditText mSearchTextEditor;
    122     SpeechOrbView mSpeechOrbView;
    123     private ImageView mBadgeView;
    124     String mSearchQuery;
    125     private String mHint;
    126     private String mTitle;
    127     private Drawable mBadgeDrawable;
    128     final Handler mHandler = new Handler();
    129     private final InputMethodManager mInputMethodManager;
    130     boolean mAutoStartRecognition = false;
    131     private Drawable mBarBackground;
    132 
    133     private final int mTextColor;
    134     private final int mTextColorSpeechMode;
    135     private final int mTextHintColor;
    136     private final int mTextHintColorSpeechMode;
    137     private int mBackgroundAlpha;
    138     private int mBackgroundSpeechAlpha;
    139     private int mBarHeight;
    140     private SpeechRecognizer mSpeechRecognizer;
    141     private SpeechRecognitionCallback mSpeechRecognitionCallback;
    142     private boolean mListening;
    143     SoundPool mSoundPool;
    144     SparseIntArray mSoundMap = new SparseIntArray();
    145     boolean mRecognizing = false;
    146     private final Context mContext;
    147     private AudioManager mAudioManager;
    148     private SearchBarPermissionListener mPermissionListener;
    149 
    150     public SearchBar(Context context) {
    151         this(context, null);
    152     }
    153 
    154     public SearchBar(Context context, AttributeSet attrs) {
    155         this(context, attrs, 0);
    156     }
    157 
    158     public SearchBar(Context context, AttributeSet attrs, int defStyle) {
    159         super(context, attrs, defStyle);
    160         mContext = context;
    161 
    162         Resources r = getResources();
    163 
    164         LayoutInflater inflater = LayoutInflater.from(getContext());
    165         inflater.inflate(R.layout.lb_search_bar, this, true);
    166 
    167         mBarHeight = getResources().getDimensionPixelSize(R.dimen.lb_search_bar_height);
    168         RelativeLayout.LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
    169                 mBarHeight);
    170         params.addRule(ALIGN_PARENT_TOP, RelativeLayout.TRUE);
    171         setLayoutParams(params);
    172         setBackgroundColor(Color.TRANSPARENT);
    173         setClipChildren(false);
    174 
    175         mSearchQuery = "";
    176         mInputMethodManager =
    177                 (InputMethodManager)context.getSystemService(Context.INPUT_METHOD_SERVICE);
    178 
    179         mTextColorSpeechMode = r.getColor(R.color.lb_search_bar_text_speech_mode);
    180         mTextColor = r.getColor(R.color.lb_search_bar_text);
    181 
    182         mBackgroundSpeechAlpha = r.getInteger(R.integer.lb_search_bar_speech_mode_background_alpha);
    183         mBackgroundAlpha = r.getInteger(R.integer.lb_search_bar_text_mode_background_alpha);
    184 
    185         mTextHintColorSpeechMode = r.getColor(R.color.lb_search_bar_hint_speech_mode);
    186         mTextHintColor = r.getColor(R.color.lb_search_bar_hint);
    187 
    188         mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
    189     }
    190 
    191     @Override
    192     protected void onFinishInflate() {
    193         super.onFinishInflate();
    194 
    195         RelativeLayout items = (RelativeLayout)findViewById(R.id.lb_search_bar_items);
    196         mBarBackground = items.getBackground();
    197 
    198         mSearchTextEditor = (SearchEditText)findViewById(R.id.lb_search_text_editor);
    199         mBadgeView = (ImageView)findViewById(R.id.lb_search_bar_badge);
    200         if (null != mBadgeDrawable) {
    201             mBadgeView.setImageDrawable(mBadgeDrawable);
    202         }
    203 
    204         mSearchTextEditor.setOnFocusChangeListener(new OnFocusChangeListener() {
    205             @Override
    206             public void onFocusChange(View view, boolean hasFocus) {
    207                 if (DEBUG) Log.v(TAG, "EditText.onFocusChange " + hasFocus);
    208                 if (hasFocus) {
    209                     showNativeKeyboard();
    210                 } else {
    211                     hideNativeKeyboard();
    212                 }
    213                 updateUi(hasFocus);
    214             }
    215         });
    216         final Runnable mOnTextChangedRunnable = new Runnable() {
    217             @Override
    218             public void run() {
    219                 setSearchQueryInternal(mSearchTextEditor.getText().toString());
    220             }
    221         };
    222         mSearchTextEditor.addTextChangedListener(new TextWatcher() {
    223             @Override
    224             public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) {
    225             }
    226 
    227             @Override
    228             public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) {
    229                 // don't propagate event during speech recognition.
    230                 if (mRecognizing) {
    231                     return;
    232                 }
    233                 // while IME opens,  text editor becomes "" then restores to current value
    234                 mHandler.removeCallbacks(mOnTextChangedRunnable);
    235                 mHandler.post(mOnTextChangedRunnable);
    236             }
    237 
    238             @Override
    239             public void afterTextChanged(Editable editable) {
    240 
    241             }
    242         });
    243         mSearchTextEditor.setOnKeyboardDismissListener(
    244                 new SearchEditText.OnKeyboardDismissListener() {
    245                     @Override
    246                     public void onKeyboardDismiss() {
    247                         if (null != mSearchBarListener) {
    248                             mSearchBarListener.onKeyboardDismiss(mSearchQuery);
    249                         }
    250                     }
    251                 });
    252 
    253         mSearchTextEditor.setOnEditorActionListener(new TextView.OnEditorActionListener() {
    254             @Override
    255             public boolean onEditorAction(TextView textView, int action, KeyEvent keyEvent) {
    256                 if (DEBUG) Log.v(TAG, "onEditorAction: " + action + " event: " + keyEvent);
    257                 boolean handled = true;
    258                 if ((EditorInfo.IME_ACTION_SEARCH == action
    259                         || EditorInfo.IME_NULL == action) && null != mSearchBarListener) {
    260                     if (DEBUG) Log.v(TAG, "Action or enter pressed");
    261                     hideNativeKeyboard();
    262                     mHandler.postDelayed(new Runnable() {
    263                         @Override
    264                         public void run() {
    265                             if (DEBUG) Log.v(TAG, "Delayed action handling (search)");
    266                             submitQuery();
    267                         }
    268                     }, 500);
    269 
    270                 } else if (EditorInfo.IME_ACTION_NONE == action && null != mSearchBarListener) {
    271                     if (DEBUG) Log.v(TAG, "Escaped North");
    272                     hideNativeKeyboard();
    273                     mHandler.postDelayed(new Runnable() {
    274                         @Override
    275                         public void run() {
    276                             if (DEBUG) Log.v(TAG, "Delayed action handling (escape_north)");
    277                             mSearchBarListener.onKeyboardDismiss(mSearchQuery);
    278                         }
    279                     }, 500);
    280                 } else if (EditorInfo.IME_ACTION_GO == action) {
    281                     if (DEBUG) Log.v(TAG, "Voice Clicked");
    282                         hideNativeKeyboard();
    283                         mHandler.postDelayed(new Runnable() {
    284                             @Override
    285                             public void run() {
    286                                 if (DEBUG) Log.v(TAG, "Delayed action handling (voice_mode)");
    287                                 mAutoStartRecognition = true;
    288                                 mSpeechOrbView.requestFocus();
    289                             }
    290                         }, 500);
    291                 } else {
    292                     handled = false;
    293                 }
    294 
    295                 return handled;
    296             }
    297         });
    298 
    299         mSearchTextEditor.setPrivateImeOptions("EscapeNorth=1;VoiceDismiss=1;");
    300 
    301         mSpeechOrbView = (SpeechOrbView)findViewById(R.id.lb_search_bar_speech_orb);
    302         mSpeechOrbView.setOnOrbClickedListener(new OnClickListener() {
    303             @Override
    304             public void onClick(View view) {
    305                 toggleRecognition();
    306             }
    307         });
    308         mSpeechOrbView.setOnFocusChangeListener(new OnFocusChangeListener() {
    309             @Override
    310             public void onFocusChange(View view, boolean hasFocus) {
    311                 if (DEBUG) Log.v(TAG, "SpeechOrb.onFocusChange " + hasFocus);
    312                 if (hasFocus) {
    313                     hideNativeKeyboard();
    314                     if (mAutoStartRecognition) {
    315                         startRecognition();
    316                         mAutoStartRecognition = false;
    317                     }
    318                 } else {
    319                     stopRecognition();
    320                 }
    321                 updateUi(hasFocus);
    322             }
    323         });
    324 
    325         updateUi(hasFocus());
    326         updateHint();
    327     }
    328 
    329     @Override
    330     protected void onAttachedToWindow() {
    331         super.onAttachedToWindow();
    332         if (DEBUG) Log.v(TAG, "Loading soundPool");
    333         mSoundPool = new SoundPool(2, AudioManager.STREAM_SYSTEM, 0);
    334         loadSounds(mContext);
    335     }
    336 
    337     @Override
    338     protected void onDetachedFromWindow() {
    339         stopRecognition();
    340         if (DEBUG) Log.v(TAG, "Releasing SoundPool");
    341         mSoundPool.release();
    342         super.onDetachedFromWindow();
    343     }
    344 
    345     /**
    346      * Sets a listener for when the term search changes
    347      * @param listener
    348      */
    349     public void setSearchBarListener(SearchBarListener listener) {
    350         mSearchBarListener = listener;
    351     }
    352 
    353     /**
    354      * Sets the search query
    355      * @param query the search query to use
    356      */
    357     public void setSearchQuery(String query) {
    358         stopRecognition();
    359         mSearchTextEditor.setText(query);
    360         setSearchQueryInternal(query);
    361     }
    362 
    363     void setSearchQueryInternal(String query) {
    364         if (DEBUG) Log.v(TAG, "setSearchQueryInternal " + query);
    365         if (TextUtils.equals(mSearchQuery, query)) {
    366             return;
    367         }
    368         mSearchQuery = query;
    369 
    370         if (null != mSearchBarListener) {
    371             mSearchBarListener.onSearchQueryChange(mSearchQuery);
    372         }
    373     }
    374 
    375     /**
    376      * Sets the title text used in the hint shown in the search bar.
    377      * @param title The hint to use.
    378      */
    379     public void setTitle(String title) {
    380         mTitle = title;
    381         updateHint();
    382     }
    383 
    384     /**
    385      * Sets background color of not-listening state search orb.
    386      *
    387      * @param colors SearchOrbView.Colors.
    388      */
    389     public void setSearchAffordanceColors(SearchOrbView.Colors colors) {
    390         if (mSpeechOrbView != null) {
    391             mSpeechOrbView.setNotListeningOrbColors(colors);
    392         }
    393     }
    394 
    395     /**
    396      * Sets background color of listening state search orb.
    397      *
    398      * @param colors SearchOrbView.Colors.
    399      */
    400     public void setSearchAffordanceColorsInListening(SearchOrbView.Colors colors) {
    401         if (mSpeechOrbView != null) {
    402             mSpeechOrbView.setListeningOrbColors(colors);
    403         }
    404     }
    405 
    406     /**
    407      * Returns the current title
    408      */
    409     public String getTitle() {
    410         return mTitle;
    411     }
    412 
    413     /**
    414      * Returns the current search bar hint text.
    415      */
    416     public CharSequence getHint() {
    417         return mHint;
    418     }
    419 
    420     /**
    421      * Sets the badge drawable showing inside the search bar.
    422      * @param drawable The drawable to be used in the search bar.
    423      */
    424     public void setBadgeDrawable(Drawable drawable) {
    425         mBadgeDrawable = drawable;
    426         if (null != mBadgeView) {
    427             mBadgeView.setImageDrawable(drawable);
    428             if (null != drawable) {
    429                 mBadgeView.setVisibility(View.VISIBLE);
    430             } else {
    431                 mBadgeView.setVisibility(View.GONE);
    432             }
    433         }
    434     }
    435 
    436     /**
    437      * Returns the badge drawable
    438      */
    439     public Drawable getBadgeDrawable() {
    440         return mBadgeDrawable;
    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(List<String> completions) {
    449         List<CompletionInfo> infos = new ArrayList<>();
    450         if (null != completions) {
    451             for (String completion : completions) {
    452                 infos.add(new CompletionInfo(infos.size(), infos.size(), completion));
    453             }
    454         }
    455         CompletionInfo[] array = new CompletionInfo[infos.size()];
    456         displayCompletions(infos.toArray(array));
    457     }
    458 
    459     /**
    460      * Updates the completion list shown by the IME
    461      *
    462      * @param completions list of completions shown in the IME, can be null or empty to clear them
    463      */
    464     public void displayCompletions(CompletionInfo[] completions) {
    465         mInputMethodManager.displayCompletions(mSearchTextEditor, completions);
    466     }
    467 
    468     /**
    469      * Sets the speech recognizer to be used when doing voice search. The Activity/Fragment is in
    470      * charge of creating and destroying the recognizer with its own lifecycle.
    471      *
    472      * @param recognizer a SpeechRecognizer
    473      */
    474     public void setSpeechRecognizer(SpeechRecognizer recognizer) {
    475         stopRecognition();
    476         if (null != mSpeechRecognizer) {
    477             mSpeechRecognizer.setRecognitionListener(null);
    478             if (mListening) {
    479                 mSpeechRecognizer.cancel();
    480                 mListening = false;
    481             }
    482         }
    483         mSpeechRecognizer = recognizer;
    484         if (mSpeechRecognitionCallback != null && mSpeechRecognizer != null) {
    485             throw new IllegalStateException("Can't have speech recognizer and request");
    486         }
    487     }
    488 
    489     /**
    490      * Sets the speech recognition callback.
    491      *
    492      * @deprecated Launching voice recognition activity is no longer supported. App should declare
    493      *             android.permission.RECORD_AUDIO in AndroidManifest file. See details in
    494      *             {@link androidx.leanback.app.SearchSupportFragment}.
    495      */
    496     @Deprecated
    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         }
    584 
    585         mSpeechRecognizer.setRecognitionListener(null);
    586     }
    587 
    588     /**
    589      * Sets listener that handles runtime permission requests.
    590      * @param listener Listener that handles runtime permission requests.
    591      */
    592     public void setPermissionListener(SearchBarPermissionListener listener) {
    593         mPermissionListener = listener;
    594     }
    595 
    596     public void startRecognition() {
    597         if (DEBUG) Log.v(TAG, String.format("startRecognition (listening: %s, recognizing: %s)",
    598                 mListening, mRecognizing));
    599 
    600         if (mRecognizing) return;
    601         if (!hasFocus()) {
    602             requestFocus();
    603         }
    604         if (mSpeechRecognitionCallback != null) {
    605             mSearchTextEditor.setText("");
    606             mSearchTextEditor.setHint("");
    607             mSpeechRecognitionCallback.recognizeSpeech();
    608             mRecognizing = true;
    609             return;
    610         }
    611         if (null == mSpeechRecognizer) return;
    612         int res = getContext().checkCallingOrSelfPermission(Manifest.permission.RECORD_AUDIO);
    613         if (PackageManager.PERMISSION_GRANTED != res) {
    614             if (Build.VERSION.SDK_INT >= 23 && mPermissionListener != null) {
    615                 mPermissionListener.requestAudioPermission();
    616                 return;
    617             } else {
    618                 throw new IllegalStateException(Manifest.permission.RECORD_AUDIO
    619                         + " required for search");
    620             }
    621         }
    622 
    623         mRecognizing = true;
    624 
    625         mSearchTextEditor.setText("");
    626 
    627         Intent recognizerIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
    628 
    629         recognizerIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
    630                 RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
    631         recognizerIntent.putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, true);
    632 
    633         mSpeechRecognizer.setRecognitionListener(new RecognitionListener() {
    634             @Override
    635             public void onReadyForSpeech(Bundle bundle) {
    636                 if (DEBUG) Log.v(TAG, "onReadyForSpeech");
    637                 mSpeechOrbView.showListening();
    638                 playSearchOpen();
    639             }
    640 
    641             @Override
    642             public void onBeginningOfSpeech() {
    643                 if (DEBUG) Log.v(TAG, "onBeginningOfSpeech");
    644             }
    645 
    646             @Override
    647             public void onRmsChanged(float rmsdB) {
    648                 if (DEBUG) Log.v(TAG, "onRmsChanged " + rmsdB);
    649                 int level = rmsdB < 0 ? 0 : (int)(10 * rmsdB);
    650                 mSpeechOrbView.setSoundLevel(level);
    651             }
    652 
    653             @Override
    654             public void onBufferReceived(byte[] bytes) {
    655                 if (DEBUG) Log.v(TAG, "onBufferReceived " + bytes.length);
    656             }
    657 
    658             @Override
    659             public void onEndOfSpeech() {
    660                 if (DEBUG) Log.v(TAG, "onEndOfSpeech");
    661             }
    662 
    663             @Override
    664             public void onError(int error) {
    665                 if (DEBUG) Log.v(TAG, "onError " + error);
    666                 switch (error) {
    667                     case SpeechRecognizer.ERROR_NETWORK_TIMEOUT:
    668                         Log.w(TAG, "recognizer network timeout");
    669                         break;
    670                     case SpeechRecognizer.ERROR_NETWORK:
    671                         Log.w(TAG, "recognizer network error");
    672                         break;
    673                     case SpeechRecognizer.ERROR_AUDIO:
    674                         Log.w(TAG, "recognizer audio error");
    675                         break;
    676                     case SpeechRecognizer.ERROR_SERVER:
    677                         Log.w(TAG, "recognizer server error");
    678                         break;
    679                     case SpeechRecognizer.ERROR_CLIENT:
    680                         Log.w(TAG, "recognizer client error");
    681                         break;
    682                     case SpeechRecognizer.ERROR_SPEECH_TIMEOUT:
    683                         Log.w(TAG, "recognizer speech timeout");
    684                         break;
    685                     case SpeechRecognizer.ERROR_NO_MATCH:
    686                         Log.w(TAG, "recognizer no match");
    687                         break;
    688                     case SpeechRecognizer.ERROR_RECOGNIZER_BUSY:
    689                         Log.w(TAG, "recognizer busy");
    690                         break;
    691                     case SpeechRecognizer.ERROR_INSUFFICIENT_PERMISSIONS:
    692                         Log.w(TAG, "recognizer insufficient permissions");
    693                         break;
    694                     default:
    695                         Log.d(TAG, "recognizer other error");
    696                         break;
    697                 }
    698 
    699                 stopRecognition();
    700                 playSearchFailure();
    701             }
    702 
    703             @Override
    704             public void onResults(Bundle bundle) {
    705                 if (DEBUG) Log.v(TAG, "onResults");
    706                 final ArrayList<String> matches =
    707                         bundle.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION);
    708                 if (matches != null) {
    709                     if (DEBUG) Log.v(TAG, "Got results" + matches);
    710 
    711                     mSearchQuery = matches.get(0);
    712                     mSearchTextEditor.setText(mSearchQuery);
    713                     submitQuery();
    714                 }
    715 
    716                 stopRecognition();
    717                 playSearchSuccess();
    718             }
    719 
    720             @Override
    721             public void onPartialResults(Bundle bundle) {
    722                 ArrayList<String> results = bundle.getStringArrayList(
    723                         SpeechRecognizer.RESULTS_RECOGNITION);
    724                 if (DEBUG) {
    725                     Log.v(TAG, "onPartialResults " + bundle + " results "
    726                             + (results == null ? results : results.size()));
    727                 }
    728                 if (results == null || results.size() == 0) {
    729                     return;
    730                 }
    731 
    732                 // stableText: high confidence text from PartialResults, if any.
    733                 // Otherwise, existing stable text.
    734                 final String stableText = results.get(0);
    735                 if (DEBUG) Log.v(TAG, "onPartialResults stableText " + stableText);
    736 
    737                 // pendingText: low confidence text from PartialResults, if any.
    738                 // Otherwise, empty string.
    739                 final String pendingText = results.size() > 1 ? results.get(1) : null;
    740                 if (DEBUG) Log.v(TAG, "onPartialResults pendingText " + pendingText);
    741 
    742                 mSearchTextEditor.updateRecognizedText(stableText, pendingText);
    743             }
    744 
    745             @Override
    746             public void onEvent(int i, Bundle bundle) {
    747 
    748             }
    749         });
    750 
    751         mListening = true;
    752         mSpeechRecognizer.startListening(recognizerIntent);
    753     }
    754 
    755     void updateUi(boolean hasFocus) {
    756         if (hasFocus) {
    757             mBarBackground.setAlpha(mBackgroundSpeechAlpha);
    758             if (isVoiceMode()) {
    759                 mSearchTextEditor.setTextColor(mTextHintColorSpeechMode);
    760                 mSearchTextEditor.setHintTextColor(mTextHintColorSpeechMode);
    761             } else {
    762                 mSearchTextEditor.setTextColor(mTextColorSpeechMode);
    763                 mSearchTextEditor.setHintTextColor(mTextHintColorSpeechMode);
    764             }
    765         } else {
    766             mBarBackground.setAlpha(mBackgroundAlpha);
    767             mSearchTextEditor.setTextColor(mTextColor);
    768             mSearchTextEditor.setHintTextColor(mTextHintColor);
    769         }
    770 
    771         updateHint();
    772     }
    773 
    774     private boolean isVoiceMode() {
    775         return mSpeechOrbView.isFocused();
    776     }
    777 
    778     void submitQuery() {
    779         if (!TextUtils.isEmpty(mSearchQuery) && null != mSearchBarListener) {
    780             mSearchBarListener.onSearchQuerySubmit(mSearchQuery);
    781         }
    782     }
    783 
    784     private void loadSounds(Context context) {
    785         int[] sounds = {
    786                 R.raw.lb_voice_failure,
    787                 R.raw.lb_voice_open,
    788                 R.raw.lb_voice_no_input,
    789                 R.raw.lb_voice_success,
    790         };
    791         for (int sound : sounds) {
    792             mSoundMap.put(sound, mSoundPool.load(context, sound, 1));
    793         }
    794     }
    795 
    796     private void play(final int resId) {
    797         mHandler.post(new Runnable() {
    798             @Override
    799             public void run() {
    800                 int sound = mSoundMap.get(resId);
    801                 mSoundPool.play(sound, FULL_LEFT_VOLUME, FULL_RIGHT_VOLUME, DEFAULT_PRIORITY,
    802                         DO_NOT_LOOP, DEFAULT_RATE);
    803             }
    804         });
    805     }
    806 
    807     void playSearchOpen() {
    808         play(R.raw.lb_voice_open);
    809     }
    810 
    811     void playSearchFailure() {
    812         play(R.raw.lb_voice_failure);
    813     }
    814 
    815     private void playSearchNoInput() {
    816         play(R.raw.lb_voice_no_input);
    817     }
    818 
    819     void playSearchSuccess() {
    820         play(R.raw.lb_voice_success);
    821     }
    822 
    823     @Override
    824     public void setNextFocusDownId(int viewId) {
    825         mSpeechOrbView.setNextFocusDownId(viewId);
    826         mSearchTextEditor.setNextFocusDownId(viewId);
    827     }
    828 
    829 }
    830