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