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