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