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