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