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