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