SearchActivity.java revision 13904ef3365894e342285492c0ffeb2287504916
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 mSuggestionsAdapter.setPromoter(createSuggestionsPromoter(null)); 340 mResultsAdapter.setCorpus(mCorpus); 341 mResultsAdapter.setPromoter(createResultsPromoter(mCorpus)); 342 } else { 343 mSuggestionsAdapter.setCorpus(mCorpus); 344 mSuggestionsAdapter.setPromoter(createSuggestionsPromoter(mCorpus)); 345 } 346 if (mCorpusIndicator != null) { 347 mCorpusIndicator.setImageDrawable(sourceIcon); 348 } 349 350 updateUi(getQuery().length() == 0); 351 } 352 353 private Promoter createSuggestionsPromoter(Corpus corpus) { 354 if (separateResults()) { 355 return getQsbApplication().createWebPromoter(); 356 } else if (corpus == null) { 357 return getQsbApplication().createBlendingPromoter(); 358 } else { 359 return getQsbApplication().createSingleCorpusPromoter(); 360 } 361 } 362 363 private Promoter createResultsPromoter(Corpus corpus) { 364 if (separateResults()) { 365 if (corpus == null) { 366 return getQsbApplication().createResultsPromoter(); 367 } else { 368 return getQsbApplication().createSingleCorpusPromoter(); 369 } 370 } else { 371 return null; 372 } 373 } 374 375 private String getCorpusName() { 376 return mCorpus == null ? null : mCorpus.getName(); 377 } 378 379 private QsbApplication getQsbApplication() { 380 return QsbApplication.get(this); 381 } 382 383 protected boolean separateResults() { 384 return mSeparateResults; 385 } 386 387 private Config getConfig() { 388 return getQsbApplication().getConfig(); 389 } 390 391 private Corpora getCorpora() { 392 return getQsbApplication().getCorpora(); 393 } 394 395 private CorpusRanker getCorpusRanker() { 396 return getQsbApplication().getCorpusRanker(); 397 } 398 399 private ShortcutRepository getShortcutRepository() { 400 return getQsbApplication().getShortcutRepository(); 401 } 402 403 private SuggestionsProvider getSuggestionsProvider() { 404 return getQsbApplication().getSuggestionsProvider(); 405 } 406 407 private CorpusViewFactory getCorpusViewFactory() { 408 return getQsbApplication().getCorpusViewFactory(); 409 } 410 411 private VoiceSearch getVoiceSearch() { 412 return QsbApplication.get(this).getVoiceSearch(); 413 } 414 415 private Logger getLogger() { 416 return getQsbApplication().getLogger(); 417 } 418 419 @VisibleForTesting 420 public void setOnDestroyListener(OnDestroyListener l) { 421 mDestroyListener = l; 422 } 423 424 @Override 425 protected void onDestroy() { 426 if (DBG) Log.d(TAG, "onDestroy()"); 427 getCorpora().unregisterDataSetObserver(mCorporaObserver); 428 mSuggestionsView.setAdapter(null); // closes mSuggestionsAdapter 429 if (mResultsView != null) { 430 mResultsView.setAdapter(null); 431 } 432 super.onDestroy(); 433 if (mDestroyListener != null) { 434 mDestroyListener.onDestroyed(); 435 } 436 } 437 438 @Override 439 protected void onStop() { 440 if (DBG) Log.d(TAG, "onStop()"); 441 if (!mTookAction) { 442 // TODO: This gets logged when starting other activities, e.g. by opening the search 443 // settings, or clicking a notification in the status bar. 444 // TODO we should log both sets of suggestions in 2-pane mode 445 getLogger().logExit(getCurrentSuggestions(), getQuery().length()); 446 } 447 // Close all open suggestion cursors. The query will be redone in onResume() 448 // if we come back to this activity. 449 mSuggestionsAdapter.setSuggestions(null); 450 if (mResultsAdapter != null) { 451 mResultsAdapter.setSuggestions(null); 452 } 453 getQsbApplication().getShortcutRefresher().reset(); 454 dismissCorpusSelectionDialog(); 455 super.onStop(); 456 } 457 458 @Override 459 protected void onRestart() { 460 if (DBG) Log.d(TAG, "onRestart()"); 461 super.onRestart(); 462 } 463 464 @Override 465 protected void onResume() { 466 if (DBG) Log.d(TAG, "onResume()"); 467 super.onResume(); 468 updateSuggestionsBuffered(); 469 if (!isCorpusSelectionDialogShowing()) { 470 mQueryTextView.requestFocus(); 471 } 472 if (TRACE) Debug.stopMethodTracing(); 473 } 474 475 @Override 476 public boolean onCreateOptionsMenu(Menu menu) { 477 super.onCreateOptionsMenu(menu); 478 SearchSettings.addSearchSettingsMenuItem(this, menu); 479 return true; 480 } 481 482 @Override 483 public void onWindowFocusChanged(boolean hasFocus) { 484 super.onWindowFocusChanged(hasFocus); 485 if (hasFocus) { 486 // Launch the IME after a bit 487 mHandler.postDelayed(mShowInputMethodTask, 0); 488 } 489 } 490 491 protected String getQuery() { 492 CharSequence q = mQueryTextView.getText(); 493 return q == null ? "" : q.toString(); 494 } 495 496 /** 497 * Sets the text in the query box. Does not update the suggestions. 498 */ 499 private void setQuery(String query, boolean selectAll) { 500 mUpdateSuggestions = false; 501 mQueryTextView.setText(query); 502 mQueryTextView.setTextSelection(selectAll); 503 mUpdateSuggestions = true; 504 } 505 506 protected void updateUi(boolean queryEmpty) { 507 updateQueryTextView(queryEmpty); 508 updateSearchGoButton(queryEmpty); 509 updateVoiceSearchButton(queryEmpty); 510 } 511 512 private void updateQueryTextView(boolean queryEmpty) { 513 if (queryEmpty) { 514 if (isSearchCorpusWeb()) { 515 mQueryTextView.setBackgroundDrawable(mQueryTextEmptyBg); 516 mQueryTextView.setHint(null); 517 } else { 518 if (mQueryTextNotEmptyBg == null) { 519 mQueryTextNotEmptyBg = 520 getResources().getDrawable(R.drawable.textfield_search_empty); 521 } 522 mQueryTextView.setBackgroundDrawable(mQueryTextNotEmptyBg); 523 mQueryTextView.setHint(mCorpus.getHint()); 524 } 525 } else { 526 mQueryTextView.setBackgroundResource(R.drawable.textfield_search); 527 } 528 } 529 530 private void updateSearchGoButton(boolean queryEmpty) { 531 if (queryEmpty) { 532 mSearchGoButton.setVisibility(View.GONE); 533 } else { 534 mSearchGoButton.setVisibility(View.VISIBLE); 535 } 536 } 537 538 protected void updateVoiceSearchButton(boolean queryEmpty) { 539 if (queryEmpty && getVoiceSearch().shouldShowVoiceSearch(mCorpus)) { 540 mVoiceSearchButton.setVisibility(View.VISIBLE); 541 mQueryTextView.setPrivateImeOptions(IME_OPTION_NO_MICROPHONE); 542 } else { 543 mVoiceSearchButton.setVisibility(View.GONE); 544 mQueryTextView.setPrivateImeOptions(null); 545 } 546 } 547 548 protected void showCorpusSelectionDialog() { 549 if (mCorpusSelectionDialog == null) { 550 mCorpusSelectionDialog = createCorpusSelectionDialog(); 551 mCorpusSelectionDialog.setOwnerActivity(this); 552 mCorpusSelectionDialog.setOnDismissListener(new CorpusSelectorDismissListener()); 553 mCorpusSelectionDialog.setOnCorpusSelectedListener(new CorpusSelectionListener()); 554 } 555 mCorpusSelectionDialog.show(mCorpus); 556 } 557 558 protected CorpusSelectionDialog createCorpusSelectionDialog() { 559 return new CorpusSelectionDialog(this); 560 } 561 562 protected boolean isCorpusSelectionDialogShowing() { 563 return mCorpusSelectionDialog != null && mCorpusSelectionDialog.isShowing(); 564 } 565 566 protected void dismissCorpusSelectionDialog() { 567 if (mCorpusSelectionDialog != null) { 568 mCorpusSelectionDialog.dismiss(); 569 } 570 } 571 572 /** 573 * @return true if a search was performed as a result of this click, false otherwise. 574 */ 575 protected boolean onSearchClicked(int method) { 576 String query = CharMatcher.WHITESPACE.trimAndCollapseFrom(getQuery(), ' '); 577 if (DBG) Log.d(TAG, "Search clicked, query=" + query); 578 579 // Don't do empty queries 580 if (TextUtils.getTrimmedLength(query) == 0) return false; 581 582 Corpus searchCorpus = getSearchCorpus(); 583 if (searchCorpus == null) return false; 584 585 mTookAction = true; 586 587 // Log search start 588 getLogger().logSearch(mCorpus, method, query.length()); 589 590 // Create shortcut 591 SuggestionData searchShortcut = searchCorpus.createSearchShortcut(query); 592 if (searchShortcut != null) { 593 ListSuggestionCursor cursor = new ListSuggestionCursor(query); 594 cursor.add(searchShortcut); 595 getShortcutRepository().reportClick(cursor, 0); 596 } 597 598 // Start search 599 Intent intent = searchCorpus.createSearchIntent(query, mAppSearchData); 600 launchIntent(intent); 601 return true; 602 } 603 604 protected void onVoiceSearchClicked() { 605 if (DBG) Log.d(TAG, "Voice Search clicked"); 606 Corpus searchCorpus = getSearchCorpus(); 607 if (searchCorpus == null) return; 608 609 mTookAction = true; 610 611 // Log voice search start 612 getLogger().logVoiceSearch(searchCorpus); 613 614 // Start voice search 615 Intent intent = searchCorpus.createVoiceSearchIntent(mAppSearchData); 616 launchIntent(intent); 617 } 618 619 protected void onSettingsClicked() { 620 SearchSettings.launchSettings(this); 621 } 622 623 /** 624 * Gets the corpus to use for any searches. This is the web corpus in "All" mode, 625 * and the selected corpus otherwise. 626 */ 627 protected Corpus getSearchCorpus() { 628 if (mCorpus != null) { 629 return mCorpus; 630 } else { 631 Corpus webCorpus = getCorpora().getWebCorpus(); 632 if (webCorpus == null) { 633 Log.e(TAG, "No web corpus"); 634 } 635 return webCorpus; 636 } 637 } 638 639 /** 640 * Checks if the corpus used for typed searches is the web corpus. 641 */ 642 protected boolean isSearchCorpusWeb() { 643 Corpus corpus = getSearchCorpus(); 644 return corpus != null && corpus.isWebCorpus(); 645 } 646 647 @Deprecated 648 protected SuggestionCursor getCurrentSuggestions() { 649 return mSuggestionsAdapter.getCurrentSuggestions(); 650 } 651 652 protected SuggestionCursor getCurrentSuggestions(SuggestionsAdapter adapter, int position) { 653 SuggestionCursor suggestions = adapter.getCurrentSuggestions(); 654 if (suggestions == null) { 655 return null; 656 } 657 int count = suggestions.getCount(); 658 if (position < 0 || position >= count) { 659 Log.w(TAG, "Invalid suggestion position " + position + ", count = " + count); 660 return null; 661 } 662 suggestions.moveTo(position); 663 return suggestions; 664 } 665 666 private Suggestions getSuggestions() { 667 return mSuggestionsAdapter.getSuggestions(); 668 } 669 670 protected Set<Corpus> getCurrentIncludedCorpora() { 671 Suggestions suggestions = getSuggestions(); 672 return suggestions == null ? null : suggestions.getIncludedCorpora(); 673 } 674 675 protected void launchIntent(Intent intent) { 676 if (intent == null) { 677 return; 678 } 679 try { 680 startActivity(intent); 681 } catch (RuntimeException ex) { 682 // Since the intents for suggestions specified by suggestion providers, 683 // guard against them not being handled, not allowed, etc. 684 Log.e(TAG, "Failed to start " + intent.toUri(0), ex); 685 } 686 } 687 688 protected boolean launchSuggestion(SuggestionsAdapter adapter, int position) { 689 SuggestionCursor suggestions = getCurrentSuggestions(adapter, position); 690 if (suggestions == null) return false; 691 692 if (DBG) Log.d(TAG, "Launching suggestion " + position); 693 mTookAction = true; 694 695 // Log suggestion click 696 getLogger().logSuggestionClick(position, suggestions, getCurrentIncludedCorpora(), 697 Logger.SUGGESTION_CLICK_TYPE_LAUNCH); 698 699 // Create shortcut 700 getShortcutRepository().reportClick(suggestions, position); 701 702 // Launch intent 703 suggestions.moveTo(position); 704 Intent intent = SuggestionUtils.getSuggestionIntent(suggestions, mAppSearchData); 705 launchIntent(intent); 706 707 return true; 708 } 709 710 protected void clickedQuickContact(SuggestionsAdapter adapter, int position) { 711 SuggestionCursor suggestions = getCurrentSuggestions(adapter, position); 712 if (suggestions == null) return; 713 714 if (DBG) Log.d(TAG, "Used suggestion " + position); 715 mTookAction = true; 716 717 // Log suggestion click 718 getLogger().logSuggestionClick(position, suggestions, getCurrentIncludedCorpora(), 719 Logger.SUGGESTION_CLICK_TYPE_QUICK_CONTACT); 720 721 // Create shortcut 722 getShortcutRepository().reportClick(suggestions, position); 723 } 724 725 protected boolean onSuggestionLongClicked(SuggestionsAdapter adapter, int position) { 726 if (DBG) Log.d(TAG, "Long clicked on suggestion " + position); 727 return false; 728 } 729 730 protected boolean onSuggestionKeyDown(SuggestionsAdapter adapter, 731 int position, int keyCode, KeyEvent event) { 732 // Treat enter or search as a click 733 if ( keyCode == KeyEvent.KEYCODE_ENTER 734 || keyCode == KeyEvent.KEYCODE_SEARCH 735 || keyCode == KeyEvent.KEYCODE_DPAD_CENTER) { 736 return launchSuggestion(adapter, position); 737 } 738 739 return false; 740 } 741 742 protected void refineSuggestion(SuggestionsAdapter adapter, int position) { 743 if (DBG) Log.d(TAG, "query refine clicked, pos " + position); 744 SuggestionCursor suggestions = getCurrentSuggestions(adapter, position); 745 if (suggestions == null) { 746 return; 747 } 748 String query = suggestions.getSuggestionQuery(); 749 if (TextUtils.isEmpty(query)) { 750 return; 751 } 752 753 // Log refine click 754 getLogger().logSuggestionClick(position, suggestions, getCurrentIncludedCorpora(), 755 Logger.SUGGESTION_CLICK_TYPE_REFINE); 756 757 // Put query + space in query text view 758 String queryWithSpace = query + ' '; 759 setQuery(queryWithSpace, false); 760 updateSuggestions(queryWithSpace); 761 mQueryTextView.requestFocus(); 762 } 763 764 /** 765 * Hides the input method. 766 */ 767 protected void considerHidingInputMethod() { 768 if (getConfig().isKeyboardDismissedOnScroll()) { 769 mQueryTextView.hideInputMethod(); 770 } 771 } 772 773 protected void showInputMethodForQuery() { 774 mQueryTextView.showInputMethod(); 775 } 776 777 protected void onSuggestionListFocusChange(SuggestionsView suggestions, boolean focused) { 778 } 779 780 protected void onQueryTextViewFocusChange(boolean focused) { 781 } 782 783 /** 784 * Hides the input method when the suggestions get focus. 785 */ 786 private class SuggestListFocusListener implements OnFocusChangeListener { 787 public void onFocusChange(View v, boolean focused) { 788 if (DBG) Log.d(TAG, "Suggestions focus change, now: " + focused); 789 if (focused && v instanceof SuggestionsView) { 790 SuggestionsView view = (SuggestionsView) v; 791 considerHidingInputMethod(); 792 onSuggestionListFocusChange(view, focused); 793 } 794 } 795 } 796 797 private class QueryTextViewFocusListener implements OnFocusChangeListener { 798 public void onFocusChange(View v, boolean focused) { 799 if (DBG) Log.d(TAG, "Query focus change, now: " + focused); 800 if (focused) { 801 // The query box got focus, show the input method 802 showInputMethodForQuery(); 803 } 804 onQueryTextViewFocusChange(focused); 805 } 806 } 807 808 private void updateSuggestionsBuffered() { 809 mHandler.removeCallbacks(mUpdateSuggestionsTask); 810 long delay = getConfig().getTypingUpdateSuggestionsDelayMillis(); 811 mHandler.postDelayed(mUpdateSuggestionsTask, delay); 812 } 813 814 private void gotSuggestions(Suggestions suggestions) { 815 if (mStarting) { 816 mStarting = false; 817 String source = getIntent().getStringExtra(Search.SOURCE); 818 int latency = mStartLatencyTracker.getLatency(); 819 getLogger().logStart(latency, source, mCorpus, 820 suggestions == null ? null : suggestions.getExpectedCorpora()); 821 getQsbApplication().onStartupComplete(); 822 } 823 } 824 825 private List<Corpus> getCorporaToQuery() { 826 if (mCorpus == null) { 827 // No corpus selected, use all enabled corpora 828 // TODO: This should be done asynchronously, since it can be expensive 829 return getCorpusRanker().getRankedCorpora(); 830 } else { 831 List<Corpus> corpora = new ArrayList<Corpus>(); 832 if (separateResults()) { 833 // In two-pane mode, we always need to query the web corpus. 834 Corpus webCorpus = getCorpora().getWebCorpus(); 835 if (webCorpus != null && webCorpus != mCorpus) corpora.add(webCorpus); 836 } 837 // Query the selected corpus 838 corpora.add(mCorpus); 839 return corpora; 840 } 841 } 842 843 protected void getShortcutsForQuery(String query, Collection<Corpus> corporaToQuery, 844 final Suggestions suggestions) { 845 ShortcutRepository shortcutRepo = getShortcutRepository(); 846 if (shortcutRepo == null) return; 847 Consumer<ShortcutCursor> consumer = Consumers.createAsyncCloseableConsumer(mHandler, 848 new Consumer<ShortcutCursor>() { 849 public boolean consume(ShortcutCursor shortcuts) { 850 suggestions.setShortcuts(shortcuts); 851 return true; 852 } 853 }); 854 shortcutRepo.getShortcutsForQuery(query, corporaToQuery, consumer); 855 } 856 857 protected void updateSuggestions(String query) { 858 query = CharMatcher.WHITESPACE.trimLeadingFrom(query); 859 if (DBG) Log.d(TAG, "getSuggestions(\""+query+"\","+mCorpus + ")"); 860 getQsbApplication().getSourceTaskExecutor().cancelPendingTasks(); 861 862 List<Corpus> corporaToQuery = getCorporaToQuery(); 863 Suggestions suggestions = getSuggestionsProvider().getSuggestions( 864 query, corporaToQuery); 865 getShortcutsForQuery(query, corporaToQuery, suggestions); 866 867 // Log start latency if this is the first suggestions update 868 gotSuggestions(suggestions); 869 870 suggestions.acquire(); 871 mSuggestionsAdapter.setSuggestions(suggestions); 872 if (mResultsAdapter != null) { 873 suggestions.acquire(); 874 mResultsAdapter.setSuggestions(suggestions); 875 } 876 } 877 878 /** 879 * If the input method is in fullscreen mode, and the selector corpus 880 * is All or Web, use the web search suggestions as completions. 881 */ 882 protected void updateInputMethodSuggestions() { 883 InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); 884 if (imm == null || !imm.isFullscreenMode()) return; 885 Suggestions suggestions = mSuggestionsAdapter.getSuggestions(); 886 if (suggestions == null) return; 887 CompletionInfo[] completions = webSuggestionsToCompletions(suggestions); 888 if (DBG) Log.d(TAG, "displayCompletions(" + Arrays.toString(completions) + ")"); 889 imm.displayCompletions(mQueryTextView, completions); 890 } 891 892 private CompletionInfo[] webSuggestionsToCompletions(Suggestions suggestions) { 893 // TODO: This should also include include web search shortcuts 894 CorpusResult cursor = suggestions.getWebResult(); 895 if (cursor == null) return null; 896 int count = cursor.getCount(); 897 ArrayList<CompletionInfo> completions = new ArrayList<CompletionInfo>(count); 898 boolean usingWebCorpus = isSearchCorpusWeb(); 899 for (int i = 0; i < count; i++) { 900 cursor.moveTo(i); 901 if (!usingWebCorpus || cursor.isWebSearchSuggestion()) { 902 String text1 = cursor.getSuggestionText1(); 903 completions.add(new CompletionInfo(i, i, text1)); 904 } 905 } 906 return completions.toArray(new CompletionInfo[completions.size()]); 907 } 908 909 private boolean forwardKeyToQueryTextView(int keyCode, KeyEvent event) { 910 if (!event.isSystem() && !isDpadKey(keyCode)) { 911 if (DBG) Log.d(TAG, "Forwarding key to query box: " + event); 912 if (mQueryTextView.requestFocus()) { 913 return mQueryTextView.dispatchKeyEvent(event); 914 } 915 } 916 return false; 917 } 918 919 private boolean isDpadKey(int keyCode) { 920 switch (keyCode) { 921 case KeyEvent.KEYCODE_DPAD_UP: 922 case KeyEvent.KEYCODE_DPAD_DOWN: 923 case KeyEvent.KEYCODE_DPAD_LEFT: 924 case KeyEvent.KEYCODE_DPAD_RIGHT: 925 case KeyEvent.KEYCODE_DPAD_CENTER: 926 return true; 927 default: 928 return false; 929 } 930 } 931 932 /** 933 * Filters the suggestions list when the search text changes. 934 */ 935 private class SearchTextWatcher implements TextWatcher { 936 public void afterTextChanged(Editable s) { 937 boolean empty = s.length() == 0; 938 if (empty != mQueryWasEmpty) { 939 mQueryWasEmpty = empty; 940 updateUi(empty); 941 } 942 if (mUpdateSuggestions) { 943 updateSuggestionsBuffered(); 944 } 945 } 946 947 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 948 } 949 950 public void onTextChanged(CharSequence s, int start, int before, int count) { 951 } 952 } 953 954 /** 955 * Handles non-text keys in the query text view. 956 */ 957 private class QueryTextViewKeyListener implements View.OnKeyListener { 958 public boolean onKey(View view, int keyCode, KeyEvent event) { 959 // Handle IME search action key 960 if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_UP) { 961 // if no action was taken, consume the key event so that the keyboard 962 // remains on screen. 963 return !onSearchClicked(Logger.SEARCH_METHOD_KEYBOARD); 964 } 965 return false; 966 } 967 } 968 969 /** 970 * Handles key events on the search and voice search buttons, 971 * by refocusing to EditText. 972 */ 973 private class ButtonsKeyListener implements View.OnKeyListener { 974 public boolean onKey(View v, int keyCode, KeyEvent event) { 975 return forwardKeyToQueryTextView(keyCode, event); 976 } 977 } 978 979 /** 980 * Handles key events on the suggestions list view. 981 */ 982 private class SuggestionsViewKeyListener implements View.OnKeyListener { 983 public boolean onKey(View v, int keyCode, KeyEvent event) { 984 if (event.getAction() == KeyEvent.ACTION_DOWN 985 && v instanceof SuggestionsView) { 986 SuggestionsView view = ((SuggestionsView) v); 987 int position = view.getSelectedPosition(); 988 if (onSuggestionKeyDown(view.getAdapter(), position, keyCode, event)) { 989 return true; 990 } 991 } 992 return forwardKeyToQueryTextView(keyCode, event); 993 } 994 } 995 996 private class InputMethodCloser implements SuggestionsView.OnScrollListener { 997 998 public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, 999 int totalItemCount) { 1000 } 1001 1002 public void onScrollStateChanged(AbsListView view, int scrollState) { 1003 considerHidingInputMethod(); 1004 } 1005 } 1006 1007 private class ClickHandler implements SuggestionClickListener { 1008 private final SuggestionsAdapter mAdapter; 1009 1010 public ClickHandler(SuggestionsAdapter suggestionsAdapter) { 1011 mAdapter = suggestionsAdapter; 1012 } 1013 1014 public void onSuggestionQuickContactClicked(int position) { 1015 clickedQuickContact(mAdapter, position); 1016 } 1017 1018 public void onSuggestionClicked(int position) { 1019 launchSuggestion(mAdapter, position); 1020 } 1021 1022 public boolean onSuggestionLongClicked(int position) { 1023 return SearchActivity.this.onSuggestionLongClicked(mAdapter, position); 1024 } 1025 1026 public void onSuggestionQueryRefineClicked(int position) { 1027 refineSuggestion(mAdapter, position); 1028 } 1029 } 1030 1031 /** 1032 * Listens for clicks on the source selector. 1033 */ 1034 private class SearchGoButtonClickListener implements View.OnClickListener { 1035 public void onClick(View view) { 1036 onSearchClicked(Logger.SEARCH_METHOD_BUTTON); 1037 } 1038 } 1039 1040 /** 1041 * Listens for clicks on the search button. 1042 */ 1043 private class CorpusIndicatorClickListener implements View.OnClickListener { 1044 public void onClick(View view) { 1045 showCorpusSelectionDialog(); 1046 } 1047 } 1048 1049 private class CorpusSelectorDismissListener implements DialogInterface.OnDismissListener { 1050 public void onDismiss(DialogInterface dialog) { 1051 if (DBG) Log.d(TAG, "Corpus selector dismissed"); 1052 clearStartedIntoCorpusSelectionDialog(); 1053 } 1054 } 1055 1056 private class CorpusSelectionListener 1057 implements CorpusSelectionDialog.OnCorpusSelectedListener { 1058 public void onCorpusSelected(String corpusName) { 1059 setCorpus(corpusName); 1060 updateSuggestions(getQuery()); 1061 mQueryTextView.requestFocus(); 1062 showInputMethodForQuery(); 1063 } 1064 } 1065 1066 /** 1067 * Listens for clicks on the voice search button. 1068 */ 1069 private class VoiceSearchButtonClickListener implements View.OnClickListener { 1070 public void onClick(View view) { 1071 onVoiceSearchClicked(); 1072 } 1073 } 1074 1075 /** 1076 * Listens for clicks on the settings button. 1077 */ 1078 private class SettingsButtonClickListener implements View.OnClickListener { 1079 public void onClick(View view) { 1080 onSettingsClicked(); 1081 } 1082 } 1083 1084 private class CorporaObserver extends DataSetObserver { 1085 @Override 1086 public void onChanged() { 1087 setCorpus(getCorpusName()); 1088 updateSuggestions(getQuery()); 1089 } 1090 } 1091 1092 private class SuggestionsObserver extends DataSetObserver { 1093 @Override 1094 public void onChanged() { 1095 updateInputMethodSuggestions(); 1096 } 1097 } 1098 1099 public interface OnDestroyListener { 1100 void onDestroyed(); 1101 } 1102 1103} 1104