SearchActivity.java revision f165f2d6c6f8173b94937ae3202f0b638348dffd
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.android.quicksearchbox.util.Consumer; 26import com.android.quicksearchbox.util.Consumers; 27import com.google.common.annotations.VisibleForTesting; 28import com.google.common.base.CharMatcher; 29 30import android.app.Activity; 31import android.app.SearchManager; 32import android.content.DialogInterface; 33import android.content.Intent; 34import android.database.DataSetObserver; 35import android.graphics.drawable.Drawable; 36import android.net.Uri; 37import android.os.Bundle; 38import android.os.Debug; 39import android.os.Handler; 40import android.text.Editable; 41import android.text.TextUtils; 42import android.text.TextWatcher; 43import android.util.Log; 44import android.view.KeyEvent; 45import android.view.Menu; 46import android.view.View; 47import android.view.View.OnFocusChangeListener; 48import android.view.inputmethod.CompletionInfo; 49import android.view.inputmethod.InputMethodManager; 50import android.widget.AbsListView; 51import android.widget.ImageButton; 52import android.widget.ImageView; 53 54import java.io.File; 55import java.util.ArrayList; 56import java.util.Arrays; 57import java.util.Collection; 58import java.util.List; 59import java.util.Set; 60 61/** 62 * The main activity for Quick Search Box. Shows the search UI. 63 * 64 */ 65public class SearchActivity extends Activity { 66 67 private static final boolean DBG = false; 68 private static final String TAG = "QSB.SearchActivity"; 69 private static final boolean TRACE = false; 70 71 private static final String SCHEME_CORPUS = "qsb.corpus"; 72 73 public static final String INTENT_ACTION_QSB_AND_SELECT_CORPUS 74 = "com.android.quicksearchbox.action.QSB_AND_SELECT_CORPUS"; 75 76 // The string used for privateImeOptions to identify to the IME that it should not show 77 // a microphone button since one already exists in the search dialog. 78 // TODO: This should move to android-common or something. 79 private static final String IME_OPTION_NO_MICROPHONE = "nm"; 80 81 // Keys for the saved instance state. 82 private static final String INSTANCE_KEY_CORPUS = "corpus"; 83 private static final String INSTANCE_KEY_QUERY = "query"; 84 85 // Measures time from for last onCreate()/onNewIntent() call. 86 private LatencyTracker mStartLatencyTracker; 87 // Whether QSB is starting. True between the calls to onCreate()/onNewIntent() and onResume(). 88 private boolean mStarting; 89 // True if the user has taken some action, e.g. launching a search, voice search, 90 // or suggestions, since QSB was last started. 91 private boolean mTookAction; 92 93 private CorpusSelectionDialog mCorpusSelectionDialog; 94 95 protected SuggestionsAdapter mSuggestionsAdapter; 96 97 // The list adapter for showing results other than query completion 98 // suggestions 99 protected SuggestionsAdapter mResultsAdapter; 100 101 private CorporaObserver mCorporaObserver; 102 103 protected QueryTextView mQueryTextView; 104 // True if the query was empty on the previous call to updateQuery() 105 protected boolean mQueryWasEmpty = true; 106 protected Drawable mQueryTextEmptyBg; 107 protected Drawable mQueryTextNotEmptyBg; 108 109 protected SuggestionsView mSuggestionsView; 110 111 // View that shows the results other than the query completions 112 protected SuggestionsView mResultsView; 113 114 protected boolean mSeparateResults; 115 116 protected ImageButton mSearchGoButton; 117 protected ImageButton mVoiceSearchButton; 118 protected ImageButton mCorpusIndicator; 119 120 protected ImageView mSettingsButton; 121 122 private Corpus mCorpus; 123 private Bundle mAppSearchData; 124 private boolean mUpdateSuggestions; 125 126 private final Handler mHandler = new Handler(); 127 private final Runnable mUpdateSuggestionsTask = new Runnable() { 128 public void run() { 129 updateSuggestions(getQuery()); 130 } 131 }; 132 133 private final Runnable mShowInputMethodTask = new Runnable() { 134 public void run() { 135 showInputMethodForQuery(); 136 } 137 }; 138 139 private OnDestroyListener mDestroyListener; 140 141 /** Called when the activity is first created. */ 142 @Override 143 public void onCreate(Bundle savedInstanceState) { 144 if (TRACE) startMethodTracing(); 145 recordStartTime(); 146 if (DBG) Log.d(TAG, "onCreate()"); 147 super.onCreate(savedInstanceState); 148 149 setContentView(); 150 151 SuggestListFocusListener suggestionFocusListener = new SuggestListFocusListener(); 152 SuggestionsViewKeyListener suggestionViewKeyListener = new SuggestionsViewKeyListener(); 153 InputMethodCloser imeCloser = new InputMethodCloser(); 154 155 mQueryTextView = (QueryTextView) findViewById(R.id.search_src_text); 156 157 mSuggestionsView = (SuggestionsView) findViewById(R.id.suggestions); 158 mSuggestionsView.setOnScrollListener(imeCloser); 159 mSuggestionsView.setOnKeyListener(suggestionViewKeyListener); 160 mSuggestionsView.setOnFocusChangeListener(suggestionFocusListener); 161 162 mResultsView = (SuggestionsView) findViewById(R.id.shortcuts); 163 mSeparateResults = mResultsView != null; 164 165 mSuggestionsAdapter = getQsbApplication().createSuggestionsAdapter(); 166 mSuggestionsAdapter.setSuggestionClickListener(new ClickHandler(mSuggestionsAdapter)); 167 mSuggestionsAdapter.setOnFocusChangeListener(suggestionFocusListener); 168 169 if (mSeparateResults) { 170 mResultsAdapter = getQsbApplication().createSuggestionsAdapter(); 171 mResultsAdapter.setSuggestionClickListener(new ClickHandler(mResultsAdapter)); 172 mResultsAdapter.setOnFocusChangeListener(suggestionFocusListener); 173 mResultsView.setOnScrollListener(imeCloser); 174 mResultsView.setOnFocusChangeListener(suggestionFocusListener); 175 mResultsView.setOnKeyListener(suggestionViewKeyListener); 176 } 177 178 mSearchGoButton = (ImageButton) findViewById(R.id.search_go_btn); 179 mVoiceSearchButton = (ImageButton) findViewById(R.id.search_voice_btn); 180 mCorpusIndicator = (ImageButton) findViewById(R.id.corpus_indicator); 181 mSettingsButton = (ImageView) findViewById(R.id.settings_icon); 182 183 mQueryTextView.addTextChangedListener(new SearchTextWatcher()); 184 mQueryTextView.setOnKeyListener(new QueryTextViewKeyListener()); 185 mQueryTextView.setOnFocusChangeListener(new QueryTextViewFocusListener()); 186 mQueryTextView.setSuggestionClickListener(new ClickHandler(mSuggestionsAdapter)); 187 mQueryTextEmptyBg = mQueryTextView.getBackground(); 188 189 if (mCorpusIndicator != null) { 190 mCorpusIndicator.setOnClickListener(new CorpusIndicatorClickListener()); 191 } 192 193 mSearchGoButton.setOnClickListener(new SearchGoButtonClickListener()); 194 195 mVoiceSearchButton.setOnClickListener(new VoiceSearchButtonClickListener()); 196 197 if (mSettingsButton != null) { 198 mSettingsButton.setOnClickListener(new SettingsButtonClickListener()); 199 } 200 201 ButtonsKeyListener buttonsKeyListener = new ButtonsKeyListener(); 202 mSearchGoButton.setOnKeyListener(buttonsKeyListener); 203 mVoiceSearchButton.setOnKeyListener(buttonsKeyListener); 204 if (mCorpusIndicator != null) { 205 mCorpusIndicator.setOnKeyListener(buttonsKeyListener); 206 } 207 208 mUpdateSuggestions = true; 209 210 // First get setup from intent 211 Intent intent = getIntent(); 212 setupFromIntent(intent); 213 // Then restore any saved instance state 214 restoreInstanceState(savedInstanceState); 215 216 mSuggestionsAdapter.registerDataSetObserver(new SuggestionsObserver()); 217 218 // Do this at the end, to avoid updating the list view when setSource() 219 // is called. 220 mSuggestionsView.setAdapter(mSuggestionsAdapter); 221 222 if (mSeparateResults) { 223 mResultsAdapter.registerDataSetObserver(new SuggestionsObserver()); 224 mResultsView.setAdapter(mResultsAdapter); 225 } 226 mCorporaObserver = new CorporaObserver(); 227 getCorpora().registerDataSetObserver(mCorporaObserver); 228 } 229 230 protected void setContentView() { 231 setContentView(R.layout.search_activity); 232 } 233 234 private void startMethodTracing() { 235 File traceDir = getDir("traces", 0); 236 String traceFile = new File(traceDir, "qsb.trace").getAbsolutePath(); 237 Debug.startMethodTracing(traceFile); 238 } 239 240 @Override 241 protected void onNewIntent(Intent intent) { 242 if (DBG) Log.d(TAG, "onNewIntent()"); 243 recordStartTime(); 244 setIntent(intent); 245 setupFromIntent(intent); 246 } 247 248 private void recordStartTime() { 249 mStartLatencyTracker = new LatencyTracker(); 250 mStarting = true; 251 mTookAction = false; 252 } 253 254 protected void restoreInstanceState(Bundle savedInstanceState) { 255 if (savedInstanceState == null) return; 256 String corpusName = savedInstanceState.getString(INSTANCE_KEY_CORPUS); 257 String query = savedInstanceState.getString(INSTANCE_KEY_QUERY); 258 setCorpus(corpusName); 259 setQuery(query, false); 260 } 261 262 @Override 263 protected void onSaveInstanceState(Bundle outState) { 264 super.onSaveInstanceState(outState); 265 // We don't save appSearchData, since we always get the value 266 // from the intent and the user can't change it. 267 268 outState.putString(INSTANCE_KEY_CORPUS, getCorpusName()); 269 outState.putString(INSTANCE_KEY_QUERY, getQuery()); 270 } 271 272 private void setupFromIntent(Intent intent) { 273 if (DBG) Log.d(TAG, "setupFromIntent(" + intent.toUri(0) + ")"); 274 String corpusName = getCorpusNameFromUri(intent.getData()); 275 String query = intent.getStringExtra(SearchManager.QUERY); 276 Bundle appSearchData = intent.getBundleExtra(SearchManager.APP_DATA); 277 boolean selectAll = intent.getBooleanExtra(SearchManager.EXTRA_SELECT_QUERY, false); 278 279 setCorpus(corpusName); 280 setQuery(query, selectAll); 281 mAppSearchData = appSearchData; 282 283 if (startedIntoCorpusSelectionDialog()) { 284 showCorpusSelectionDialog(); 285 } 286 } 287 288 public boolean startedIntoCorpusSelectionDialog() { 289 return INTENT_ACTION_QSB_AND_SELECT_CORPUS.equals(getIntent().getAction()); 290 } 291 292 /** 293 * Removes corpus selector intent action, so that BACK works normally after 294 * dismissing and reopening the corpus selector. 295 */ 296 private void clearStartedIntoCorpusSelectionDialog() { 297 Intent oldIntent = getIntent(); 298 if (SearchActivity.INTENT_ACTION_QSB_AND_SELECT_CORPUS.equals(oldIntent.getAction())) { 299 Intent newIntent = new Intent(oldIntent); 300 newIntent.setAction(SearchManager.INTENT_ACTION_GLOBAL_SEARCH); 301 setIntent(newIntent); 302 } 303 } 304 305 public static Uri getCorpusUri(Corpus corpus) { 306 if (corpus == null) return null; 307 return new Uri.Builder() 308 .scheme(SCHEME_CORPUS) 309 .authority(corpus.getName()) 310 .build(); 311 } 312 313 private String getCorpusNameFromUri(Uri uri) { 314 if (uri == null) return null; 315 if (!SCHEME_CORPUS.equals(uri.getScheme())) return null; 316 return uri.getAuthority(); 317 } 318 319 private Corpus getCorpus(String sourceName) { 320 if (sourceName == null) return null; 321 Corpus corpus = getCorpora().getCorpus(sourceName); 322 if (corpus == null) { 323 Log.w(TAG, "Unknown corpus " + sourceName); 324 return null; 325 } 326 return corpus; 327 } 328 329 private void setCorpus(String corpusName) { 330 if (DBG) Log.d(TAG, "setCorpus(" + corpusName + ")"); 331 mCorpus = getCorpus(corpusName); 332 Drawable sourceIcon; 333 if (mCorpus == null) { 334 sourceIcon = getCorpusViewFactory().getGlobalSearchIcon(); 335 } else { 336 sourceIcon = mCorpus.getCorpusIcon(); 337 } 338 if (separateResults()) { 339 mResultsAdapter.setCorpus(mCorpus); 340 mResultsAdapter.setPromoter(createResultsPromoter(mCorpus)); 341 } else { 342 mSuggestionsAdapter.setCorpus(mCorpus); 343 mSuggestionsAdapter.setPromoter(createSuggestionsPromoter(mCorpus)); 344 } 345 if (mCorpusIndicator != null) { 346 mCorpusIndicator.setImageDrawable(sourceIcon); 347 } 348 349 updateUi(getQuery().length() == 0); 350 } 351 352 private Promoter createSuggestionsPromoter(Corpus corpus) { 353 if (separateResults()) { 354 return getQsbApplication().createWebPromoter(); 355 } else if (corpus == null) { 356 return getQsbApplication().createBlendingPromoter(); 357 } else { 358 return getQsbApplication().createSingleCorpusPromoter(); 359 } 360 } 361 362 private Promoter createResultsPromoter(Corpus corpus) { 363 if (separateResults()) { 364 if (corpus == null) { 365 return getQsbApplication().createResultsPromoter(); 366 } else { 367 return getQsbApplication().createSingleCorpusPromoter(); 368 } 369 } else { 370 return null; 371 } 372 } 373 374 private String getCorpusName() { 375 return mCorpus == null ? null : mCorpus.getName(); 376 } 377 378 private QsbApplication getQsbApplication() { 379 return QsbApplication.get(this); 380 } 381 382 protected boolean separateResults() { 383 return mSeparateResults; 384 } 385 386 private Config getConfig() { 387 return getQsbApplication().getConfig(); 388 } 389 390 private Corpora getCorpora() { 391 return getQsbApplication().getCorpora(); 392 } 393 394 private CorpusRanker getCorpusRanker() { 395 return getQsbApplication().getCorpusRanker(); 396 } 397 398 private ShortcutRepository getShortcutRepository() { 399 return getQsbApplication().getShortcutRepository(); 400 } 401 402 private SuggestionsProvider getSuggestionsProvider() { 403 return getQsbApplication().getSuggestionsProvider(); 404 } 405 406 private CorpusViewFactory getCorpusViewFactory() { 407 return getQsbApplication().getCorpusViewFactory(); 408 } 409 410 private VoiceSearch getVoiceSearch() { 411 return QsbApplication.get(this).getVoiceSearch(); 412 } 413 414 private Logger getLogger() { 415 return getQsbApplication().getLogger(); 416 } 417 418 @VisibleForTesting 419 public void setOnDestroyListener(OnDestroyListener l) { 420 mDestroyListener = l; 421 } 422 423 @Override 424 protected void onDestroy() { 425 if (DBG) Log.d(TAG, "onDestroy()"); 426 getCorpora().unregisterDataSetObserver(mCorporaObserver); 427 mSuggestionsView.setAdapter(null); // closes mSuggestionsAdapter 428 if (mResultsView != null) { 429 mResultsView.setAdapter(null); 430 } 431 super.onDestroy(); 432 if (mDestroyListener != null) { 433 mDestroyListener.onDestroyed(); 434 } 435 } 436 437 @Override 438 protected void onStop() { 439 if (DBG) Log.d(TAG, "onStop()"); 440 if (!mTookAction) { 441 // TODO: This gets logged when starting other activities, e.g. by opening the search 442 // settings, or clicking a notification in the status bar. 443 // TODO we should log both sets of suggestions in 2-pane mode 444 getLogger().logExit(getCurrentSuggestions(), getQuery().length()); 445 } 446 // Close all open suggestion cursors. The query will be redone in onResume() 447 // if we come back to this activity. 448 mSuggestionsAdapter.setSuggestions(null); 449 if (mResultsAdapter != null) { 450 mResultsAdapter.setSuggestions(null); 451 } 452 getQsbApplication().getShortcutRefresher().reset(); 453 dismissCorpusSelectionDialog(); 454 super.onStop(); 455 } 456 457 @Override 458 protected void onRestart() { 459 if (DBG) Log.d(TAG, "onRestart()"); 460 super.onRestart(); 461 } 462 463 @Override 464 protected void onResume() { 465 if (DBG) Log.d(TAG, "onResume()"); 466 super.onResume(); 467 updateSuggestionsBuffered(); 468 if (!isCorpusSelectionDialogShowing()) { 469 mQueryTextView.requestFocus(); 470 } 471 if (TRACE) Debug.stopMethodTracing(); 472 } 473 474 @Override 475 public boolean onCreateOptionsMenu(Menu menu) { 476 super.onCreateOptionsMenu(menu); 477 SearchSettings.addSearchSettingsMenuItem(this, menu); 478 return true; 479 } 480 481 @Override 482 public void onWindowFocusChanged(boolean hasFocus) { 483 super.onWindowFocusChanged(hasFocus); 484 if (hasFocus) { 485 // Launch the IME after a bit 486 mHandler.postDelayed(mShowInputMethodTask, 0); 487 } 488 } 489 490 protected String getQuery() { 491 CharSequence q = mQueryTextView.getText(); 492 return q == null ? "" : q.toString(); 493 } 494 495 /** 496 * Sets the text in the query box. Does not update the suggestions. 497 */ 498 private void setQuery(String query, boolean selectAll) { 499 mUpdateSuggestions = false; 500 mQueryTextView.setText(query); 501 mQueryTextView.setTextSelection(selectAll); 502 mUpdateSuggestions = true; 503 } 504 505 protected void updateUi(boolean queryEmpty) { 506 updateQueryTextView(queryEmpty); 507 updateSearchGoButton(queryEmpty); 508 updateVoiceSearchButton(queryEmpty); 509 } 510 511 private void updateQueryTextView(boolean queryEmpty) { 512 if (queryEmpty) { 513 if (isSearchCorpusWeb()) { 514 mQueryTextView.setBackgroundDrawable(mQueryTextEmptyBg); 515 mQueryTextView.setHint(null); 516 } else { 517 if (mQueryTextNotEmptyBg == null) { 518 mQueryTextNotEmptyBg = 519 getResources().getDrawable(R.drawable.textfield_search_empty); 520 } 521 mQueryTextView.setBackgroundDrawable(mQueryTextNotEmptyBg); 522 mQueryTextView.setHint(mCorpus.getHint()); 523 } 524 } else { 525 mQueryTextView.setBackgroundResource(R.drawable.textfield_search); 526 } 527 } 528 529 private void updateSearchGoButton(boolean queryEmpty) { 530 if (queryEmpty) { 531 mSearchGoButton.setVisibility(View.GONE); 532 } else { 533 mSearchGoButton.setVisibility(View.VISIBLE); 534 } 535 } 536 537 protected void updateVoiceSearchButton(boolean queryEmpty) { 538 if (queryEmpty && getVoiceSearch().shouldShowVoiceSearch(mCorpus)) { 539 mVoiceSearchButton.setVisibility(View.VISIBLE); 540 mQueryTextView.setPrivateImeOptions(IME_OPTION_NO_MICROPHONE); 541 } else { 542 mVoiceSearchButton.setVisibility(View.GONE); 543 mQueryTextView.setPrivateImeOptions(null); 544 } 545 } 546 547 protected void showCorpusSelectionDialog() { 548 if (mCorpusSelectionDialog == null) { 549 mCorpusSelectionDialog = createCorpusSelectionDialog(); 550 mCorpusSelectionDialog.setOwnerActivity(this); 551 mCorpusSelectionDialog.setOnDismissListener(new CorpusSelectorDismissListener()); 552 mCorpusSelectionDialog.setOnCorpusSelectedListener(new CorpusSelectionListener()); 553 } 554 mCorpusSelectionDialog.show(mCorpus); 555 } 556 557 protected CorpusSelectionDialog createCorpusSelectionDialog() { 558 return new CorpusSelectionDialog(this); 559 } 560 561 protected boolean isCorpusSelectionDialogShowing() { 562 return mCorpusSelectionDialog != null && mCorpusSelectionDialog.isShowing(); 563 } 564 565 protected void dismissCorpusSelectionDialog() { 566 if (mCorpusSelectionDialog != null) { 567 mCorpusSelectionDialog.dismiss(); 568 } 569 } 570 571 /** 572 * @return true if a search was performed as a result of this click, false otherwise. 573 */ 574 protected boolean onSearchClicked(int method) { 575 String query = CharMatcher.WHITESPACE.trimAndCollapseFrom(getQuery(), ' '); 576 if (DBG) Log.d(TAG, "Search clicked, query=" + query); 577 578 // Don't do empty queries 579 if (TextUtils.getTrimmedLength(query) == 0) return false; 580 581 Corpus searchCorpus = getSearchCorpus(); 582 if (searchCorpus == null) return false; 583 584 mTookAction = true; 585 586 // Log search start 587 getLogger().logSearch(mCorpus, method, query.length()); 588 589 // Create shortcut 590 SuggestionData searchShortcut = searchCorpus.createSearchShortcut(query); 591 if (searchShortcut != null) { 592 ListSuggestionCursor cursor = new ListSuggestionCursor(query); 593 cursor.add(searchShortcut); 594 getShortcutRepository().reportClick(cursor, 0); 595 } 596 597 // Start search 598 Intent intent = searchCorpus.createSearchIntent(query, mAppSearchData); 599 launchIntent(intent); 600 return true; 601 } 602 603 protected void onVoiceSearchClicked() { 604 if (DBG) Log.d(TAG, "Voice Search clicked"); 605 Corpus searchCorpus = getSearchCorpus(); 606 if (searchCorpus == null) return; 607 608 mTookAction = true; 609 610 // Log voice search start 611 getLogger().logVoiceSearch(searchCorpus); 612 613 // Start voice search 614 Intent intent = searchCorpus.createVoiceSearchIntent(mAppSearchData); 615 launchIntent(intent); 616 } 617 618 protected void onSettingsClicked() { 619 SearchSettings.launchSettings(this); 620 } 621 622 /** 623 * Gets the corpus to use for any searches. This is the web corpus in "All" mode, 624 * and the selected corpus otherwise. 625 */ 626 protected Corpus getSearchCorpus() { 627 if (mCorpus != null) { 628 return mCorpus; 629 } else { 630 Corpus webCorpus = getCorpora().getWebCorpus(); 631 if (webCorpus == null) { 632 Log.e(TAG, "No web corpus"); 633 } 634 return webCorpus; 635 } 636 } 637 638 /** 639 * Checks if the corpus used for typed searches is the web corpus. 640 */ 641 protected boolean isSearchCorpusWeb() { 642 Corpus corpus = getSearchCorpus(); 643 return corpus != null && corpus.isWebCorpus(); 644 } 645 646 @Deprecated 647 protected SuggestionCursor getCurrentSuggestions() { 648 return mSuggestionsAdapter.getCurrentSuggestions(); 649 } 650 651 protected SuggestionCursor getCurrentSuggestions(SuggestionsAdapter adapter, int position) { 652 SuggestionCursor suggestions = adapter.getCurrentSuggestions(); 653 if (suggestions == null) { 654 return null; 655 } 656 int count = suggestions.getCount(); 657 if (position < 0 || position >= count) { 658 Log.w(TAG, "Invalid suggestion position " + position + ", count = " + count); 659 return null; 660 } 661 suggestions.moveTo(position); 662 return suggestions; 663 } 664 665 private Suggestions getSuggestions() { 666 return mSuggestionsAdapter.getSuggestions(); 667 } 668 669 protected Set<Corpus> getCurrentIncludedCorpora() { 670 Suggestions suggestions = getSuggestions(); 671 return suggestions == null ? null : suggestions.getIncludedCorpora(); 672 } 673 674 protected void launchIntent(Intent intent) { 675 if (intent == null) { 676 return; 677 } 678 try { 679 startActivity(intent); 680 } catch (RuntimeException ex) { 681 // Since the intents for suggestions specified by suggestion providers, 682 // guard against them not being handled, not allowed, etc. 683 Log.e(TAG, "Failed to start " + intent.toUri(0), ex); 684 } 685 } 686 687 protected boolean launchSuggestion(SuggestionsAdapter adapter, int position) { 688 SuggestionCursor suggestions = getCurrentSuggestions(adapter, position); 689 if (suggestions == null) return false; 690 691 if (DBG) Log.d(TAG, "Launching suggestion " + position); 692 mTookAction = true; 693 694 // Log suggestion click 695 getLogger().logSuggestionClick(position, suggestions, getCurrentIncludedCorpora(), 696 Logger.SUGGESTION_CLICK_TYPE_LAUNCH); 697 698 // Create shortcut 699 getShortcutRepository().reportClick(suggestions, position); 700 701 // Launch intent 702 suggestions.moveTo(position); 703 Intent intent = SuggestionUtils.getSuggestionIntent(suggestions, mAppSearchData); 704 launchIntent(intent); 705 706 return true; 707 } 708 709 protected void clickedQuickContact(SuggestionsAdapter adapter, int position) { 710 SuggestionCursor suggestions = getCurrentSuggestions(adapter, position); 711 if (suggestions == null) return; 712 713 if (DBG) Log.d(TAG, "Used suggestion " + position); 714 mTookAction = true; 715 716 // Log suggestion click 717 getLogger().logSuggestionClick(position, suggestions, getCurrentIncludedCorpora(), 718 Logger.SUGGESTION_CLICK_TYPE_QUICK_CONTACT); 719 720 // Create shortcut 721 getShortcutRepository().reportClick(suggestions, position); 722 } 723 724 protected boolean onSuggestionLongClicked(SuggestionsAdapter adapter, int position) { 725 if (DBG) Log.d(TAG, "Long clicked on suggestion " + position); 726 return false; 727 } 728 729 protected boolean onSuggestionKeyDown(SuggestionsAdapter adapter, 730 int position, int keyCode, KeyEvent event) { 731 // Treat enter or search as a click 732 if ( keyCode == KeyEvent.KEYCODE_ENTER 733 || keyCode == KeyEvent.KEYCODE_SEARCH 734 || keyCode == KeyEvent.KEYCODE_DPAD_CENTER) { 735 return launchSuggestion(adapter, position); 736 } 737 738 return false; 739 } 740 741 protected void refineSuggestion(SuggestionsAdapter adapter, int position) { 742 if (DBG) Log.d(TAG, "query refine clicked, pos " + position); 743 SuggestionCursor suggestions = getCurrentSuggestions(adapter, position); 744 if (suggestions == null) { 745 return; 746 } 747 String query = suggestions.getSuggestionQuery(); 748 if (TextUtils.isEmpty(query)) { 749 return; 750 } 751 752 // Log refine click 753 getLogger().logSuggestionClick(position, suggestions, getCurrentIncludedCorpora(), 754 Logger.SUGGESTION_CLICK_TYPE_REFINE); 755 756 // Put query + space in query text view 757 String queryWithSpace = query + ' '; 758 setQuery(queryWithSpace, false); 759 updateSuggestions(queryWithSpace); 760 mQueryTextView.requestFocus(); 761 } 762 763 /** 764 * Hides the input method. 765 */ 766 protected void considerHidingInputMethod() { 767 if (getConfig().isKeyboardDismissedOnScroll()) { 768 mQueryTextView.hideInputMethod(); 769 } 770 } 771 772 protected void showInputMethodForQuery() { 773 mQueryTextView.showInputMethod(); 774 } 775 776 protected void onSuggestionListFocusChange(SuggestionsView suggestions, boolean focused) { 777 } 778 779 protected void onQueryTextViewFocusChange(boolean focused) { 780 } 781 782 /** 783 * Hides the input method when the suggestions get focus. 784 */ 785 private class SuggestListFocusListener implements OnFocusChangeListener { 786 public void onFocusChange(View v, boolean focused) { 787 if (DBG) Log.d(TAG, "Suggestions focus change, now: " + focused); 788 if (focused && v instanceof SuggestionsView) { 789 SuggestionsView view = (SuggestionsView) v; 790 considerHidingInputMethod(); 791 onSuggestionListFocusChange(view, focused); 792 } 793 } 794 } 795 796 private class QueryTextViewFocusListener implements OnFocusChangeListener { 797 public void onFocusChange(View v, boolean focused) { 798 if (DBG) Log.d(TAG, "Query focus change, now: " + focused); 799 if (focused) { 800 // The query box got focus, show the input method 801 showInputMethodForQuery(); 802 } 803 onQueryTextViewFocusChange(focused); 804 } 805 } 806 807 private void updateSuggestionsBuffered() { 808 mHandler.removeCallbacks(mUpdateSuggestionsTask); 809 long delay = getConfig().getTypingUpdateSuggestionsDelayMillis(); 810 mHandler.postDelayed(mUpdateSuggestionsTask, delay); 811 } 812 813 private void gotSuggestions(Suggestions suggestions) { 814 if (mStarting) { 815 mStarting = false; 816 String source = getIntent().getStringExtra(Search.SOURCE); 817 int latency = mStartLatencyTracker.getLatency(); 818 getLogger().logStart(latency, source, mCorpus, 819 suggestions == null ? null : suggestions.getExpectedCorpora()); 820 getQsbApplication().onStartupComplete(); 821 } 822 } 823 824 private List<Corpus> getCorporaToQuery() { 825 if (mCorpus == null) { 826 // No corpus selected, use all enabled corpora 827 // TODO: This should be done asynchronously, since it can be expensive 828 return getCorpusRanker().getRankedCorpora(); 829 } else { 830 List<Corpus> corpora = new ArrayList<Corpus>(); 831 if (separateResults()) { 832 // In two-pane mode, we always need to query the web corpus. 833 Corpus webCorpus = getCorpora().getWebCorpus(); 834 if (webCorpus != null && webCorpus != mCorpus) corpora.add(webCorpus); 835 } 836 // Query the selected corpus 837 corpora.add(mCorpus); 838 return corpora; 839 } 840 } 841 842 protected void getShortcutsForQuery(String query, Collection<Corpus> corporaToQuery, 843 final Suggestions suggestions) { 844 ShortcutRepository shortcutRepo = getShortcutRepository(); 845 if (shortcutRepo == null) return; 846 Consumer<ShortcutCursor> consumer = Consumers.createAsyncCloseableConsumer(mHandler, 847 new Consumer<ShortcutCursor>() { 848 public boolean consume(ShortcutCursor shortcuts) { 849 suggestions.setShortcuts(shortcuts); 850 return true; 851 } 852 }); 853 shortcutRepo.getShortcutsForQuery(query, corporaToQuery, consumer); 854 } 855 856 protected void updateSuggestions(String query) { 857 query = CharMatcher.WHITESPACE.trimLeadingFrom(query); 858 if (DBG) Log.d(TAG, "getSuggestions(\""+query+"\","+mCorpus + ")"); 859 getQsbApplication().getSourceTaskExecutor().cancelPendingTasks(); 860 861 List<Corpus> corporaToQuery = getCorporaToQuery(); 862 Suggestions suggestions = getSuggestionsProvider().getSuggestions( 863 query, corporaToQuery); 864 getShortcutsForQuery(query, corporaToQuery, suggestions); 865 866 // Log start latency if this is the first suggestions update 867 gotSuggestions(suggestions); 868 869 suggestions.acquire(); 870 mSuggestionsAdapter.setSuggestions(suggestions); 871 if (mResultsAdapter != null) { 872 suggestions.acquire(); 873 mResultsAdapter.setSuggestions(suggestions); 874 } 875 } 876 877 /** 878 * If the input method is in fullscreen mode, and the selector corpus 879 * is All or Web, use the web search suggestions as completions. 880 */ 881 protected void updateInputMethodSuggestions() { 882 InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); 883 if (imm == null || !imm.isFullscreenMode()) return; 884 Suggestions suggestions = mSuggestionsAdapter.getSuggestions(); 885 if (suggestions == null) return; 886 CompletionInfo[] completions = webSuggestionsToCompletions(suggestions); 887 if (DBG) Log.d(TAG, "displayCompletions(" + Arrays.toString(completions) + ")"); 888 imm.displayCompletions(mQueryTextView, completions); 889 } 890 891 private CompletionInfo[] webSuggestionsToCompletions(Suggestions suggestions) { 892 // TODO: This should also include include web search shortcuts 893 CorpusResult cursor = suggestions.getWebResult(); 894 if (cursor == null) return null; 895 int count = cursor.getCount(); 896 ArrayList<CompletionInfo> completions = new ArrayList<CompletionInfo>(count); 897 boolean usingWebCorpus = isSearchCorpusWeb(); 898 for (int i = 0; i < count; i++) { 899 cursor.moveTo(i); 900 if (!usingWebCorpus || cursor.isWebSearchSuggestion()) { 901 String text1 = cursor.getSuggestionText1(); 902 completions.add(new CompletionInfo(i, i, text1)); 903 } 904 } 905 return completions.toArray(new CompletionInfo[completions.size()]); 906 } 907 908 private boolean forwardKeyToQueryTextView(int keyCode, KeyEvent event) { 909 if (!event.isSystem() && !isDpadKey(keyCode)) { 910 if (DBG) Log.d(TAG, "Forwarding key to query box: " + event); 911 if (mQueryTextView.requestFocus()) { 912 return mQueryTextView.dispatchKeyEvent(event); 913 } 914 } 915 return false; 916 } 917 918 private boolean isDpadKey(int keyCode) { 919 switch (keyCode) { 920 case KeyEvent.KEYCODE_DPAD_UP: 921 case KeyEvent.KEYCODE_DPAD_DOWN: 922 case KeyEvent.KEYCODE_DPAD_LEFT: 923 case KeyEvent.KEYCODE_DPAD_RIGHT: 924 case KeyEvent.KEYCODE_DPAD_CENTER: 925 return true; 926 default: 927 return false; 928 } 929 } 930 931 /** 932 * Filters the suggestions list when the search text changes. 933 */ 934 private class SearchTextWatcher implements TextWatcher { 935 public void afterTextChanged(Editable s) { 936 boolean empty = s.length() == 0; 937 if (empty != mQueryWasEmpty) { 938 mQueryWasEmpty = empty; 939 updateUi(empty); 940 } 941 if (mUpdateSuggestions) { 942 updateSuggestionsBuffered(); 943 } 944 } 945 946 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 947 } 948 949 public void onTextChanged(CharSequence s, int start, int before, int count) { 950 } 951 } 952 953 /** 954 * Handles non-text keys in the query text view. 955 */ 956 private class QueryTextViewKeyListener implements View.OnKeyListener { 957 public boolean onKey(View view, int keyCode, KeyEvent event) { 958 // Handle IME search action key 959 if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_UP) { 960 // if no action was taken, consume the key event so that the keyboard 961 // remains on screen. 962 return !onSearchClicked(Logger.SEARCH_METHOD_KEYBOARD); 963 } 964 return false; 965 } 966 } 967 968 /** 969 * Handles key events on the search and voice search buttons, 970 * by refocusing to EditText. 971 */ 972 private class ButtonsKeyListener implements View.OnKeyListener { 973 public boolean onKey(View v, int keyCode, KeyEvent event) { 974 return forwardKeyToQueryTextView(keyCode, event); 975 } 976 } 977 978 /** 979 * Handles key events on the suggestions list view. 980 */ 981 private class SuggestionsViewKeyListener implements View.OnKeyListener { 982 public boolean onKey(View v, int keyCode, KeyEvent event) { 983 if (event.getAction() == KeyEvent.ACTION_DOWN 984 && v instanceof SuggestionsView) { 985 SuggestionsView view = ((SuggestionsView) v); 986 int position = view.getSelectedPosition(); 987 if (onSuggestionKeyDown(view.getAdapter(), position, keyCode, event)) { 988 return true; 989 } 990 } 991 return forwardKeyToQueryTextView(keyCode, event); 992 } 993 } 994 995 private class InputMethodCloser implements SuggestionsView.OnScrollListener { 996 997 public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, 998 int totalItemCount) { 999 } 1000 1001 public void onScrollStateChanged(AbsListView view, int scrollState) { 1002 considerHidingInputMethod(); 1003 } 1004 } 1005 1006 private class ClickHandler implements SuggestionClickListener { 1007 private final SuggestionsAdapter mAdapter; 1008 1009 public ClickHandler(SuggestionsAdapter suggestionsAdapter) { 1010 mAdapter = suggestionsAdapter; 1011 } 1012 1013 public void onSuggestionQuickContactClicked(int position) { 1014 clickedQuickContact(mAdapter, position); 1015 } 1016 1017 public void onSuggestionClicked(int position) { 1018 launchSuggestion(mAdapter, position); 1019 } 1020 1021 public boolean onSuggestionLongClicked(int position) { 1022 return SearchActivity.this.onSuggestionLongClicked(mAdapter, position); 1023 } 1024 1025 public void onSuggestionQueryRefineClicked(int position) { 1026 refineSuggestion(mAdapter, position); 1027 } 1028 } 1029 1030 /** 1031 * Listens for clicks on the source selector. 1032 */ 1033 private class SearchGoButtonClickListener implements View.OnClickListener { 1034 public void onClick(View view) { 1035 onSearchClicked(Logger.SEARCH_METHOD_BUTTON); 1036 } 1037 } 1038 1039 /** 1040 * Listens for clicks on the search button. 1041 */ 1042 private class CorpusIndicatorClickListener implements View.OnClickListener { 1043 public void onClick(View view) { 1044 showCorpusSelectionDialog(); 1045 } 1046 } 1047 1048 private class CorpusSelectorDismissListener implements DialogInterface.OnDismissListener { 1049 public void onDismiss(DialogInterface dialog) { 1050 if (DBG) Log.d(TAG, "Corpus selector dismissed"); 1051 clearStartedIntoCorpusSelectionDialog(); 1052 } 1053 } 1054 1055 private class CorpusSelectionListener 1056 implements CorpusSelectionDialog.OnCorpusSelectedListener { 1057 public void onCorpusSelected(String corpusName) { 1058 setCorpus(corpusName); 1059 updateSuggestions(getQuery()); 1060 mQueryTextView.requestFocus(); 1061 showInputMethodForQuery(); 1062 } 1063 } 1064 1065 /** 1066 * Listens for clicks on the voice search button. 1067 */ 1068 private class VoiceSearchButtonClickListener implements View.OnClickListener { 1069 public void onClick(View view) { 1070 onVoiceSearchClicked(); 1071 } 1072 } 1073 1074 /** 1075 * Listens for clicks on the settings button. 1076 */ 1077 private class SettingsButtonClickListener implements View.OnClickListener { 1078 public void onClick(View view) { 1079 onSettingsClicked(); 1080 } 1081 } 1082 1083 private class CorporaObserver extends DataSetObserver { 1084 @Override 1085 public void onChanged() { 1086 setCorpus(getCorpusName()); 1087 updateSuggestions(getQuery()); 1088 } 1089 } 1090 1091 private class SuggestionsObserver extends DataSetObserver { 1092 @Override 1093 public void onChanged() { 1094 updateInputMethodSuggestions(); 1095 } 1096 } 1097 1098 public interface OnDestroyListener { 1099 void onDestroyed(); 1100 } 1101 1102} 1103