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