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