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