SearchActivity.java revision 535931433926d342c6277034cad91143ae28b72d
1/* 2 * Copyright (C) 2009 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.quicksearchbox; 18 19import com.android.common.Search; 20import com.android.quicksearchbox.ui.CorpusViewFactory; 21import com.android.quicksearchbox.ui.SuggestionClickListener; 22import com.android.quicksearchbox.ui.SuggestionSelectionListener; 23import com.android.quicksearchbox.ui.SuggestionsAdapter; 24import com.android.quicksearchbox.ui.SuggestionsFooter; 25import com.android.quicksearchbox.ui.SuggestionsView; 26 27import android.app.Activity; 28import android.app.Dialog; 29import android.app.SearchManager; 30import android.content.Intent; 31import android.database.DataSetObserver; 32import android.graphics.drawable.Animatable; 33import android.graphics.drawable.Drawable; 34import android.net.Uri; 35import android.os.Bundle; 36import android.os.SystemClock; 37import android.text.Editable; 38import android.text.TextUtils; 39import android.text.TextWatcher; 40import android.util.Log; 41import android.view.KeyEvent; 42import android.view.Menu; 43import android.view.View; 44import android.view.ViewGroup; 45import android.view.View.OnFocusChangeListener; 46import android.view.inputmethod.InputMethodManager; 47import android.widget.EditText; 48import android.widget.ImageButton; 49 50import java.util.ArrayList; 51 52/** 53 * The main activity for Quick Search Box. Shows the search UI. 54 * 55 */ 56public class SearchActivity extends Activity { 57 58 private static final boolean DBG = true; 59 private static final String TAG = "QSB.SearchActivity"; 60 61 private static final String SCHEME_CORPUS = "qsb.corpus"; 62 63 public static final String INTENT_ACTION_QSB_AND_SELECT_CORPUS 64 = "com.android.quicksearchbox.action.QSB_AND_SELECT_CORPUS"; 65 66 // The string used for privateImeOptions to identify to the IME that it should not show 67 // a microphone button since one already exists in the search dialog. 68 // TODO: This should move to android-common or something. 69 private static final String IME_OPTION_NO_MICROPHONE = "nm"; 70 71 // Keys for the saved instance state. 72 private static final String INSTANCE_KEY_CORPUS = "corpus"; 73 private static final String INSTANCE_KEY_USER_QUERY = "query"; 74 75 // Dialog IDs 76 private static final int CORPUS_SELECTION_DIALOG = 0; 77 78 // Timestamp for last onCreate()/onNewIntent() call, as returned by SystemClock.uptimeMillis(). 79 private long mStartTime; 80 // Whether QSB is starting. True between the calls to onCreate()/onNewIntent() and onResume(). 81 private boolean mStarting; 82 // True if the user has taken some action, e.g. launching a search, voice search, 83 // or suggestions, since QSB was last started. 84 private boolean mTookAction; 85 86 protected SuggestionsAdapter mSuggestionsAdapter; 87 88 protected EditText mQueryTextView; 89 // True if the query was empty on the previous call to updateQuery() 90 protected boolean mQueryWasEmpty = true; 91 92 protected SuggestionsView mSuggestionsView; 93 protected SuggestionsFooter mSuggestionsFooter; 94 95 protected ImageButton mSearchGoButton; 96 protected ImageButton mVoiceSearchButton; 97 protected ImageButton mCorpusIndicator; 98 99 private Launcher mLauncher; 100 101 private Corpus mCorpus; 102 private Bundle mAppSearchData; 103 private boolean mUpdateSuggestions; 104 private String mUserQuery; 105 private boolean mSelectAll; 106 107 /** Called when the activity is first created. */ 108 @Override 109 public void onCreate(Bundle savedInstanceState) { 110 recordStartTime(); 111 if (DBG) Log.d(TAG, "onCreate()"); 112 super.onCreate(savedInstanceState); 113 114 setContentView(R.layout.search_activity); 115 116 mSuggestionsAdapter = getQsbApplication().createSuggestionsAdapter(); 117 118 mQueryTextView = (EditText) findViewById(R.id.search_src_text); 119 mSuggestionsView = (SuggestionsView) findViewById(R.id.suggestions); 120 mSuggestionsView.setSuggestionClickListener(new ClickHandler()); 121 mSuggestionsView.setSuggestionSelectionListener(new SelectionHandler()); 122 mSuggestionsView.setInteractionListener(new InputMethodCloser()); 123 mSuggestionsView.setOnKeyListener(new SuggestionsViewKeyListener()); 124 mSuggestionsView.setOnFocusChangeListener(new SuggestListFocusListener()); 125 126 mSuggestionsFooter = getQsbApplication().createSuggestionsFooter(); 127 ViewGroup footerFrame = (ViewGroup) findViewById(R.id.footer); 128 mSuggestionsFooter.addToContainer(footerFrame); 129 130 mSearchGoButton = (ImageButton) findViewById(R.id.search_go_btn); 131 mVoiceSearchButton = (ImageButton) findViewById(R.id.search_voice_btn); 132 mCorpusIndicator = (ImageButton) findViewById(R.id.corpus_indicator); 133 134 mLauncher = new Launcher(this); 135 136 mQueryTextView.addTextChangedListener(new SearchTextWatcher()); 137 mQueryTextView.setOnKeyListener(new QueryTextViewKeyListener()); 138 mQueryTextView.setOnFocusChangeListener(new QueryTextViewFocusListener()); 139 140 mCorpusIndicator.setOnClickListener(new CorpusIndicatorClickListener()); 141 142 mSearchGoButton.setOnClickListener(new SearchGoButtonClickListener()); 143 144 mVoiceSearchButton.setOnClickListener(new VoiceSearchButtonClickListener()); 145 146 ButtonsKeyListener buttonsKeyListener = new ButtonsKeyListener(); 147 mSearchGoButton.setOnKeyListener(buttonsKeyListener); 148 mVoiceSearchButton.setOnKeyListener(buttonsKeyListener); 149 mCorpusIndicator.setOnKeyListener(buttonsKeyListener); 150 151 mUpdateSuggestions = true; 152 153 // First get setup from intent 154 Intent intent = getIntent(); 155 setupFromIntent(intent); 156 // Then restore any saved instance state 157 restoreInstanceState(savedInstanceState); 158 159 // Do this at the end, to avoid updating the list view when setSource() 160 // is called. 161 mSuggestionsView.setAdapter(mSuggestionsAdapter); 162 mSuggestionsFooter.setAdapter(mSuggestionsAdapter); 163 } 164 165 @Override 166 protected void onNewIntent(Intent intent) { 167 recordStartTime(); 168 setIntent(intent); 169 setupFromIntent(intent); 170 } 171 172 private void recordStartTime() { 173 mStartTime = SystemClock.uptimeMillis(); 174 mStarting = true; 175 mTookAction = false; 176 } 177 178 protected void restoreInstanceState(Bundle savedInstanceState) { 179 if (savedInstanceState == null) return; 180 String corpusName = savedInstanceState.getString(INSTANCE_KEY_CORPUS); 181 String query = savedInstanceState.getString(INSTANCE_KEY_USER_QUERY); 182 setCorpus(getCorpus(corpusName)); 183 setUserQuery(query); 184 } 185 186 @Override 187 protected void onSaveInstanceState(Bundle outState) { 188 super.onSaveInstanceState(outState); 189 // We don't save appSearchData, since we always get the value 190 // from the intent and the user can't change it. 191 192 String corpusName = mCorpus == null ? null : mCorpus.getName(); 193 outState.putString(INSTANCE_KEY_CORPUS, corpusName); 194 outState.putString(INSTANCE_KEY_USER_QUERY, mUserQuery); 195 } 196 197 private void setupFromIntent(Intent intent) { 198 if (DBG) Log.d(TAG, "setupFromIntent(" + intent.toUri(0) + ")"); 199 Corpus corpus = getCorpusFromUri(intent.getData()); 200 String query = intent.getStringExtra(SearchManager.QUERY); 201 Bundle appSearchData = intent.getBundleExtra(SearchManager.APP_DATA); 202 203 setCorpus(corpus); 204 setUserQuery(query); 205 mSelectAll = intent.getBooleanExtra(SearchManager.EXTRA_SELECT_QUERY, false); 206 mAppSearchData = appSearchData; 207 208 if (INTENT_ACTION_QSB_AND_SELECT_CORPUS.equals(intent.getAction())) { 209 showCorpusSelectionDialog(); 210 } 211 } 212 213 public static Uri getCorpusUri(Corpus corpus) { 214 if (corpus == null) return null; 215 return new Uri.Builder() 216 .scheme(SCHEME_CORPUS) 217 .authority(corpus.getName()) 218 .build(); 219 } 220 221 private Corpus getCorpusFromUri(Uri uri) { 222 if (uri == null) return null; 223 if (!SCHEME_CORPUS.equals(uri.getScheme())) return null; 224 String name = uri.getAuthority(); 225 return getCorpus(name); 226 } 227 228 private Corpus getCorpus(String sourceName) { 229 if (sourceName == null) return null; 230 Corpus corpus = getCorpora().getCorpus(sourceName); 231 if (corpus == null) { 232 Log.w(TAG, "Unknown corpus " + sourceName); 233 return null; 234 } 235 return corpus; 236 } 237 238 private void setCorpus(Corpus corpus) { 239 if (DBG) Log.d(TAG, "setCorpus(" + corpus + ")"); 240 mCorpus = corpus; 241 Drawable sourceIcon; 242 if (corpus == null) { 243 sourceIcon = getCorpusViewFactory().getGlobalSearchIcon(); 244 } else { 245 sourceIcon = corpus.getCorpusIcon(); 246 } 247 mSuggestionsAdapter.setCorpus(corpus); 248 mCorpusIndicator.setImageDrawable(sourceIcon); 249 250 updateVoiceSearchButton(getQuery().length() == 0); 251 } 252 253 private QsbApplication getQsbApplication() { 254 return (QsbApplication) getApplication(); 255 } 256 257 private Corpora getCorpora() { 258 return getQsbApplication().getCorpora(); 259 } 260 261 private CorpusRanker getCorpusRanker() { 262 return getQsbApplication().getCorpusRanker(); 263 } 264 265 private ShortcutRepository getShortcutRepository() { 266 return getQsbApplication().getShortcutRepository(); 267 } 268 269 private SuggestionsProvider getSuggestionsProvider() { 270 return getQsbApplication().getSuggestionsProvider(mCorpus); 271 } 272 273 private CorpusViewFactory getCorpusViewFactory() { 274 return getQsbApplication().getCorpusViewFactory(); 275 } 276 277 private Logger getLogger() { 278 return getQsbApplication().getLogger(); 279 } 280 281 @Override 282 protected void onDestroy() { 283 if (DBG) Log.d(TAG, "onDestroy()"); 284 super.onDestroy(); 285 mSuggestionsFooter.setAdapter(null); 286 mSuggestionsView.setAdapter(null); // closes mSuggestionsAdapter 287 } 288 289 @Override 290 protected void onStop() { 291 if (DBG) Log.d(TAG, "onStop()"); 292 // Close all open suggestion cursors. The query will be redone in onResume() 293 // if we come back to this activity. 294 mSuggestionsAdapter.setSuggestions(null); 295 getQsbApplication().getShortcutRefresher().reset(); 296 super.onStop(); 297 } 298 299 @Override 300 protected void onResume() { 301 if (DBG) Log.d(TAG, "onResume()"); 302 super.onResume(); 303 setQuery(mUserQuery, mSelectAll); 304 // Only select everything the first time after creating the activity. 305 mSelectAll = false; 306 updateSuggestions(mUserQuery); 307 mQueryTextView.requestFocus(); 308 if (mStarting) { 309 mStarting = false; 310 // Start up latency should not exceed 2^31 ms (~ 25 days). Note that 311 // SystemClock.uptimeMillis() does not advance during deep sleep. 312 int latency = (int) (SystemClock.uptimeMillis() - mStartTime); 313 String source = getIntent().getStringExtra(Search.SOURCE); 314 getLogger().logStart(latency, source, mCorpus, 315 getSuggestionsProvider().getOrderedCorpora()); 316 } 317 } 318 319 @Override 320 public boolean onCreateOptionsMenu(Menu menu) { 321 super.onCreateOptionsMenu(menu); 322 SearchSettings.addSearchSettingsMenuItem(this, menu); 323 return true; 324 } 325 326 /** 327 * Sets the query as typed by the user. Does not update the suggestions 328 * or the text in the query box. 329 */ 330 protected void setUserQuery(String userQuery) { 331 if (userQuery == null) userQuery = ""; 332 mUserQuery = userQuery; 333 } 334 335 protected String getQuery() { 336 CharSequence q = mQueryTextView.getText(); 337 return q == null ? "" : q.toString(); 338 } 339 340 /** 341 * Restores the query entered by the user. 342 */ 343 private void restoreUserQuery() { 344 if (DBG) Log.d(TAG, "Restoring query to '" + mUserQuery + "'"); 345 setQuery(mUserQuery, false); 346 } 347 348 /** 349 * Sets the text in the query box. Does not update the suggestions, 350 * and does not change the saved user-entered query. 351 * {@link #restoreUserQuery()} will restore the query to the last 352 * user-entered query. 353 */ 354 private void setQuery(String query, boolean selectAll) { 355 mUpdateSuggestions = false; 356 mQueryTextView.setText(query); 357 setTextSelection(selectAll); 358 mUpdateSuggestions = true; 359 } 360 361 /** 362 * Sets the text selection in the query text view. 363 * 364 * @param selectAll If {@code true}, selects the entire query. 365 * If {@false}, no characters are selected, and the cursor is placed 366 * at the end of the query. 367 */ 368 private void setTextSelection(boolean selectAll) { 369 if (selectAll) { 370 mQueryTextView.setSelection(0, mQueryTextView.length()); 371 } else { 372 mQueryTextView.setSelection(mQueryTextView.length()); 373 } 374 } 375 376 private void updateQueryTextView(boolean queryEmpty) { 377 if (queryEmpty) { 378 mQueryTextView.setBackgroundResource(R.drawable.textfield_search_empty); 379 mQueryTextView.setCompoundDrawablesWithIntrinsicBounds( 380 R.drawable.placeholder_google, 0, 0, 0); 381 mQueryTextView.setCursorVisible(false); 382 } else { 383 mQueryTextView.setBackgroundResource(R.drawable.textfield_search); 384 mQueryTextView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); 385 mQueryTextView.setCursorVisible(true); 386 } 387 } 388 389 private void updateSearchGoButton(boolean queryEmpty) { 390 if (queryEmpty) { 391 mSearchGoButton.setVisibility(View.GONE); 392 } else { 393 mSearchGoButton.setVisibility(View.VISIBLE); 394 } 395 } 396 397 protected void updateVoiceSearchButton(boolean queryEmpty) { 398 if (queryEmpty && mLauncher.shouldShowVoiceSearch(mCorpus)) { 399 mVoiceSearchButton.setVisibility(View.VISIBLE); 400 mQueryTextView.setPrivateImeOptions(IME_OPTION_NO_MICROPHONE); 401 } else { 402 mVoiceSearchButton.setVisibility(View.GONE); 403 mQueryTextView.setPrivateImeOptions(null); 404 } 405 } 406 407 protected void showCorpusSelectionDialog() { 408 showDialog(CORPUS_SELECTION_DIALOG); 409 } 410 411 protected void dismissCorpusSelectionDialog() { 412 dismissDialog(CORPUS_SELECTION_DIALOG); 413 } 414 415 @Override 416 protected Dialog onCreateDialog(int id, Bundle args) { 417 switch (id) { 418 case CORPUS_SELECTION_DIALOG: 419 return createCorpusSelectionDialog(); 420 default: 421 throw new IllegalArgumentException("Unknown dialog: " + id); 422 } 423 } 424 425 @Override 426 protected void onPrepareDialog(int id, Dialog dialog, Bundle args) { 427 super.onPrepareDialog(id, dialog, args); 428 switch (id) { 429 case CORPUS_SELECTION_DIALOG: 430 prepareCorpusSelectionDialog((CorpusSelectionDialog) dialog); 431 break; 432 default: 433 throw new IllegalArgumentException("Unknown dialog: " + id); 434 } 435 } 436 437 protected CorpusSelectionDialog createCorpusSelectionDialog() { 438 return new CorpusSelectionDialog(this); 439 } 440 441 protected void prepareCorpusSelectionDialog(CorpusSelectionDialog dialog) { 442 dialog.setCorpus(mCorpus); 443 dialog.setQuery(getQuery()); 444 dialog.setAppData(mAppSearchData); 445 } 446 447 protected void onSearchClicked(int method) { 448 String query = getQuery(); 449 if (DBG) Log.d(TAG, "Search clicked, query=" + query); 450 Corpus searchCorpus = mLauncher.getSearchCorpus(getCorpora(), mCorpus); 451 if (searchCorpus == null) return; 452 453 mTookAction = true; 454 455 // Log search start 456 getLogger().logSearch(mCorpus, method, query.length()); 457 458 // Create shortcut 459 SuggestionData searchShortcut = searchCorpus.createSearchShortcut(query); 460 if (searchShortcut != null) { 461 DataSuggestionCursor cursor = new DataSuggestionCursor(query); 462 cursor.add(searchShortcut); 463 getShortcutRepository().reportClick(cursor, 0); 464 } 465 466 // Start search 467 Intent intent = searchCorpus.createSearchIntent(query, mAppSearchData); 468 mLauncher.launchIntent(intent); 469 } 470 471 protected void onVoiceSearchClicked() { 472 if (DBG) Log.d(TAG, "Voice Search clicked"); 473 Corpus searchCorpus = mLauncher.getSearchCorpus(getCorpora(), mCorpus); 474 if (searchCorpus == null) return; 475 476 mTookAction = true; 477 478 // Log voice search start 479 getLogger().logVoiceSearch(searchCorpus); 480 481 // Start voice search 482 Intent intent = searchCorpus.createVoiceSearchIntent(mAppSearchData); 483 mLauncher.launchIntent(intent); 484 } 485 486 protected SuggestionCursor getSuggestions() { 487 return mSuggestionsAdapter.getCurrentSuggestions(); 488 } 489 490 protected boolean launchSuggestion(int position) { 491 if (DBG) Log.d(TAG, "Launching suggestion " + position); 492 mTookAction = true; 493 SuggestionCursor suggestions = getSuggestions(); 494 495 // Log suggestion click 496 // TODO: This should be just the queried sources, but currently 497 // all sources are queried 498 ArrayList<Corpus> corpora = getCorpusRanker().rankCorpora(getCorpora().getEnabledCorpora()); 499 getLogger().logSuggestionClick(position, suggestions, corpora); 500 501 // Create shortcut 502 getShortcutRepository().reportClick(suggestions, position); 503 504 // Launch intent 505 Intent intent = mLauncher.getSuggestionIntent(suggestions, position, mAppSearchData); 506 mLauncher.launchIntent(intent); 507 508 return true; 509 } 510 511 protected boolean onSuggestionLongClicked(int position) { 512 if (DBG) Log.d(TAG, "Long clicked on suggestion " + position); 513 return false; 514 } 515 516 protected boolean onSuggestionKeyDown(int position, int keyCode, KeyEvent event) { 517 // Treat enter or search as a click 518 if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_SEARCH) { 519 return launchSuggestion(position); 520 } 521 522 if (keyCode == KeyEvent.KEYCODE_DPAD_UP 523 && mSuggestionsView.getSelectedItemPosition() == 0) { 524 // Moved up from the top suggestion, restore the user query and focus query box 525 if (DBG) Log.d(TAG, "Up and out"); 526 restoreUserQuery(); 527 return false; // let the framework handle the move 528 } 529 530 if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT 531 || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { 532 // Moved left / right from a suggestion, keep current query, move 533 // focus to query box, and move cursor to far left / right 534 if (DBG) Log.d(TAG, "Left/right on a suggestion"); 535 int cursorPos = (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) ? 0 : mQueryTextView.length(); 536 mQueryTextView.setSelection(cursorPos); 537 mQueryTextView.requestFocus(); 538 // TODO: should we modify the list selection? 539 return true; 540 } 541 542 return false; 543 } 544 545 protected void onSourceSelected() { 546 if (DBG) Log.d(TAG, "No suggestion selected"); 547 restoreUserQuery(); 548 } 549 550 protected int getSelectedPosition() { 551 return mSuggestionsView.getSelectedPosition(); 552 } 553 554 /** 555 * Hides the input method. 556 */ 557 protected void hideInputMethod() { 558 InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); 559 if (imm != null) { 560 imm.hideSoftInputFromWindow(mQueryTextView.getWindowToken(), 0); 561 } 562 } 563 564 protected void showInputMethodForQuery() { 565 InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); 566 if (imm != null) { 567 imm.showSoftInput(mQueryTextView, InputMethodManager.SHOW_IMPLICIT); 568 } 569 } 570 571 /** 572 * Hides the input method when the suggestions get focus. 573 */ 574 private class SuggestListFocusListener implements OnFocusChangeListener { 575 public void onFocusChange(View v, boolean focused) { 576 if (DBG) Log.d(TAG, "Suggestions focus change, now: " + focused); 577 if (focused) { 578 // The suggestions list got focus, hide the input method 579 hideInputMethod(); 580 } 581 } 582 } 583 584 private class QueryTextViewFocusListener implements OnFocusChangeListener { 585 public void onFocusChange(View v, boolean focused) { 586 if (DBG) Log.d(TAG, "Query focus change, now: " + focused); 587 if (focused) { 588 // The query box got focus, show the input method if the 589 // query box got focus? 590 showInputMethodForQuery(); 591 } 592 } 593 } 594 595 private void startSearchProgress() { 596 // TODO: Do we need a progress indicator? 597 } 598 599 private void stopSearchProgress() { 600 Drawable animation = mSearchGoButton.getDrawable(); 601 if (animation instanceof Animatable) { 602 // TODO: Is this needed, or is it done automatically when the 603 // animation is removed? 604 ((Animatable) animation).stop(); 605 } 606 mSearchGoButton.setImageResource(R.drawable.ic_btn_search); 607 } 608 609 private void updateSuggestions(String query) { 610 query = ltrim(query); 611 LatencyTracker latency = new LatencyTracker(TAG); 612 Suggestions suggestions = getSuggestionsProvider().getSuggestions(query); 613 latency.addEvent("getSuggestions_done"); 614 if (!suggestions.isDone()) { 615 suggestions.registerDataSetObserver(new ProgressUpdater(suggestions)); 616 startSearchProgress(); 617 } else { 618 stopSearchProgress(); 619 } 620 mSuggestionsAdapter.setSuggestions(suggestions); 621 latency.addEvent("shortcuts_shown"); 622 long userVisibleLatency = latency.getUserVisibleLatency(); 623 if (DBG) { 624 Log.d(TAG, "User visible latency (shortcuts): " + userVisibleLatency + " ms."); 625 } 626 } 627 628 private boolean forwardKeyToQueryTextView(int keyCode, KeyEvent event) { 629 if (!event.isSystem() && !isDpadKey(keyCode)) { 630 if (DBG) Log.d(TAG, "Forwarding key to query box: " + event); 631 if (mQueryTextView.requestFocus()) { 632 return mQueryTextView.dispatchKeyEvent(event); 633 } 634 } 635 return false; 636 } 637 638 private boolean isDpadKey(int keyCode) { 639 switch (keyCode) { 640 case KeyEvent.KEYCODE_DPAD_UP: 641 case KeyEvent.KEYCODE_DPAD_DOWN: 642 case KeyEvent.KEYCODE_DPAD_LEFT: 643 case KeyEvent.KEYCODE_DPAD_RIGHT: 644 case KeyEvent.KEYCODE_DPAD_CENTER: 645 return true; 646 default: 647 return false; 648 } 649 } 650 651 /** 652 * Filters the suggestions list when the search text changes. 653 */ 654 private class SearchTextWatcher implements TextWatcher { 655 public void afterTextChanged(Editable s) { 656 boolean empty = s.length() == 0; 657 if (empty != mQueryWasEmpty) { 658 mQueryWasEmpty = empty; 659 updateQueryTextView(empty); 660 updateSearchGoButton(empty); 661 updateVoiceSearchButton(empty); 662 } 663 if (mUpdateSuggestions) { 664 String query = s == null ? "" : s.toString(); 665 setUserQuery(query); 666 updateSuggestions(query); 667 } 668 } 669 670 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 671 } 672 673 public void onTextChanged(CharSequence s, int start, int before, int count) { 674 } 675 } 676 677 /** 678 * Handles non-text keys in the query text view. 679 */ 680 private class QueryTextViewKeyListener implements View.OnKeyListener { 681 public boolean onKey(View view, int keyCode, KeyEvent event) { 682 // Handle IME search action key 683 if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_UP) { 684 onSearchClicked(Logger.SEARCH_METHOD_KEYBOARD); 685 } 686 return false; 687 } 688 } 689 690 /** 691 * Handles key events on the search and voice search buttons, 692 * by refocusing to EditText. 693 */ 694 private class ButtonsKeyListener implements View.OnKeyListener { 695 public boolean onKey(View v, int keyCode, KeyEvent event) { 696 return forwardKeyToQueryTextView(keyCode, event); 697 } 698 } 699 700 /** 701 * Handles key events on the suggestions list view. 702 */ 703 private class SuggestionsViewKeyListener implements View.OnKeyListener { 704 public boolean onKey(View v, int keyCode, KeyEvent event) { 705 if (event.getAction() == KeyEvent.ACTION_DOWN) { 706 int position = getSelectedPosition(); 707 if (onSuggestionKeyDown(position, keyCode, event)) { 708 return true; 709 } 710 } 711 return forwardKeyToQueryTextView(keyCode, event); 712 } 713 } 714 715 private class InputMethodCloser implements SuggestionsView.InteractionListener { 716 public void onInteraction() { 717 hideInputMethod(); 718 } 719 } 720 721 private class ClickHandler implements SuggestionClickListener { 722 public void onSuggestionClicked(int position) { 723 launchSuggestion(position); 724 } 725 726 public boolean onSuggestionLongClicked(int position) { 727 return SearchActivity.this.onSuggestionLongClicked(position); 728 } 729 } 730 731 private class SelectionHandler implements SuggestionSelectionListener { 732 public void onSuggestionSelected(int position) { 733 SuggestionCursor suggestions = getSuggestions(); 734 suggestions.moveTo(position); 735 String displayQuery = suggestions.getSuggestionDisplayQuery(); 736 if (TextUtils.isEmpty(displayQuery)) { 737 restoreUserQuery(); 738 } else { 739 setQuery(displayQuery, false); 740 } 741 } 742 743 public void onNothingSelected() { 744 // This happens when a suggestion has been selected with the 745 // dpad / trackball and then a different UI element is touched. 746 // Do nothing, since we want to keep the query of the selection 747 // in the search box. 748 } 749 } 750 751 /** 752 * Listens for clicks on the source selector. 753 */ 754 private class SearchGoButtonClickListener implements View.OnClickListener { 755 public void onClick(View view) { 756 onSearchClicked(Logger.SEARCH_METHOD_BUTTON); 757 } 758 } 759 760 /** 761 * Listens for clicks on the search button. 762 */ 763 private class CorpusIndicatorClickListener implements View.OnClickListener { 764 public void onClick(View view) { 765 showCorpusSelectionDialog(); 766 } 767 } 768 769 /** 770 * Listens for clicks on the voice search button. 771 */ 772 private class VoiceSearchButtonClickListener implements View.OnClickListener { 773 public void onClick(View view) { 774 onVoiceSearchClicked(); 775 } 776 } 777 778 /** 779 * Updates the progress bar when the suggestions adapter changes its progress. 780 */ 781 private class ProgressUpdater extends DataSetObserver { 782 private final Suggestions mSuggestions; 783 784 public ProgressUpdater(Suggestions suggestions) { 785 mSuggestions = suggestions; 786 } 787 788 @Override 789 public void onChanged() { 790 if (mSuggestions.isDone()) { 791 stopSearchProgress(); 792 } 793 } 794 } 795 796 private static String ltrim(String text) { 797 int start = 0; 798 int length = text.length(); 799 while (start < length && Character.isWhitespace(text.charAt(start))) { 800 start++; 801 } 802 return start > 0 ? text.substring(start, length) : text; 803 } 804 805} 806