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