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 */ onSearchQueryChange(String query)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 */ onSearchQuerySubmit(String query)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 */ onKeyboardDismiss(String query)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 */ requestAudioPermission()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 SearchBar(Context context)150 public SearchBar(Context context) { 151 this(context, null); 152 } 153 SearchBar(Context context, AttributeSet attrs)154 public SearchBar(Context context, AttributeSet attrs) { 155 this(context, attrs, 0); 156 } 157 SearchBar(Context context, AttributeSet attrs, int defStyle)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 onFinishInflate()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 onAttachedToWindow()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 onDetachedFromWindow()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 */ setSearchBarListener(SearchBarListener listener)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 */ setSearchQuery(String query)357 public void setSearchQuery(String query) { 358 stopRecognition(); 359 mSearchTextEditor.setText(query); 360 setSearchQueryInternal(query); 361 } 362 setSearchQueryInternal(String query)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 */ setTitle(String title)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 */ setSearchAffordanceColors(SearchOrbView.Colors colors)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 */ setSearchAffordanceColorsInListening(SearchOrbView.Colors colors)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 */ getTitle()409 public String getTitle() { 410 return mTitle; 411 } 412 413 /** 414 * Returns the current search bar hint text. 415 */ getHint()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 */ setBadgeDrawable(Drawable drawable)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 */ getBadgeDrawable()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 */ displayCompletions(List<String> completions)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 */ displayCompletions(CompletionInfo[] completions)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 */ setSpeechRecognizer(SpeechRecognizer recognizer)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 setSpeechRecognitionCallback(SpeechRecognitionCallback request)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 hideNativeKeyboard()504 void hideNativeKeyboard() { 505 mInputMethodManager.hideSoftInputFromWindow(mSearchTextEditor.getWindowToken(), 506 InputMethodManager.RESULT_UNCHANGED_SHOWN); 507 } 508 showNativeKeyboard()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 */ updateHint()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 toggleRecognition()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 */ isRecognizing()556 public boolean isRecognizing() { 557 return mRecognizing; 558 } 559 560 /** 561 * Stops the speech recognition, if already started. 562 */ stopRecognition()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 */ setPermissionListener(SearchBarPermissionListener listener)592 public void setPermissionListener(SearchBarPermissionListener listener) { 593 mPermissionListener = listener; 594 } 595 startRecognition()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 updateUi(boolean hasFocus)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 isVoiceMode()774 private boolean isVoiceMode() { 775 return mSpeechOrbView.isFocused(); 776 } 777 submitQuery()778 void submitQuery() { 779 if (!TextUtils.isEmpty(mSearchQuery) && null != mSearchBarListener) { 780 mSearchBarListener.onSearchQuerySubmit(mSearchQuery); 781 } 782 } 783 loadSounds(Context context)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 play(final int resId)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 playSearchOpen()807 void playSearchOpen() { 808 play(R.raw.lb_voice_open); 809 } 810 playSearchFailure()811 void playSearchFailure() { 812 play(R.raw.lb_voice_failure); 813 } 814 playSearchNoInput()815 private void playSearchNoInput() { 816 play(R.raw.lb_voice_no_input); 817 } 818 playSearchSuccess()819 void playSearchSuccess() { 820 play(R.raw.lb_voice_success); 821 } 822 823 @Override setNextFocusDownId(int viewId)824 public void setNextFocusDownId(int viewId) { 825 mSpeechOrbView.setNextFocusDownId(viewId); 826 mSearchTextEditor.setNextFocusDownId(viewId); 827 } 828 829 } 830