SearchBar.java revision 76c3b90228d8c4afc6d24c683e9c95f41ae619c9
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.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; 46 47/** 48 * <p>SearchBar is a search widget.</p> 49 * 50 * <p>Note: Your application will need to request android.permission.RECORD_AUDIO</p> 51 */ 52public class SearchBar extends RelativeLayout { 53 private static final String TAG = SearchBar.class.getSimpleName(); 54 private static final boolean DEBUG = false; 55 56 private SpeechRecognizer mSpeechRecognizer; 57 private boolean mListening; 58 59 /** 60 * Listener for search query changes 61 */ 62 public interface SearchBarListener { 63 64 /** 65 * Method invoked when the search bar detects a change in the query. 66 * 67 * @param query The current full query. 68 */ 69 public void onSearchQueryChange(String query); 70 71 /** 72 * <p>Method invoked when the search query is submitted.</p> 73 * 74 * <p>This method can be called without a preceeding onSearchQueryChange, 75 * in particular in the case of a voice input.</p> 76 * 77 * @param query The query being submitted. 78 */ 79 public void onSearchQuerySubmit(String query); 80 81 /** 82 * Method invoked when the IME is being dismissed. 83 * 84 * @param query The query set in the search bar at the time the IME is being dismissed. 85 */ 86 public void onKeyboardDismiss(String query); 87 } 88 89 private SearchBarListener mSearchBarListener; 90 private SearchEditText mSearchTextEditor; 91 private SpeechOrbView mSpeechOrbView; 92 private ImageView mBadgeView; 93 private String mSearchQuery; 94 private String mTitle; 95 private Drawable mBadgeDrawable; 96 private final Handler mHandler = new Handler(); 97 private final InputMethodManager mInputMethodManager; 98 private boolean mAutoStartRecognition = false; 99 private Drawable mBarBackground; 100 101 private int mTextColor; 102 private int mTextSpeechColor; 103 private int mBackgroundAlpha; 104 private int mBackgroundSpeechAlpha; 105 private int mBarHeight; 106 107 public SearchBar(Context context) { 108 this(context, null); 109 } 110 111 public SearchBar(Context context, AttributeSet attrs) { 112 this(context, attrs, 0); 113 } 114 115 public SearchBar(Context context, AttributeSet attrs, int defStyle) { 116 super(context, attrs, defStyle); 117 enforceAudioRecordPermission(); 118 119 Resources r = getResources(); 120 121 LayoutInflater inflater = LayoutInflater.from(getContext()); 122 inflater.inflate(R.layout.lb_search_bar, this, true); 123 124 mBarHeight = getResources().getDimensionPixelSize(R.dimen.lb_search_bar_height); 125 RelativeLayout.LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 126 mBarHeight); 127 params.addRule(ALIGN_PARENT_TOP, RelativeLayout.TRUE); 128 setLayoutParams(params); 129 setBackgroundColor(Color.TRANSPARENT); 130 setClipChildren(false); 131 132 mSearchQuery = ""; 133 mInputMethodManager = 134 (InputMethodManager)context.getSystemService(Context.INPUT_METHOD_SERVICE); 135 mSpeechRecognizer = SpeechRecognizer.createSpeechRecognizer(context); 136 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 private void hideNativeKeyboard() { 352 mInputMethodManager.hideSoftInputFromWindow(mSearchTextEditor.getWindowToken(), 353 InputMethodManager.RESULT_UNCHANGED_SHOWN); 354 } 355 356 private void showNativeKeyboard() { 357 mHandler.post(new Runnable() { 358 @Override 359 public void run() { 360 mSearchTextEditor.requestFocusFromTouch(); 361 mSearchTextEditor.dispatchTouchEvent(MotionEvent.obtain(SystemClock.uptimeMillis(), 362 SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN, 363 mSearchTextEditor.getWidth(), mSearchTextEditor.getHeight(), 0)); 364 mSearchTextEditor.dispatchTouchEvent(MotionEvent.obtain(SystemClock.uptimeMillis(), 365 SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, 366 mSearchTextEditor.getWidth(), mSearchTextEditor.getHeight(), 0)); 367 } 368 }); 369 } 370 371 /** 372 * This will update the hint for the search bar properly depending on state and provided title 373 */ 374 private void updateHint() { 375 if (null == mSearchTextEditor) return; 376 377 String title = getResources().getString(R.string.lb_search_bar_hint); 378 if (!TextUtils.isEmpty(mTitle)) { 379 if (isVoiceMode()) { 380 title = getResources().getString(R.string.lb_search_bar_hint_with_title_speech, mTitle); 381 } else { 382 title = getResources().getString(R.string.lb_search_bar_hint_with_title, mTitle); 383 } 384 } else if (isVoiceMode()) { 385 title = getResources().getString(R.string.lb_search_bar_hint_speech); 386 } 387 mSearchTextEditor.setHint(title); 388 } 389 390 private void stopRecognition() { 391 if (DEBUG) Log.v(TAG, "stopRecognition " + mListening); 392 mSpeechOrbView.showNotListening(); 393 394 if (mListening) { 395 mSpeechRecognizer.cancel(); 396 } 397 } 398 399 private void startRecognition() { 400 if (DEBUG) Log.v(TAG, "startRecognition " + mListening); 401 402 mSearchTextEditor.setText(""); 403 404 Intent recognizerIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); 405 406 recognizerIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, 407 RecognizerIntent.LANGUAGE_MODEL_FREE_FORM); 408 recognizerIntent.putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, true); 409 410 mSpeechRecognizer.setRecognitionListener(new RecognitionListener() { 411 @Override 412 public void onReadyForSpeech(Bundle bundle) { 413 if (DEBUG) Log.v(TAG, "onReadyForSpeech"); 414 } 415 416 @Override 417 public void onBeginningOfSpeech() { 418 if (DEBUG) Log.v(TAG, "onBeginningOfSpeech"); 419 mListening = true; 420 } 421 422 @Override 423 public void onRmsChanged(float rmsdB) { 424 if (DEBUG) Log.v(TAG, "onRmsChanged " + rmsdB); 425 int level = rmsdB < 0 ? 0 : (int)(10 * rmsdB); 426 mSpeechOrbView.setSoundLevel(level); 427 } 428 429 @Override 430 public void onBufferReceived(byte[] bytes) { 431 if (DEBUG) Log.v(TAG, "onBufferReceived " + bytes.length); 432 } 433 434 @Override 435 public void onEndOfSpeech() { 436 if (DEBUG) Log.v(TAG, "onEndOfSpeech"); 437 mListening = false; 438 } 439 440 @Override 441 public void onError(int error) { 442 if (DEBUG) Log.v(TAG, "onError " + error); 443 switch (error) { 444 case SpeechRecognizer.ERROR_NO_MATCH: 445 Log.d(TAG, "recognizer error no match"); 446 break; 447 case SpeechRecognizer.ERROR_SERVER: 448 Log.d(TAG, "recognizer error server error"); 449 break; 450 case SpeechRecognizer.ERROR_SPEECH_TIMEOUT: 451 Log.d(TAG, "recognizer error speech timeout"); 452 break; 453 case SpeechRecognizer.ERROR_CLIENT: 454 Log.d(TAG, "recognizer error client error"); 455 break; 456 default: 457 Log.d(TAG, "recognizer other error"); 458 break; 459 } 460 461 mSpeechRecognizer.stopListening(); 462 mListening = false; 463 mSpeechRecognizer.setRecognitionListener(null); 464 mSpeechOrbView.showNotListening(); 465 } 466 467 @Override 468 public void onResults(Bundle bundle) { 469 if (DEBUG) Log.v(TAG, "onResults"); 470 final ArrayList<String> matches = 471 bundle.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION); 472 if (matches != null) { 473 Log.v(TAG, "Got results" + matches); 474 475 mSearchQuery = matches.get(0); 476 mSearchTextEditor.setText(mSearchQuery); 477 submitQuery(); 478 479 if (mListening) { 480 mSpeechRecognizer.stopListening(); 481 } 482 } 483 mSpeechRecognizer.setRecognitionListener(null); 484 mSpeechOrbView.showNotListening(); 485 } 486 487 @Override 488 public void onPartialResults(Bundle bundle) { 489 490 } 491 492 @Override 493 public void onEvent(int i, Bundle bundle) { 494 495 } 496 }); 497 498 mSpeechOrbView.showListening(); 499 mSpeechRecognizer.startListening(recognizerIntent); 500 mListening = true; 501 } 502 503 private void updateUi() { 504 if (DEBUG) Log.v(TAG, String.format("Update UI %s %s", 505 isVoiceMode() ? "Voice" : "Text", 506 hasFocus() ? "Focused" : "Unfocused")); 507 if (isVoiceMode()) { 508 mBarBackground.setAlpha(mBackgroundSpeechAlpha); 509 mSearchTextEditor.setTextColor(mTextSpeechColor); 510 } else { 511 mBarBackground.setAlpha(mBackgroundAlpha); 512 mSearchTextEditor.setTextColor(mTextColor); 513 } 514 515 updateHint(); 516 } 517 518 private boolean isVoiceMode() { 519 return mSpeechOrbView.isFocused(); 520 } 521 522 private void submitQuery() { 523 if (!TextUtils.isEmpty(mSearchQuery) && null != mSearchBarListener) { 524 mSearchBarListener.onSearchQuerySubmit(mSearchQuery); 525 } 526 } 527 528 private void enforceAudioRecordPermission() { 529 String permission = "android.permission.RECORD_AUDIO"; 530 int res = getContext().checkCallingOrSelfPermission(permission); 531 if (PackageManager.PERMISSION_GRANTED != res) { 532 throw new IllegalStateException("android.premission.RECORD_AUDIO required for search"); 533 } 534 } 535 536} 537