SearchActivity.java revision 9e46c057d7f88d2a899e124f96d266c54250bb45
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.SearchActivityView; 21import com.android.quicksearchbox.ui.SuggestionClickListener; 22import com.android.quicksearchbox.ui.SuggestionsAdapter; 23import com.android.quicksearchbox.util.Consumer; 24import com.android.quicksearchbox.util.Consumers; 25import com.google.common.annotations.VisibleForTesting; 26import com.google.common.base.CharMatcher; 27 28import android.app.Activity; 29import android.app.SearchManager; 30import android.content.DialogInterface; 31import android.content.Intent; 32import android.database.DataSetObserver; 33import android.net.Uri; 34import android.os.Bundle; 35import android.os.Debug; 36import android.os.Handler; 37import android.text.TextUtils; 38import android.util.Log; 39import android.view.Menu; 40import android.view.View; 41 42import java.io.File; 43import java.util.Collection; 44import java.util.List; 45import java.util.Set; 46 47/** 48 * The main activity for Quick Search Box. Shows the search UI. 49 * 50 */ 51public class SearchActivity extends Activity { 52 53 private static final boolean DBG = false; 54 private static final String TAG = "QSB.SearchActivity"; 55 private static final boolean TRACE = false; 56 57 private static final String SCHEME_CORPUS = "qsb.corpus"; 58 59 public static final String INTENT_ACTION_QSB_AND_SELECT_CORPUS 60 = "com.android.quicksearchbox.action.QSB_AND_SELECT_CORPUS"; 61 62 // Keys for the saved instance state. 63 private static final String INSTANCE_KEY_CORPUS = "corpus"; 64 private static final String INSTANCE_KEY_QUERY = "query"; 65 66 // Measures time from for last onCreate()/onNewIntent() call. 67 private LatencyTracker mStartLatencyTracker; 68 // Whether QSB is starting. True between the calls to onCreate()/onNewIntent() and onResume(). 69 private boolean mStarting; 70 // True if the user has taken some action, e.g. launching a search, voice search, 71 // or suggestions, since QSB was last started. 72 private boolean mTookAction; 73 74 private SearchActivityView mSearchActivityView; 75 76 private CorporaObserver mCorporaObserver; 77 78 private Bundle mAppSearchData; 79 80 private final Handler mHandler = new Handler(); 81 private final Runnable mUpdateSuggestionsTask = new Runnable() { 82 public void run() { 83 updateSuggestions(getQuery()); 84 } 85 }; 86 87 private final Runnable mShowInputMethodTask = new Runnable() { 88 public void run() { 89 mSearchActivityView.showInputMethodForQuery(); 90 } 91 }; 92 93 private OnDestroyListener mDestroyListener; 94 95 /** Called when the activity is first created. */ 96 @Override 97 public void onCreate(Bundle savedInstanceState) { 98 if (TRACE) startMethodTracing(); 99 recordStartTime(); 100 if (DBG) Log.d(TAG, "onCreate()"); 101 super.onCreate(savedInstanceState); 102 103 mSearchActivityView = setupContentView(); 104 105 mSearchActivityView.setMaxPromoted(getConfig().getMaxPromotedSuggestions()); 106 107 mSearchActivityView.setSearchClickListener(new SearchActivityView.SearchClickListener() { 108 public boolean onSearchClicked(int method) { 109 return SearchActivity.this.onSearchClicked(method); 110 } 111 }); 112 113 mSearchActivityView.setQueryListener(new SearchActivityView.QueryListener() { 114 public void onQueryChanged() { 115 updateSuggestionsBuffered(); 116 } 117 }); 118 119 mSearchActivityView.setSuggestionClickListener(new ClickHandler()); 120 121 mSearchActivityView.setSettingsButtonClickListener(new View.OnClickListener() { 122 public void onClick(View v) { 123 onSettingsClicked(); 124 } 125 }); 126 127 mSearchActivityView.setVoiceSearchButtonClickListener(new View.OnClickListener() { 128 public void onClick(View view) { 129 onVoiceSearchClicked(); 130 } 131 }); 132 133 mSearchActivityView.setExitClickListener(new View.OnClickListener() { 134 public void onClick(View v) { 135 finish(); 136 } 137 }); 138 139 // First get setup from intent 140 Intent intent = getIntent(); 141 setupFromIntent(intent); 142 // Then restore any saved instance state 143 restoreInstanceState(savedInstanceState); 144 145 // Do this at the end, to avoid updating the list view when setSource() 146 // is called. 147 mSearchActivityView.start(); 148 149 mCorporaObserver = new CorporaObserver(); 150 getCorpora().registerDataSetObserver(mCorporaObserver); 151 } 152 153 protected SearchActivityView setupContentView() { 154 setContentView(R.layout.search_activity); 155 return (SearchActivityView) findViewById(R.id.search_activity_view); 156 } 157 158 protected SearchActivityView getSearchActivityView() { 159 return mSearchActivityView; 160 } 161 162 private void startMethodTracing() { 163 File traceDir = getDir("traces", 0); 164 String traceFile = new File(traceDir, "qsb.trace").getAbsolutePath(); 165 Debug.startMethodTracing(traceFile); 166 } 167 168 @Override 169 protected void onNewIntent(Intent intent) { 170 if (DBG) Log.d(TAG, "onNewIntent()"); 171 recordStartTime(); 172 setIntent(intent); 173 setupFromIntent(intent); 174 } 175 176 private void recordStartTime() { 177 mStartLatencyTracker = new LatencyTracker(); 178 mStarting = true; 179 mTookAction = false; 180 } 181 182 protected void restoreInstanceState(Bundle savedInstanceState) { 183 if (savedInstanceState == null) return; 184 String corpusName = savedInstanceState.getString(INSTANCE_KEY_CORPUS); 185 String query = savedInstanceState.getString(INSTANCE_KEY_QUERY); 186 setCorpus(corpusName); 187 setQuery(query, false); 188 } 189 190 @Override 191 protected void onSaveInstanceState(Bundle outState) { 192 super.onSaveInstanceState(outState); 193 // We don't save appSearchData, since we always get the value 194 // from the intent and the user can't change it. 195 196 outState.putString(INSTANCE_KEY_CORPUS, getCorpusName()); 197 outState.putString(INSTANCE_KEY_QUERY, getQuery()); 198 } 199 200 private void setupFromIntent(Intent intent) { 201 if (DBG) Log.d(TAG, "setupFromIntent(" + intent.toUri(0) + ")"); 202 String corpusName = getCorpusNameFromUri(intent.getData()); 203 String query = intent.getStringExtra(SearchManager.QUERY); 204 Bundle appSearchData = intent.getBundleExtra(SearchManager.APP_DATA); 205 boolean selectAll = intent.getBooleanExtra(SearchManager.EXTRA_SELECT_QUERY, false); 206 207 setCorpus(corpusName); 208 setQuery(query, selectAll); 209 mAppSearchData = appSearchData; 210 211 if (startedIntoCorpusSelectionDialog()) { 212 mSearchActivityView.showCorpusSelectionDialog(); 213 } 214 } 215 216 public boolean startedIntoCorpusSelectionDialog() { 217 return INTENT_ACTION_QSB_AND_SELECT_CORPUS.equals(getIntent().getAction()); 218 } 219 220 /** 221 * Removes corpus selector intent action, so that BACK works normally after 222 * dismissing and reopening the corpus selector. 223 */ 224 public void clearStartedIntoCorpusSelectionDialog() { 225 Intent oldIntent = getIntent(); 226 if (SearchActivity.INTENT_ACTION_QSB_AND_SELECT_CORPUS.equals(oldIntent.getAction())) { 227 Intent newIntent = new Intent(oldIntent); 228 newIntent.setAction(SearchManager.INTENT_ACTION_GLOBAL_SEARCH); 229 setIntent(newIntent); 230 } 231 } 232 233 public static Uri getCorpusUri(Corpus corpus) { 234 if (corpus == null) return null; 235 return new Uri.Builder() 236 .scheme(SCHEME_CORPUS) 237 .authority(corpus.getName()) 238 .build(); 239 } 240 241 private String getCorpusNameFromUri(Uri uri) { 242 if (uri == null) return null; 243 if (!SCHEME_CORPUS.equals(uri.getScheme())) return null; 244 return uri.getAuthority(); 245 } 246 247 private Corpus getCorpus() { 248 return mSearchActivityView.getCorpus(); 249 } 250 251 private String getCorpusName() { 252 return mSearchActivityView.getCorpusName(); 253 } 254 255 private void setCorpus(String name) { 256 mSearchActivityView.setCorpus(name); 257 } 258 259 private QsbApplication getQsbApplication() { 260 return QsbApplication.get(this); 261 } 262 263 private Config getConfig() { 264 return getQsbApplication().getConfig(); 265 } 266 267 protected SearchSettings getSettings() { 268 return getQsbApplication().getSettings(); 269 } 270 271 private Corpora getCorpora() { 272 return getQsbApplication().getCorpora(); 273 } 274 275 private CorpusRanker getCorpusRanker() { 276 return getQsbApplication().getCorpusRanker(); 277 } 278 279 private ShortcutRepository getShortcutRepository() { 280 return getQsbApplication().getShortcutRepository(); 281 } 282 283 private SuggestionsProvider getSuggestionsProvider() { 284 return getQsbApplication().getSuggestionsProvider(); 285 } 286 287 private Logger getLogger() { 288 return getQsbApplication().getLogger(); 289 } 290 291 @VisibleForTesting 292 public void setOnDestroyListener(OnDestroyListener l) { 293 mDestroyListener = l; 294 } 295 296 @Override 297 protected void onDestroy() { 298 if (DBG) Log.d(TAG, "onDestroy()"); 299 getCorpora().unregisterDataSetObserver(mCorporaObserver); 300 mSearchActivityView.destroy(); 301 super.onDestroy(); 302 if (mDestroyListener != null) { 303 mDestroyListener.onDestroyed(); 304 } 305 } 306 307 @Override 308 protected void onStop() { 309 if (DBG) Log.d(TAG, "onStop()"); 310 if (!mTookAction) { 311 // TODO: This gets logged when starting other activities, e.g. by opening the search 312 // settings, or clicking a notification in the status bar. 313 // TODO we should log both sets of suggestions in 2-pane mode 314 getLogger().logExit(getCurrentSuggestions(), getQuery().length()); 315 } 316 // Close all open suggestion cursors. The query will be redone in onResume() 317 // if we come back to this activity. 318 mSearchActivityView.clearSuggestions(); 319 getQsbApplication().getShortcutRefresher().reset(); 320 mSearchActivityView.onStop(); 321 super.onStop(); 322 } 323 324 @Override 325 protected void onRestart() { 326 if (DBG) Log.d(TAG, "onRestart()"); 327 super.onRestart(); 328 } 329 330 @Override 331 protected void onResume() { 332 if (DBG) Log.d(TAG, "onResume()"); 333 super.onResume(); 334 updateSuggestionsBuffered(); 335 mSearchActivityView.onResume(); 336 if (TRACE) Debug.stopMethodTracing(); 337 } 338 339 @Override 340 public boolean onCreateOptionsMenu(Menu menu) { 341 super.onCreateOptionsMenu(menu); 342 getSettings().addMenuItems(menu); 343 return true; 344 } 345 346 @Override 347 public boolean onPrepareOptionsMenu(Menu menu) { 348 super.onPrepareOptionsMenu(menu); 349 getSettings().updateMenuItems(menu); 350 return true; 351 } 352 353 @Override 354 public void onWindowFocusChanged(boolean hasFocus) { 355 super.onWindowFocusChanged(hasFocus); 356 if (hasFocus) { 357 // Launch the IME after a bit 358 mHandler.postDelayed(mShowInputMethodTask, 0); 359 } 360 } 361 362 protected String getQuery() { 363 return mSearchActivityView.getQuery(); 364 } 365 366 protected void setQuery(String query, boolean selectAll) { 367 mSearchActivityView.setQuery(query, selectAll); 368 } 369 370 public CorpusSelectionDialog getCorpusSelectionDialog() { 371 CorpusSelectionDialog dialog = createCorpusSelectionDialog(); 372 dialog.setOwnerActivity(this); 373 dialog.setOnDismissListener(new CorpusSelectorDismissListener()); 374 return dialog; 375 } 376 377 protected CorpusSelectionDialog createCorpusSelectionDialog() { 378 return new CorpusSelectionDialog(this, getSettings()); 379 } 380 381 /** 382 * @return true if a search was performed as a result of this click, false otherwise. 383 */ 384 protected boolean onSearchClicked(int method) { 385 String query = CharMatcher.WHITESPACE.trimAndCollapseFrom(getQuery(), ' '); 386 if (DBG) Log.d(TAG, "Search clicked, query=" + query); 387 388 // Don't do empty queries 389 if (TextUtils.getTrimmedLength(query) == 0) return false; 390 391 Corpus searchCorpus = getSearchCorpus(); 392 if (searchCorpus == null) return false; 393 394 mTookAction = true; 395 396 // Log search start 397 getLogger().logSearch(getCorpus(), method, query.length()); 398 399 // Create shortcut 400 SuggestionData searchShortcut = searchCorpus.createSearchShortcut(query); 401 if (searchShortcut != null) { 402 ListSuggestionCursor cursor = new ListSuggestionCursor(query); 403 cursor.add(searchShortcut); 404 getShortcutRepository().reportClick(cursor, 0); 405 } 406 407 // Start search 408 startSearch(searchCorpus, query); 409 return true; 410 } 411 412 protected void startSearch(Corpus searchCorpus, String query) { 413 Intent intent = searchCorpus.createSearchIntent(query, mAppSearchData); 414 launchIntent(intent); 415 } 416 417 protected void onVoiceSearchClicked() { 418 if (DBG) Log.d(TAG, "Voice Search clicked"); 419 Corpus searchCorpus = getSearchCorpus(); 420 if (searchCorpus == null) return; 421 422 mTookAction = true; 423 424 // Log voice search start 425 getLogger().logVoiceSearch(searchCorpus); 426 427 // Start voice search 428 Intent intent = searchCorpus.createVoiceSearchIntent(mAppSearchData); 429 launchIntent(intent); 430 } 431 432 protected void onSettingsClicked() { 433 startActivity(getSettings().getSearchSettingsIntent()); 434 } 435 436 protected Corpus getSearchCorpus() { 437 return mSearchActivityView.getSearchCorpus(); 438 } 439 440 protected SuggestionCursor getCurrentSuggestions() { 441 return mSearchActivityView.getCurrentSuggestions(); 442 } 443 444 protected SuggestionCursor getCurrentSuggestions(SuggestionsAdapter adapter, int position) { 445 SuggestionCursor suggestions = adapter.getCurrentSuggestions(); 446 if (suggestions == null) { 447 return null; 448 } 449 int count = suggestions.getCount(); 450 if (position < 0 || position >= count) { 451 Log.w(TAG, "Invalid suggestion position " + position + ", count = " + count); 452 return null; 453 } 454 suggestions.moveTo(position); 455 return suggestions; 456 } 457 458 protected Set<Corpus> getCurrentIncludedCorpora() { 459 Suggestions suggestions = mSearchActivityView.getSuggestions(); 460 return suggestions == null ? null : suggestions.getIncludedCorpora(); 461 } 462 463 protected void launchIntent(Intent intent) { 464 if (DBG) Log.d(TAG, "launchIntent " + intent); 465 if (intent == null) { 466 return; 467 } 468 try { 469 startActivity(intent); 470 } catch (RuntimeException ex) { 471 // Since the intents for suggestions specified by suggestion providers, 472 // guard against them not being handled, not allowed, etc. 473 Log.e(TAG, "Failed to start " + intent.toUri(0), ex); 474 } 475 } 476 477 private boolean launchSuggestion(SuggestionsAdapter adapter, int position) { 478 SuggestionCursor suggestions = getCurrentSuggestions(adapter, position); 479 if (suggestions == null) return false; 480 481 if (DBG) Log.d(TAG, "Launching suggestion " + position); 482 mTookAction = true; 483 484 // Log suggestion click 485 getLogger().logSuggestionClick(position, suggestions, getCurrentIncludedCorpora(), 486 Logger.SUGGESTION_CLICK_TYPE_LAUNCH); 487 488 // Create shortcut 489 getShortcutRepository().reportClick(suggestions, position); 490 491 // Launch intent 492 launchSuggestion(suggestions, position); 493 494 return true; 495 } 496 497 protected void launchSuggestion(SuggestionCursor suggestions, int position) { 498 suggestions.moveTo(position); 499 Intent intent = SuggestionUtils.getSuggestionIntent(suggestions, mAppSearchData); 500 launchIntent(intent); 501 } 502 503 protected void clickedQuickContact(SuggestionsAdapter adapter, int position) { 504 SuggestionCursor suggestions = getCurrentSuggestions(adapter, position); 505 if (suggestions == null) return; 506 507 if (DBG) Log.d(TAG, "Used suggestion " + position); 508 mTookAction = true; 509 510 // Log suggestion click 511 getLogger().logSuggestionClick(position, suggestions, getCurrentIncludedCorpora(), 512 Logger.SUGGESTION_CLICK_TYPE_QUICK_CONTACT); 513 514 // Create shortcut 515 getShortcutRepository().reportClick(suggestions, position); 516 } 517 518 protected void refineSuggestion(SuggestionsAdapter adapter, int position) { 519 if (DBG) Log.d(TAG, "query refine clicked, pos " + position); 520 SuggestionCursor suggestions = getCurrentSuggestions(adapter, position); 521 if (suggestions == null) { 522 return; 523 } 524 String query = suggestions.getSuggestionQuery(); 525 if (TextUtils.isEmpty(query)) { 526 return; 527 } 528 529 // Log refine click 530 getLogger().logSuggestionClick(position, suggestions, getCurrentIncludedCorpora(), 531 Logger.SUGGESTION_CLICK_TYPE_REFINE); 532 533 // Put query + space in query text view 534 String queryWithSpace = query + ' '; 535 setQuery(queryWithSpace, false); 536 updateSuggestions(queryWithSpace); 537 mSearchActivityView.focusQueryTextView(); 538 } 539 540 protected boolean onSuggestionLongClicked(SuggestionsAdapter adapter, int position) { 541 if (DBG) Log.d(TAG, "Long clicked on suggestion " + position); 542 return false; 543 } 544 545 private void updateSuggestionsBuffered() { 546 mHandler.removeCallbacks(mUpdateSuggestionsTask); 547 long delay = getConfig().getTypingUpdateSuggestionsDelayMillis(); 548 mHandler.postDelayed(mUpdateSuggestionsTask, delay); 549 } 550 551 private void gotSuggestions(Suggestions suggestions) { 552 if (mStarting) { 553 mStarting = false; 554 String source = getIntent().getStringExtra(Search.SOURCE); 555 int latency = mStartLatencyTracker.getLatency(); 556 getLogger().logStart(latency, source, getCorpus(), 557 suggestions == null ? null : suggestions.getExpectedCorpora()); 558 getQsbApplication().onStartupComplete(); 559 } 560 } 561 562 private void getCorporaToQuery(Consumer<List<Corpus>> consumer) { 563 // Always query all corpora, so that all corpus result counts are valid 564 getCorpusRanker().getCorporaInAll(Consumers.createAsyncConsumer(mHandler, consumer)); 565 } 566 567 protected void getShortcutsForQuery(String query, Collection<Corpus> corporaToQuery, 568 final Suggestions suggestions) { 569 ShortcutRepository shortcutRepo = getShortcutRepository(); 570 if (shortcutRepo == null) return; 571 Consumer<ShortcutCursor> consumer = Consumers.createAsyncCloseableConsumer(mHandler, 572 new Consumer<ShortcutCursor>() { 573 public boolean consume(ShortcutCursor shortcuts) { 574 suggestions.setShortcuts(shortcuts); 575 return true; 576 } 577 }); 578 shortcutRepo.getShortcutsForQuery(query, corporaToQuery, consumer); 579 } 580 581 public void updateSuggestions(String untrimmedQuery) { 582 final String query = CharMatcher.WHITESPACE.trimLeadingFrom(untrimmedQuery); 583 if (DBG) Log.d(TAG, "updateSuggestions(\"" + query+"\"," + getCorpus() + ")"); 584 getQsbApplication().getSourceTaskExecutor().cancelPendingTasks(); 585 getCorporaToQuery(new Consumer<List<Corpus>>(){ 586 @Override 587 public boolean consume(List<Corpus> corporaToQuery) { 588 updateSuggestions(query, corporaToQuery); 589 return true; 590 } 591 }); 592 } 593 594 protected void updateSuggestions(String query, List<Corpus> corporaToQuery) { 595 Suggestions suggestions = getSuggestionsProvider().getSuggestions( 596 query, corporaToQuery); 597 getShortcutsForQuery(query, corporaToQuery, suggestions); 598 599 // Log start latency if this is the first suggestions update 600 gotSuggestions(suggestions); 601 602 showSuggestions(suggestions); 603 } 604 605 protected void showSuggestions(Suggestions suggestions) { 606 mSearchActivityView.setSuggestions(suggestions); 607 } 608 609 private class ClickHandler implements SuggestionClickListener { 610 611 public void onSuggestionQuickContactClicked(SuggestionsAdapter adapter, int position) { 612 clickedQuickContact(adapter, position); 613 } 614 615 public void onSuggestionClicked(SuggestionsAdapter adapter, int position) { 616 launchSuggestion(adapter, position); 617 } 618 619 public boolean onSuggestionLongClicked(SuggestionsAdapter adapter, int position) { 620 return SearchActivity.this.onSuggestionLongClicked(adapter, position); 621 } 622 623 public void onSuggestionQueryRefineClicked(SuggestionsAdapter adapter, int position) { 624 refineSuggestion(adapter, position); 625 } 626 } 627 628 private class CorpusSelectorDismissListener implements DialogInterface.OnDismissListener { 629 public void onDismiss(DialogInterface dialog) { 630 if (DBG) Log.d(TAG, "Corpus selector dismissed"); 631 clearStartedIntoCorpusSelectionDialog(); 632 } 633 } 634 635 private class CorporaObserver extends DataSetObserver { 636 @Override 637 public void onChanged() { 638 setCorpus(getCorpusName()); 639 updateSuggestions(getQuery()); 640 } 641 } 642 643 public interface OnDestroyListener { 644 void onDestroyed(); 645 } 646 647} 648