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