SearchBar.java revision e5f2388b8d24876ebbd6daf302487bf452245d50
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 mSpeechRecognizer = SpeechRecognizer.createSpeechRecognizer(context); 138 139 140 mTextSpeechColor = r.getColor(R.color.lb_search_bar_text_speech_color); 141 mBackgroundSpeechAlpha = r.getInteger(R.integer.lb_search_bar_speech_mode_background_alpha); 142 143 mTextColor = r.getColor(R.color.lb_search_bar_text_color); 144 mBackgroundAlpha = r.getInteger(R.integer.lb_search_bar_text_mode_background_alpha); 145 } 146 147 @Override 148 protected void onFinishInflate() { 149 super.onFinishInflate(); 150 151 RelativeLayout items = (RelativeLayout)findViewById(R.id.lb_search_bar_items); 152 mBarBackground = items.getBackground(); 153 154 mSearchTextEditor = (SearchEditText)findViewById(R.id.lb_search_text_editor); 155 mBadgeView = (ImageView)findViewById(R.id.lb_search_bar_badge); 156 if (null != mBadgeDrawable) { 157 mBadgeView.setImageDrawable(mBadgeDrawable); 158 } 159 160 mSearchTextEditor.setOnFocusChangeListener(new OnFocusChangeListener() { 161 @Override 162 public void onFocusChange(View view, boolean hasFocus) { 163 if (DEBUG) Log.v(TAG, "EditText.onFocusChange " + hasFocus); 164 if (hasFocus) { 165 showNativeKeyboard(); 166 } 167 updateUi(); 168 } 169 }); 170 mSearchTextEditor.addTextChangedListener(new TextWatcher() { 171 @Override 172 public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) { 173 174 } 175 176 @Override 177 public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) { 178 if (mSearchTextEditor.hasFocus()) { 179 setSearchQuery(charSequence.toString()); 180 } 181 } 182 183 @Override 184 public void afterTextChanged(Editable editable) { 185 186 } 187 }); 188 mSearchTextEditor.setOnKeyboardDismissListener( 189 new SearchEditText.OnKeyboardDismissListener() { 190 @Override 191 public void onKeyboardDismiss() { 192 if (null != mSearchBarListener) { 193 mSearchBarListener.onKeyboardDismiss(mSearchQuery); 194 } 195 } 196 }); 197 198 mSearchTextEditor.setOnEditorActionListener(new TextView.OnEditorActionListener() { 199 @Override 200 public boolean onEditorAction(TextView textView, int action, KeyEvent keyEvent) { 201 if (DEBUG) Log.v(TAG, "onEditorAction: " + action + " event: " + keyEvent); 202 boolean handled = true; 203 if (EditorInfo.IME_ACTION_SEARCH == action && null != mSearchBarListener) { 204 if (DEBUG) Log.v(TAG, "Action Pressed"); 205 hideNativeKeyboard(); 206 mHandler.postDelayed(new Runnable() { 207 @Override 208 public void run() { 209 if (DEBUG) Log.v(TAG, "Delayed action handling (search)"); 210 submitQuery(); 211 } 212 }, 500); 213 214 } else if (EditorInfo.IME_ACTION_NONE == action && null != mSearchBarListener) { 215 if (DEBUG) Log.v(TAG, "Escaped North"); 216 hideNativeKeyboard(); 217 mHandler.postDelayed(new Runnable() { 218 @Override 219 public void run() { 220 if (DEBUG) Log.v(TAG, "Delayed action handling (escape_north)"); 221 mSearchBarListener.onKeyboardDismiss(mSearchQuery); 222 } 223 }, 500); 224 } else if (EditorInfo.IME_ACTION_GO == action) { 225 if (DEBUG) Log.v(TAG, "Voice Clicked"); 226 hideNativeKeyboard(); 227 mHandler.postDelayed(new Runnable() { 228 @Override 229 public void run() { 230 if (DEBUG) Log.v(TAG, "Delayed action handling (voice_mode)"); 231 mAutoStartRecognition = true; 232 mSpeechOrbView.requestFocus(); 233 } 234 }, 500); 235 } else { 236 handled = false; 237 } 238 239 return handled; 240 } 241 }); 242 243 mSearchTextEditor.setPrivateImeOptions("EscapeNorth=1;VoiceDismiss=1;"); 244 245 mSpeechOrbView = (SpeechOrbView)findViewById(R.id.lb_search_bar_speech_orb); 246 mSpeechOrbView.setOnOrbClickedListener(new OnClickListener() { 247 @Override 248 public void onClick(View view) { 249 startRecognition(); 250 } 251 }); 252 mSpeechOrbView.setOnFocusChangeListener(new OnFocusChangeListener() { 253 @Override 254 public void onFocusChange(View view, boolean hasFocus) { 255 if (DEBUG) Log.v(TAG, "SpeechOrb.onFocusChange " + hasFocus); 256 if (hasFocus) { 257 hideNativeKeyboard(); 258 if (mAutoStartRecognition) { 259 startRecognition(); 260 mAutoStartRecognition = false; 261 } 262 } else { 263 stopRecognition(); 264 } 265 updateUi(); 266 } 267 }); 268 269 updateHint(); 270 // Start in voice mode 271 mHandler.postDelayed(new Runnable() { 272 @Override 273 public void run() { 274 mAutoStartRecognition = true; 275 mSpeechOrbView.requestFocus(); 276 } 277 }, 200); 278 } 279 280 @Override 281 protected void onAttachedToWindow() { 282 super.onAttachedToWindow(); 283 mHandler.post(new Runnable() { 284 @Override 285 public void run() { 286 mSearchTextEditor.requestFocus(); 287 mSearchTextEditor.requestFocusFromTouch(); 288 } 289 }); 290 } 291 292 /** 293 * Set a listener for when the term search changes 294 * @param listener 295 */ 296 public void setSearchBarListener(SearchBarListener listener) { 297 mSearchBarListener = listener; 298 } 299 300 /** 301 * Set the search query 302 * @param query the search query to use 303 */ 304 public void setSearchQuery(String query) { 305 if (query.equals(mSearchQuery)) { 306 return; 307 } 308 mSearchQuery = query; 309 if (null != mSearchBarListener) { 310 mSearchBarListener.onSearchQueryChange(mSearchQuery); 311 } 312 } 313 314 /** 315 * Set the title text used in the hint shown in the search bar. 316 * @param title The hint to use. 317 */ 318 public void setTitle(String title) { 319 mTitle = title; 320 updateHint(); 321 } 322 323 /** 324 * Returns the current title 325 */ 326 public String getTitle() { 327 return mTitle; 328 } 329 330 /** 331 * Set the badge drawable showing inside the search bar. 332 * @param drawable The drawable to be used in the search bar. 333 */ 334 public void setBadgeDrawable(Drawable drawable) { 335 mBadgeDrawable = drawable; 336 if (null != mBadgeView) { 337 mBadgeView.setImageDrawable(drawable); 338 if (null != drawable) { 339 mBadgeView.setVisibility(View.VISIBLE); 340 } else { 341 mBadgeView.setVisibility(View.GONE); 342 } 343 } 344 } 345 346 /** 347 * Returns the badge drawable 348 */ 349 public Drawable getBadgeDrawable() { 350 return mBadgeDrawable; 351 } 352 353 /** 354 * Update the completion list shown by the IME 355 * 356 * @param completions list of completions shown in the IME, can be null or empty to clear them 357 */ 358 public void displayCompletions(List<String> completions) { 359 List<CompletionInfo> infos = new ArrayList<CompletionInfo>(); 360 if (null != completions) { 361 for (String completion : completions) { 362 infos.add(new CompletionInfo(infos.size(), infos.size(), completion)); 363 } 364 } 365 366 mInputMethodManager.displayCompletions(mSearchTextEditor, 367 infos.toArray(new CompletionInfo[] {})); 368 } 369 370 private void hideNativeKeyboard() { 371 mInputMethodManager.hideSoftInputFromWindow(mSearchTextEditor.getWindowToken(), 372 InputMethodManager.RESULT_UNCHANGED_SHOWN); 373 } 374 375 private void showNativeKeyboard() { 376 mHandler.post(new Runnable() { 377 @Override 378 public void run() { 379 mSearchTextEditor.requestFocusFromTouch(); 380 mSearchTextEditor.dispatchTouchEvent(MotionEvent.obtain(SystemClock.uptimeMillis(), 381 SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN, 382 mSearchTextEditor.getWidth(), mSearchTextEditor.getHeight(), 0)); 383 mSearchTextEditor.dispatchTouchEvent(MotionEvent.obtain(SystemClock.uptimeMillis(), 384 SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, 385 mSearchTextEditor.getWidth(), mSearchTextEditor.getHeight(), 0)); 386 } 387 }); 388 } 389 390 /** 391 * This will update the hint for the search bar properly depending on state and provided title 392 */ 393 private void updateHint() { 394 if (null == mSearchTextEditor) return; 395 396 String title = getResources().getString(R.string.lb_search_bar_hint); 397 if (!TextUtils.isEmpty(mTitle)) { 398 if (isVoiceMode()) { 399 title = getResources().getString(R.string.lb_search_bar_hint_with_title_speech, mTitle); 400 } else { 401 title = getResources().getString(R.string.lb_search_bar_hint_with_title, mTitle); 402 } 403 } else if (isVoiceMode()) { 404 title = getResources().getString(R.string.lb_search_bar_hint_speech); 405 } 406 mSearchTextEditor.setHint(title); 407 } 408 409 private void stopRecognition() { 410 if (DEBUG) Log.v(TAG, "stopRecognition " + mListening); 411 mSpeechOrbView.showNotListening(); 412 413 if (mListening) { 414 mSpeechRecognizer.cancel(); 415 } 416 } 417 418 private void startRecognition() { 419 if (DEBUG) Log.v(TAG, "startRecognition " + mListening); 420 421 mSearchTextEditor.setText(""); 422 423 Intent recognizerIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); 424 425 recognizerIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, 426 RecognizerIntent.LANGUAGE_MODEL_FREE_FORM); 427 recognizerIntent.putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, true); 428 429 mSpeechRecognizer.setRecognitionListener(new RecognitionListener() { 430 @Override 431 public void onReadyForSpeech(Bundle bundle) { 432 if (DEBUG) Log.v(TAG, "onReadyForSpeech"); 433 } 434 435 @Override 436 public void onBeginningOfSpeech() { 437 if (DEBUG) Log.v(TAG, "onBeginningOfSpeech"); 438 mListening = true; 439 } 440 441 @Override 442 public void onRmsChanged(float rmsdB) { 443 if (DEBUG) Log.v(TAG, "onRmsChanged " + rmsdB); 444 int level = rmsdB < 0 ? 0 : (int)(10 * rmsdB); 445 mSpeechOrbView.setSoundLevel(level); 446 } 447 448 @Override 449 public void onBufferReceived(byte[] bytes) { 450 if (DEBUG) Log.v(TAG, "onBufferReceived " + bytes.length); 451 } 452 453 @Override 454 public void onEndOfSpeech() { 455 if (DEBUG) Log.v(TAG, "onEndOfSpeech"); 456 mListening = false; 457 } 458 459 @Override 460 public void onError(int error) { 461 if (DEBUG) Log.v(TAG, "onError " + error); 462 switch (error) { 463 case SpeechRecognizer.ERROR_NO_MATCH: 464 Log.d(TAG, "recognizer error no match"); 465 break; 466 case SpeechRecognizer.ERROR_SERVER: 467 Log.d(TAG, "recognizer error server error"); 468 break; 469 case SpeechRecognizer.ERROR_SPEECH_TIMEOUT: 470 Log.d(TAG, "recognizer error speech timeout"); 471 break; 472 case SpeechRecognizer.ERROR_CLIENT: 473 Log.d(TAG, "recognizer error client error"); 474 break; 475 default: 476 Log.d(TAG, "recognizer other error"); 477 break; 478 } 479 480 mSpeechRecognizer.stopListening(); 481 mListening = false; 482 mSpeechRecognizer.setRecognitionListener(null); 483 mSpeechOrbView.showNotListening(); 484 } 485 486 @Override 487 public void onResults(Bundle bundle) { 488 if (DEBUG) Log.v(TAG, "onResults"); 489 final ArrayList<String> matches = 490 bundle.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION); 491 if (matches != null) { 492 Log.v(TAG, "Got results" + matches); 493 494 mSearchQuery = matches.get(0); 495 mSearchTextEditor.setText(mSearchQuery); 496 submitQuery(); 497 498 if (mListening) { 499 mSpeechRecognizer.stopListening(); 500 } 501 } 502 mSpeechRecognizer.setRecognitionListener(null); 503 mSpeechOrbView.showNotListening(); 504 } 505 506 @Override 507 public void onPartialResults(Bundle bundle) { 508 509 } 510 511 @Override 512 public void onEvent(int i, Bundle bundle) { 513 514 } 515 }); 516 517 mSpeechOrbView.showListening(); 518 mSpeechRecognizer.startListening(recognizerIntent); 519 mListening = true; 520 } 521 522 private void updateUi() { 523 if (DEBUG) Log.v(TAG, String.format("Update UI %s %s", 524 isVoiceMode() ? "Voice" : "Text", 525 hasFocus() ? "Focused" : "Unfocused")); 526 if (isVoiceMode()) { 527 mBarBackground.setAlpha(mBackgroundSpeechAlpha); 528 mSearchTextEditor.setTextColor(mTextSpeechColor); 529 } else { 530 mBarBackground.setAlpha(mBackgroundAlpha); 531 mSearchTextEditor.setTextColor(mTextColor); 532 } 533 534 updateHint(); 535 } 536 537 private boolean isVoiceMode() { 538 return mSpeechOrbView.isFocused(); 539 } 540 541 private void submitQuery() { 542 if (!TextUtils.isEmpty(mSearchQuery) && null != mSearchBarListener) { 543 mSearchBarListener.onSearchQuerySubmit(mSearchQuery); 544 } 545 } 546 547 private void enforceAudioRecordPermission() { 548 String permission = "android.permission.RECORD_AUDIO"; 549 int res = getContext().checkCallingOrSelfPermission(permission); 550 if (PackageManager.PERMISSION_GRANTED != res) { 551 throw new IllegalStateException("android.premission.RECORD_AUDIO required for search"); 552 } 553 } 554 555} 556