SearchBar.java revision 10960072d3c1f9c7f42f9ae77adbfb12f9aed138
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.util.TypedValue; 34import android.view.LayoutInflater; 35import android.view.ViewGroup; 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 private void hideNativeKeyboard() { 354 mInputMethodManager.hideSoftInputFromWindow(mSearchTextEditor.getWindowToken(), 355 InputMethodManager.RESULT_UNCHANGED_SHOWN); 356 } 357 358 private void showNativeKeyboard() { 359 mHandler.post(new Runnable() { 360 @Override 361 public void run() { 362 mSearchTextEditor.requestFocusFromTouch(); 363 mSearchTextEditor.dispatchTouchEvent(MotionEvent.obtain(SystemClock.uptimeMillis(), 364 SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN, 365 mSearchTextEditor.getWidth(), mSearchTextEditor.getHeight(), 0)); 366 mSearchTextEditor.dispatchTouchEvent(MotionEvent.obtain(SystemClock.uptimeMillis(), 367 SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, 368 mSearchTextEditor.getWidth(), mSearchTextEditor.getHeight(), 0)); 369 } 370 }); 371 } 372 373 /** 374 * This will update the hint for the search bar properly depending on state and provided title 375 */ 376 private void updateHint() { 377 if (null == mSearchTextEditor) return; 378 379 String title = getResources().getString(R.string.lb_search_bar_hint); 380 if (!TextUtils.isEmpty(mTitle)) { 381 if (isVoiceMode()) { 382 title = getResources().getString(R.string.lb_search_bar_hint_with_title_speech, mTitle); 383 } else { 384 title = getResources().getString(R.string.lb_search_bar_hint_with_title, mTitle); 385 } 386 } else if (isVoiceMode()) { 387 title = getResources().getString(R.string.lb_search_bar_hint_speech); 388 } 389 mSearchTextEditor.setHint(title); 390 } 391 392 private void stopRecognition() { 393 if (DEBUG) Log.v(TAG, "stopRecognition " + mListening); 394 mSpeechOrbView.showNotListening(); 395 396 if (mListening) { 397 mSpeechRecognizer.cancel(); 398 } 399 } 400 401 private void startRecognition() { 402 if (DEBUG) Log.v(TAG, "startRecognition " + mListening); 403 404 mSearchTextEditor.setText(""); 405 406 Intent recognizerIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); 407 408 recognizerIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, 409 RecognizerIntent.LANGUAGE_MODEL_FREE_FORM); 410 recognizerIntent.putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, true); 411 412 mSpeechRecognizer.setRecognitionListener(new RecognitionListener() { 413 @Override 414 public void onReadyForSpeech(Bundle bundle) { 415 if (DEBUG) Log.v(TAG, "onReadyForSpeech"); 416 } 417 418 @Override 419 public void onBeginningOfSpeech() { 420 if (DEBUG) Log.v(TAG, "onBeginningOfSpeech"); 421 mListening = true; 422 } 423 424 @Override 425 public void onRmsChanged(float rmsdB) { 426 if (DEBUG) Log.v(TAG, "onRmsChanged " + rmsdB); 427 int level = rmsdB < 0 ? 0 : (int)(10 * rmsdB); 428 mSpeechOrbView.setSoundLevel(level); 429 } 430 431 @Override 432 public void onBufferReceived(byte[] bytes) { 433 if (DEBUG) Log.v(TAG, "onBufferReceived " + bytes.length); 434 } 435 436 @Override 437 public void onEndOfSpeech() { 438 if (DEBUG) Log.v(TAG, "onEndOfSpeech"); 439 mListening = false; 440 } 441 442 @Override 443 public void onError(int error) { 444 if (DEBUG) Log.v(TAG, "onError " + error); 445 switch (error) { 446 case SpeechRecognizer.ERROR_NO_MATCH: 447 Log.d(TAG, "recognizer error no match"); 448 break; 449 case SpeechRecognizer.ERROR_SERVER: 450 Log.d(TAG, "recognizer error server error"); 451 break; 452 case SpeechRecognizer.ERROR_SPEECH_TIMEOUT: 453 Log.d(TAG, "recognizer error speech timeout"); 454 break; 455 case SpeechRecognizer.ERROR_CLIENT: 456 Log.d(TAG, "recognizer error client error"); 457 break; 458 default: 459 Log.d(TAG, "recognizer other error"); 460 break; 461 } 462 463 mSpeechRecognizer.stopListening(); 464 mListening = false; 465 mSpeechRecognizer.setRecognitionListener(null); 466 mSpeechOrbView.showNotListening(); 467 } 468 469 @Override 470 public void onResults(Bundle bundle) { 471 if (DEBUG) Log.v(TAG, "onResults"); 472 final ArrayList<String> matches = 473 bundle.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION); 474 if (matches != null) { 475 Log.v(TAG, "Got results" + matches); 476 477 mSearchQuery = matches.get(0); 478 mSearchTextEditor.setText(mSearchQuery); 479 submitQuery(); 480 481 if (mListening) { 482 mSpeechRecognizer.stopListening(); 483 } 484 } 485 mSpeechRecognizer.setRecognitionListener(null); 486 mSpeechOrbView.showNotListening(); 487 } 488 489 @Override 490 public void onPartialResults(Bundle bundle) { 491 492 } 493 494 @Override 495 public void onEvent(int i, Bundle bundle) { 496 497 } 498 }); 499 500 mSpeechOrbView.showListening(); 501 mSpeechRecognizer.startListening(recognizerIntent); 502 mListening = true; 503 } 504 505 private void updateUi() { 506 if (DEBUG) Log.v(TAG, String.format("Update UI %s %s", 507 isVoiceMode() ? "Voice" : "Text", 508 hasFocus() ? "Focused" : "Unfocused")); 509 if (isVoiceMode()) { 510 mBarBackground.setAlpha(mBackgroundSpeechAlpha); 511 mSearchTextEditor.setTextColor(mTextSpeechColor); 512 } else { 513 mBarBackground.setAlpha(mBackgroundAlpha); 514 mSearchTextEditor.setTextColor(mTextColor); 515 } 516 517 updateHint(); 518 } 519 520 private boolean isVoiceMode() { 521 return mSpeechOrbView.isFocused(); 522 } 523 524 private void submitQuery() { 525 if (!TextUtils.isEmpty(mSearchQuery) && null != mSearchBarListener) { 526 mSearchBarListener.onSearchQuerySubmit(mSearchQuery); 527 } 528 } 529 530 private void enforceAudioRecordPermission() { 531 String permission = "android.permission.RECORD_AUDIO"; 532 int res = getContext().checkCallingOrSelfPermission(permission); 533 if (PackageManager.PERMISSION_GRANTED != res) { 534 throw new IllegalStateException("android.premission.RECORD_AUDIO required for search"); 535 } 536 } 537 538} 539