SearchActivity.java revision 839a9fd2828f37c9dc8345f93aefa5b8ad2f857f
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.SuggestionsAdapter; 23import com.android.quicksearchbox.ui.SuggestionsView; 24 25import android.app.Activity; 26import android.app.SearchManager; 27import android.content.DialogInterface; 28import android.content.Intent; 29import android.database.DataSetObserver; 30import android.graphics.drawable.Drawable; 31import android.net.Uri; 32import android.os.Bundle; 33import android.os.Debug; 34import android.os.Handler; 35import android.text.Editable; 36import android.text.TextUtils; 37import android.text.TextWatcher; 38import android.util.Log; 39import android.view.KeyEvent; 40import android.view.Menu; 41import android.view.View; 42import android.view.View.OnFocusChangeListener; 43import android.view.inputmethod.InputMethodManager; 44import android.widget.AbsListView; 45import android.widget.EditText; 46import android.widget.ImageButton; 47 48import java.io.File; 49import java.util.Collection; 50 51/** 52 * The main activity for Quick Search Box. Shows the search UI. 53 * 54 */ 55public class SearchActivity extends Activity { 56 57 private static final boolean DBG = true; 58 private static final String TAG = "QSB.SearchActivity"; 59 private static final boolean TRACE = false; 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_QUERY = "query"; 74 75 // Measures time from for last onCreate()/onNewIntent() call. 76 private LatencyTracker mStartLatencyTracker; 77 // Whether QSB is starting. True between the calls to onCreate()/onNewIntent() and onResume(). 78 private boolean mStarting; 79 // True if the user has taken some action, e.g. launching a search, voice search, 80 // or suggestions, since QSB was last started. 81 private boolean mTookAction; 82 83 private CorpusSelectionDialog mCorpusSelectionDialog; 84 85 protected SuggestionsAdapter mSuggestionsAdapter; 86 87 private CorporaObserver mCorporaObserver; 88 89 protected EditText mQueryTextView; 90 // True if the query was empty on the previous call to updateQuery() 91 protected boolean mQueryWasEmpty = true; 92 93 protected SuggestionsView mSuggestionsView; 94 95 protected ImageButton mSearchGoButton; 96 protected ImageButton mVoiceSearchButton; 97 protected ImageButton mCorpusIndicator; 98 99 private VoiceSearch mVoiceSearch; 100 101 private Corpus mCorpus; 102 private Bundle mAppSearchData; 103 private boolean mUpdateSuggestions; 104 105 private final Handler mHandler = new Handler(); 106 private final Runnable mUpdateSuggestionsTask = new Runnable() { 107 public void run() { 108 updateSuggestions(getQuery()); 109 } 110 }; 111 112 private final Runnable mShowInputMethodTask = new Runnable() { 113 public void run() { 114 showInputMethodForQuery(); 115 } 116 }; 117 118 /** Called when the activity is first created. */ 119 @Override 120 public void onCreate(Bundle savedInstanceState) { 121 if (TRACE) startMethodTracing(); 122 recordStartTime(); 123 if (DBG) Log.d(TAG, "onCreate()"); 124 super.onCreate(savedInstanceState); 125 126 setContentView(); 127 mSuggestionsAdapter = getQsbApplication().createSuggestionsAdapter(); 128 129 mQueryTextView = (EditText) findViewById(R.id.search_src_text); 130 mSuggestionsView = (SuggestionsView) findViewById(R.id.suggestions); 131 mSuggestionsView.setSuggestionClickListener(new ClickHandler()); 132 mSuggestionsView.setOnScrollListener(new InputMethodCloser()); 133 mSuggestionsView.setOnKeyListener(new SuggestionsViewKeyListener()); 134 mSuggestionsView.setOnFocusChangeListener(new SuggestListFocusListener()); 135 136 mSearchGoButton = (ImageButton) findViewById(R.id.search_go_btn); 137 mVoiceSearchButton = (ImageButton) findViewById(R.id.search_voice_btn); 138 mCorpusIndicator = (ImageButton) findViewById(R.id.corpus_indicator); 139 140 mVoiceSearch = new VoiceSearch(this); 141 142 mQueryTextView.addTextChangedListener(new SearchTextWatcher()); 143 mQueryTextView.setOnKeyListener(new QueryTextViewKeyListener()); 144 mQueryTextView.setOnFocusChangeListener(new QueryTextViewFocusListener()); 145 146 mCorpusIndicator.setOnClickListener(new CorpusIndicatorClickListener()); 147 148 mSearchGoButton.setOnClickListener(new SearchGoButtonClickListener()); 149 150 mVoiceSearchButton.setOnClickListener(new VoiceSearchButtonClickListener()); 151 152 ButtonsKeyListener buttonsKeyListener = new ButtonsKeyListener(); 153 mSearchGoButton.setOnKeyListener(buttonsKeyListener); 154 mVoiceSearchButton.setOnKeyListener(buttonsKeyListener); 155 mCorpusIndicator.setOnKeyListener(buttonsKeyListener); 156 157 mUpdateSuggestions = true; 158 159 // First get setup from intent 160 Intent intent = getIntent(); 161 setupFromIntent(intent); 162 // Then restore any saved instance state 163 restoreInstanceState(savedInstanceState); 164 165 // Do this at the end, to avoid updating the list view when setSource() 166 // is called. 167 mSuggestionsView.setAdapter(mSuggestionsAdapter); 168 169 mCorporaObserver = new CorporaObserver(); 170 getCorpora().registerDataSetObserver(mCorporaObserver); 171 } 172 173 protected void setContentView() { 174 setContentView(R.layout.search_activity); 175 } 176 177 private void startMethodTracing() { 178 File traceDir = getDir("traces", 0); 179 String traceFile = new File(traceDir, "qsb.trace").getAbsolutePath(); 180 Debug.startMethodTracing(traceFile); 181 } 182 183 @Override 184 protected void onNewIntent(Intent intent) { 185 if (DBG) Log.d(TAG, "onNewIntent()"); 186 recordStartTime(); 187 setIntent(intent); 188 setupFromIntent(intent); 189 } 190 191 private void recordStartTime() { 192 mStartLatencyTracker = new LatencyTracker(); 193 mStarting = true; 194 mTookAction = false; 195 } 196 197 protected void restoreInstanceState(Bundle savedInstanceState) { 198 if (savedInstanceState == null) return; 199 String corpusName = savedInstanceState.getString(INSTANCE_KEY_CORPUS); 200 String query = savedInstanceState.getString(INSTANCE_KEY_QUERY); 201 setCorpus(corpusName); 202 setQuery(query, false); 203 } 204 205 @Override 206 protected void onSaveInstanceState(Bundle outState) { 207 super.onSaveInstanceState(outState); 208 // We don't save appSearchData, since we always get the value 209 // from the intent and the user can't change it. 210 211 outState.putString(INSTANCE_KEY_CORPUS, getCorpusName()); 212 outState.putString(INSTANCE_KEY_QUERY, getQuery()); 213 } 214 215 private void setupFromIntent(Intent intent) { 216 if (DBG) Log.d(TAG, "setupFromIntent(" + intent.toUri(0) + ")"); 217 String corpusName = getCorpusNameFromUri(intent.getData()); 218 String query = intent.getStringExtra(SearchManager.QUERY); 219 Bundle appSearchData = intent.getBundleExtra(SearchManager.APP_DATA); 220 boolean selectAll = intent.getBooleanExtra(SearchManager.EXTRA_SELECT_QUERY, false); 221 222 setCorpus(corpusName); 223 setQuery(query, selectAll); 224 mAppSearchData = appSearchData; 225 226 if (startedIntoCorpusSelectionDialog()) { 227 showCorpusSelectionDialog(); 228 } 229 } 230 231 public boolean startedIntoCorpusSelectionDialog() { 232 return INTENT_ACTION_QSB_AND_SELECT_CORPUS.equals(getIntent().getAction()); 233 } 234 235 /** 236 * Removes corpus selector intent action, so that BACK works normally after 237 * dismissing and reopening the corpus selector. 238 */ 239 private void clearStartedIntoCorpusSelectionDialog() { 240 Intent oldIntent = getIntent(); 241 if (SearchActivity.INTENT_ACTION_QSB_AND_SELECT_CORPUS.equals(oldIntent.getAction())) { 242 Intent newIntent = new Intent(oldIntent); 243 newIntent.setAction(SearchManager.INTENT_ACTION_GLOBAL_SEARCH); 244 setIntent(newIntent); 245 } 246 } 247 248 public static Uri getCorpusUri(Corpus corpus) { 249 if (corpus == null) return null; 250 return new Uri.Builder() 251 .scheme(SCHEME_CORPUS) 252 .authority(corpus.getName()) 253 .build(); 254 } 255 256 private String getCorpusNameFromUri(Uri uri) { 257 if (uri == null) return null; 258 if (!SCHEME_CORPUS.equals(uri.getScheme())) return null; 259 return uri.getAuthority(); 260 } 261 262 private Corpus getCorpus(String sourceName) { 263 if (sourceName == null) return null; 264 Corpus corpus = getCorpora().getCorpus(sourceName); 265 if (corpus == null) { 266 Log.w(TAG, "Unknown corpus " + sourceName); 267 return null; 268 } 269 return corpus; 270 } 271 272 private void setCorpus(String corpusName) { 273 if (DBG) Log.d(TAG, "setCorpus(" + corpusName + ")"); 274 mCorpus = getCorpus(corpusName); 275 Drawable sourceIcon; 276 if (mCorpus == null) { 277 sourceIcon = getCorpusViewFactory().getGlobalSearchIcon(); 278 } else { 279 sourceIcon = mCorpus.getCorpusIcon(); 280 } 281 mSuggestionsAdapter.setCorpus(mCorpus); 282 mCorpusIndicator.setImageDrawable(sourceIcon); 283 284 updateUi(getQuery().length() == 0); 285 } 286 287 private String getCorpusName() { 288 return mCorpus == null ? null : mCorpus.getName(); 289 } 290 291 private QsbApplication getQsbApplication() { 292 return (QsbApplication) getApplication(); 293 } 294 295 private Config getConfig() { 296 return getQsbApplication().getConfig(); 297 } 298 299 private Corpora getCorpora() { 300 return getQsbApplication().getCorpora(); 301 } 302 303 private ShortcutRepository getShortcutRepository() { 304 return getQsbApplication().getShortcutRepository(); 305 } 306 307 private SuggestionsProvider getSuggestionsProvider() { 308 return getQsbApplication().getSuggestionsProvider(); 309 } 310 311 private CorpusViewFactory getCorpusViewFactory() { 312 return getQsbApplication().getCorpusViewFactory(); 313 } 314 315 private Logger getLogger() { 316 return getQsbApplication().getLogger(); 317 } 318 319 @Override 320 protected void onDestroy() { 321 if (DBG) Log.d(TAG, "onDestroy()"); 322 super.onDestroy(); 323 getCorpora().unregisterDataSetObserver(mCorporaObserver); 324 mSuggestionsView.setAdapter(null); // closes mSuggestionsAdapter 325 } 326 327 @Override 328 protected void onStop() { 329 if (DBG) Log.d(TAG, "onStop()"); 330 if (!mTookAction) { 331 // TODO: This gets logged when starting other activities, e.g. by opening he search 332 // settings, or clicking a notification in the status bar. 333 getLogger().logExit(getCurrentSuggestions(), getQuery().length()); 334 } 335 // Close all open suggestion cursors. The query will be redone in onResume() 336 // if we come back to this activity. 337 mSuggestionsAdapter.setSuggestions(null); 338 getQsbApplication().getShortcutRefresher().reset(); 339 dismissCorpusSelectionDialog(); 340 super.onStop(); 341 } 342 343 @Override 344 protected void onRestart() { 345 if (DBG) Log.d(TAG, "onRestart()"); 346 super.onRestart(); 347 } 348 349 @Override 350 protected void onResume() { 351 if (DBG) Log.d(TAG, "onResume()"); 352 super.onResume(); 353 updateSuggestionsBuffered(); 354 if (!isCorpusSelectionDialogShowing()) { 355 mQueryTextView.requestFocus(); 356 } 357 if (TRACE) Debug.stopMethodTracing(); 358 } 359 360 @Override 361 public boolean onCreateOptionsMenu(Menu menu) { 362 super.onCreateOptionsMenu(menu); 363 SearchSettings.addSearchSettingsMenuItem(this, menu); 364 return true; 365 } 366 367 @Override 368 public void onWindowFocusChanged(boolean hasFocus) { 369 super.onWindowFocusChanged(hasFocus); 370 if (hasFocus) { 371 // Launch the IME after a bit 372 mHandler.postDelayed(mShowInputMethodTask, 0); 373 } 374 } 375 376 protected String getQuery() { 377 CharSequence q = mQueryTextView.getText(); 378 return q == null ? "" : q.toString(); 379 } 380 381 /** 382 * Sets the text in the query box. Does not update the suggestions, 383 * and does not change the saved user-entered query. 384 * {@link #restoreUserQuery()} will restore the query to the last 385 * user-entered query. 386 */ 387 private void setQuery(String query, boolean selectAll) { 388 mUpdateSuggestions = false; 389 mQueryTextView.setText(query); 390 setTextSelection(selectAll); 391 mUpdateSuggestions = true; 392 } 393 394 /** 395 * Sets the text selection in the query text view. 396 * 397 * @param selectAll If {@code true}, selects the entire query. 398 * If {@false}, no characters are selected, and the cursor is placed 399 * at the end of the query. 400 */ 401 private void setTextSelection(boolean selectAll) { 402 if (selectAll) { 403 mQueryTextView.selectAll(); 404 } else { 405 mQueryTextView.setSelection(mQueryTextView.length()); 406 } 407 } 408 409 protected void updateUi(boolean queryEmpty) { 410 updateQueryTextView(queryEmpty); 411 updateSearchGoButton(queryEmpty); 412 updateVoiceSearchButton(queryEmpty); 413 } 414 415 private void updateQueryTextView(boolean queryEmpty) { 416 if (queryEmpty) { 417 if (mCorpus == null || mCorpus.isWebCorpus()) { 418 mQueryTextView.setBackgroundResource(R.drawable.textfield_search_empty_google); 419 mQueryTextView.setHint(null); 420 } else { 421 mQueryTextView.setBackgroundResource(R.drawable.textfield_search_empty); 422 mQueryTextView.setHint(mCorpus.getHint()); 423 } 424 } else { 425 mQueryTextView.setBackgroundResource(R.drawable.textfield_search); 426 } 427 } 428 429 private void updateSearchGoButton(boolean queryEmpty) { 430 if (queryEmpty) { 431 mSearchGoButton.setVisibility(View.GONE); 432 } else { 433 mSearchGoButton.setVisibility(View.VISIBLE); 434 } 435 } 436 437 protected void updateVoiceSearchButton(boolean queryEmpty) { 438 if (queryEmpty && mVoiceSearch.shouldShowVoiceSearch(mCorpus)) { 439 mVoiceSearchButton.setVisibility(View.VISIBLE); 440 mQueryTextView.setPrivateImeOptions(IME_OPTION_NO_MICROPHONE); 441 } else { 442 mVoiceSearchButton.setVisibility(View.GONE); 443 mQueryTextView.setPrivateImeOptions(null); 444 } 445 } 446 447 protected void showCorpusSelectionDialog() { 448 if (mCorpusSelectionDialog == null) { 449 mCorpusSelectionDialog = new CorpusSelectionDialog(this); 450 mCorpusSelectionDialog.setOwnerActivity(this); 451 mCorpusSelectionDialog.setOnDismissListener(new CorpusSelectorDismissListener()); 452 mCorpusSelectionDialog.setOnCorpusSelectedListener(new CorpusSelectionListener()); 453 } 454 mCorpusSelectionDialog.show(mCorpus); 455 } 456 457 protected boolean isCorpusSelectionDialogShowing() { 458 return mCorpusSelectionDialog != null && mCorpusSelectionDialog.isShowing(); 459 } 460 461 protected void dismissCorpusSelectionDialog() { 462 if (mCorpusSelectionDialog != null) { 463 mCorpusSelectionDialog.dismiss(); 464 } 465 } 466 467 /** 468 * @return true if a search was performed as a result of this click, false otherwise. 469 */ 470 protected boolean onSearchClicked(int method) { 471 String query = ltrim(getQuery()); 472 if (DBG) Log.d(TAG, "Search clicked, query=" + query); 473 474 // Don't do empty queries 475 if (TextUtils.getTrimmedLength(query) == 0) return false; 476 477 Corpus searchCorpus = getSearchCorpus(); 478 if (searchCorpus == null) return false; 479 480 mTookAction = true; 481 482 // Log search start 483 getLogger().logSearch(mCorpus, method, query.length()); 484 485 // Create shortcut 486 SuggestionData searchShortcut = searchCorpus.createSearchShortcut(query); 487 if (searchShortcut != null) { 488 DataSuggestionCursor cursor = new DataSuggestionCursor(query); 489 cursor.add(searchShortcut); 490 getShortcutRepository().reportClick(cursor, 0); 491 } 492 493 // Start search 494 Intent intent = searchCorpus.createSearchIntent(query, mAppSearchData); 495 launchIntent(intent); 496 return true; 497 } 498 499 protected void onVoiceSearchClicked() { 500 if (DBG) Log.d(TAG, "Voice Search clicked"); 501 Corpus searchCorpus = getSearchCorpus(); 502 if (searchCorpus == null) return; 503 504 mTookAction = true; 505 506 // Log voice search start 507 getLogger().logVoiceSearch(searchCorpus); 508 509 // Start voice search 510 Intent intent = searchCorpus.createVoiceSearchIntent(mAppSearchData); 511 launchIntent(intent); 512 } 513 514 /** 515 * Gets the corpus to use for any searches. This is the web corpus in "All" mode, 516 * and the selected corpus otherwise. 517 */ 518 protected Corpus getSearchCorpus() { 519 if (mCorpus != null) { 520 return mCorpus; 521 } else { 522 Corpus webCorpus = getCorpora().getWebCorpus(); 523 if (webCorpus == null) { 524 Log.e(TAG, "No web corpus"); 525 } 526 return webCorpus; 527 } 528 } 529 530 protected SuggestionCursor getCurrentSuggestions() { 531 return mSuggestionsAdapter.getCurrentSuggestions(); 532 } 533 534 protected void launchIntent(Intent intent) { 535 if (intent == null) { 536 return; 537 } 538 try { 539 startActivity(intent); 540 } catch (RuntimeException ex) { 541 // Since the intents for suggestions specified by suggestion providers, 542 // guard against them not being handled, not allowed, etc. 543 Log.e(TAG, "Failed to start " + intent.toUri(0), ex); 544 } 545 } 546 547 protected boolean launchSuggestion(int position) { 548 SuggestionCursor suggestions = getCurrentSuggestions(); 549 if (position < 0 || position >= suggestions.getCount()) { 550 Log.w(TAG, "Tried to launch invalid suggestion " + position); 551 return false; 552 } 553 554 if (DBG) Log.d(TAG, "Launching suggestion " + position); 555 mTookAction = true; 556 557 // Log suggestion click 558 Collection<Corpus> corpora = mSuggestionsAdapter.getSuggestions().getIncludedCorpora(); 559 getLogger().logSuggestionClick(position, suggestions, corpora); 560 561 // Create shortcut 562 getShortcutRepository().reportClick(suggestions, position); 563 564 // Launch intent 565 suggestions.moveTo(position); 566 Intent intent = suggestions.getSuggestionIntent(mAppSearchData); 567 launchIntent(intent); 568 569 return true; 570 } 571 572 protected boolean onSuggestionLongClicked(int position) { 573 if (DBG) Log.d(TAG, "Long clicked on suggestion " + position); 574 return false; 575 } 576 577 protected boolean onSuggestionKeyDown(int position, int keyCode, KeyEvent event) { 578 // Treat enter or search as a click 579 if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_SEARCH) { 580 return launchSuggestion(position); 581 } 582 583 return false; 584 } 585 586 protected int getSelectedPosition() { 587 return mSuggestionsView.getSelectedPosition(); 588 } 589 590 /** 591 * Hides the input method. 592 */ 593 protected void hideInputMethod() { 594 InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); 595 if (imm != null) { 596 imm.hideSoftInputFromWindow(mQueryTextView.getWindowToken(), 0); 597 } 598 } 599 600 protected void showInputMethodForQuery() { 601 InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); 602 if (imm != null) { 603 imm.showSoftInput(mQueryTextView, 0); 604 } 605 } 606 607 /** 608 * Hides the input method when the suggestions get focus. 609 */ 610 private class SuggestListFocusListener implements OnFocusChangeListener { 611 public void onFocusChange(View v, boolean focused) { 612 if (DBG) Log.d(TAG, "Suggestions focus change, now: " + focused); 613 if (focused) { 614 // The suggestions list got focus, hide the input method 615 hideInputMethod(); 616 } 617 } 618 } 619 620 private class QueryTextViewFocusListener implements OnFocusChangeListener { 621 public void onFocusChange(View v, boolean focused) { 622 if (DBG) Log.d(TAG, "Query focus change, now: " + focused); 623 if (focused) { 624 // The query box got focus, show the input method 625 showInputMethodForQuery(); 626 } 627 } 628 } 629 630 private int getMaxSuggestions() { 631 Config config = getConfig(); 632 return mCorpus == null 633 ? config.getMaxPromotedSuggestions() 634 : config.getMaxResultsPerSource(); 635 } 636 637 private void updateSuggestionsBuffered() { 638 mHandler.removeCallbacks(mUpdateSuggestionsTask); 639 long delay = getConfig().getTypingUpdateSuggestionsDelayMillis(); 640 mHandler.postDelayed(mUpdateSuggestionsTask, delay); 641 } 642 643 protected void updateSuggestions(String query) { 644 645 query = ltrim(query); 646 Suggestions suggestions = getSuggestionsProvider().getSuggestions( 647 query, mCorpus, getMaxSuggestions()); 648 649 // Log start latency if this is the first suggestions update 650 if (mStarting) { 651 mStarting = false; 652 String source = getIntent().getStringExtra(Search.SOURCE); 653 int latency = mStartLatencyTracker.getLatency(); 654 getLogger().logStart(latency, source, mCorpus, suggestions.getExpectedCorpora()); 655 } 656 657 mSuggestionsAdapter.setSuggestions(suggestions); 658 } 659 660 private boolean forwardKeyToQueryTextView(int keyCode, KeyEvent event) { 661 if (!event.isSystem() && !isDpadKey(keyCode)) { 662 if (DBG) Log.d(TAG, "Forwarding key to query box: " + event); 663 if (mQueryTextView.requestFocus()) { 664 return mQueryTextView.dispatchKeyEvent(event); 665 } 666 } 667 return false; 668 } 669 670 private boolean isDpadKey(int keyCode) { 671 switch (keyCode) { 672 case KeyEvent.KEYCODE_DPAD_UP: 673 case KeyEvent.KEYCODE_DPAD_DOWN: 674 case KeyEvent.KEYCODE_DPAD_LEFT: 675 case KeyEvent.KEYCODE_DPAD_RIGHT: 676 case KeyEvent.KEYCODE_DPAD_CENTER: 677 return true; 678 default: 679 return false; 680 } 681 } 682 683 /** 684 * Filters the suggestions list when the search text changes. 685 */ 686 private class SearchTextWatcher implements TextWatcher { 687 public void afterTextChanged(Editable s) { 688 boolean empty = s.length() == 0; 689 if (empty != mQueryWasEmpty) { 690 mQueryWasEmpty = empty; 691 updateUi(empty); 692 } 693 if (mUpdateSuggestions) { 694 updateSuggestionsBuffered(); 695 } 696 } 697 698 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 699 } 700 701 public void onTextChanged(CharSequence s, int start, int before, int count) { 702 } 703 } 704 705 /** 706 * Handles non-text keys in the query text view. 707 */ 708 private class QueryTextViewKeyListener implements View.OnKeyListener { 709 public boolean onKey(View view, int keyCode, KeyEvent event) { 710 // Handle IME search action key 711 if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_UP) { 712 // if no action was taken, consume the key event so that the keyboard 713 // remains on screen. 714 return !onSearchClicked(Logger.SEARCH_METHOD_KEYBOARD); 715 } 716 return false; 717 } 718 } 719 720 /** 721 * Handles key events on the search and voice search buttons, 722 * by refocusing to EditText. 723 */ 724 private class ButtonsKeyListener implements View.OnKeyListener { 725 public boolean onKey(View v, int keyCode, KeyEvent event) { 726 return forwardKeyToQueryTextView(keyCode, event); 727 } 728 } 729 730 /** 731 * Handles key events on the suggestions list view. 732 */ 733 private class SuggestionsViewKeyListener implements View.OnKeyListener { 734 public boolean onKey(View v, int keyCode, KeyEvent event) { 735 if (event.getAction() == KeyEvent.ACTION_DOWN) { 736 int position = getSelectedPosition(); 737 if (onSuggestionKeyDown(position, keyCode, event)) { 738 return true; 739 } 740 } 741 return forwardKeyToQueryTextView(keyCode, event); 742 } 743 } 744 745 private class InputMethodCloser implements SuggestionsView.OnScrollListener { 746 747 public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, 748 int totalItemCount) { 749 } 750 751 public void onScrollStateChanged(AbsListView view, int scrollState) { 752 hideInputMethod(); 753 } 754 } 755 756 private class ClickHandler implements SuggestionClickListener { 757 public void onSuggestionClicked(int position) { 758 launchSuggestion(position); 759 } 760 761 public boolean onSuggestionLongClicked(int position) { 762 return SearchActivity.this.onSuggestionLongClicked(position); 763 } 764 765 public void onSuggestionQueryRefineClicked(int position) { 766 if (DBG) Log.d(TAG, "query refine clicked, pos " + position); 767 SuggestionCursor suggestions = getCurrentSuggestions(); 768 if (suggestions != null) { 769 suggestions.moveTo(position); 770 String query = suggestions.getSuggestionQuery(); 771 if (!TextUtils.isEmpty(query)) { 772 query += " "; 773 setQuery(query, false); 774 updateSuggestions(query); 775 } 776 } 777 } 778 } 779 780 /** 781 * Listens for clicks on the source selector. 782 */ 783 private class SearchGoButtonClickListener implements View.OnClickListener { 784 public void onClick(View view) { 785 onSearchClicked(Logger.SEARCH_METHOD_BUTTON); 786 } 787 } 788 789 /** 790 * Listens for clicks on the search button. 791 */ 792 private class CorpusIndicatorClickListener implements View.OnClickListener { 793 public void onClick(View view) { 794 showCorpusSelectionDialog(); 795 } 796 } 797 798 private class CorpusSelectorDismissListener implements DialogInterface.OnDismissListener { 799 public void onDismiss(DialogInterface dialog) { 800 if (DBG) Log.d(TAG, "Corpus selector dismissed"); 801 clearStartedIntoCorpusSelectionDialog(); 802 } 803 } 804 805 private class CorpusSelectionListener 806 implements CorpusSelectionDialog.OnCorpusSelectedListener { 807 public void onCorpusSelected(String corpusName) { 808 setCorpus(corpusName); 809 updateSuggestions(getQuery()); 810 mQueryTextView.requestFocus(); 811 showInputMethodForQuery(); 812 } 813 } 814 815 /** 816 * Listens for clicks on the voice search button. 817 */ 818 private class VoiceSearchButtonClickListener implements View.OnClickListener { 819 public void onClick(View view) { 820 onVoiceSearchClicked(); 821 } 822 } 823 824 private class CorporaObserver extends DataSetObserver { 825 @Override 826 public void onChanged() { 827 setCorpus(getCorpusName()); 828 updateSuggestions(getQuery()); 829 } 830 } 831 832 private static String ltrim(String text) { 833 int start = 0; 834 int length = text.length(); 835 while (start < length && Character.isWhitespace(text.charAt(start))) { 836 start++; 837 } 838 return start > 0 ? text.substring(start, length) : text; 839 } 840 841} 842