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