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