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 17 package com.android.quicksearchbox; 18 19 import android.app.Activity; 20 import android.app.SearchManager; 21 import android.content.Intent; 22 import android.net.Uri; 23 import android.os.Bundle; 24 import android.os.Debug; 25 import android.os.Handler; 26 import android.text.TextUtils; 27 import android.util.Log; 28 import android.view.Menu; 29 import android.view.View; 30 31 import com.android.common.Search; 32 import com.android.quicksearchbox.ui.SearchActivityView; 33 import com.android.quicksearchbox.ui.SuggestionClickListener; 34 import com.android.quicksearchbox.ui.SuggestionsAdapter; 35 import com.google.common.annotations.VisibleForTesting; 36 import com.google.common.base.CharMatcher; 37 38 import java.io.File; 39 40 /** 41 * The main activity for Quick Search Box. Shows the search UI. 42 * 43 */ 44 public class SearchActivity extends Activity { 45 46 private static final boolean DBG = false; 47 private static final String TAG = "QSB.SearchActivity"; 48 49 private static final String SCHEME_CORPUS = "qsb.corpus"; 50 51 private static final String INTENT_EXTRA_TRACE_START_UP = "trace_start_up"; 52 53 // Keys for the saved instance state. 54 private static final String INSTANCE_KEY_QUERY = "query"; 55 56 private static final String ACTIVITY_HELP_CONTEXT = "search"; 57 58 private boolean mTraceStartUp; 59 // Measures time from for last onCreate()/onNewIntent() call. 60 private LatencyTracker mStartLatencyTracker; 61 // Measures time spent inside onCreate() 62 private LatencyTracker mOnCreateTracker; 63 private int mOnCreateLatency; 64 // Whether QSB is starting. True between the calls to onCreate()/onNewIntent() and onResume(). 65 private boolean mStarting; 66 // True if the user has taken some action, e.g. launching a search, voice search, 67 // or suggestions, since QSB was last started. 68 private boolean mTookAction; 69 70 private SearchActivityView mSearchActivityView; 71 72 private Source mSource; 73 74 private Bundle mAppSearchData; 75 76 private final Handler mHandler = new Handler(); 77 private final Runnable mUpdateSuggestionsTask = new Runnable() { 78 @Override 79 public void run() { 80 updateSuggestions(); 81 } 82 }; 83 84 private final Runnable mShowInputMethodTask = new Runnable() { 85 @Override 86 public void run() { 87 mSearchActivityView.showInputMethodForQuery(); 88 } 89 }; 90 91 private OnDestroyListener mDestroyListener; 92 93 /** Called when the activity is first created. */ 94 @Override 95 public void onCreate(Bundle savedInstanceState) { 96 mTraceStartUp = getIntent().hasExtra(INTENT_EXTRA_TRACE_START_UP); 97 if (mTraceStartUp) { 98 String traceFile = new File(getDir("traces", 0), "qsb-start.trace").getAbsolutePath(); 99 Log.i(TAG, "Writing start-up trace to " + traceFile); 100 Debug.startMethodTracing(traceFile); 101 } 102 recordStartTime(); 103 if (DBG) Log.d(TAG, "onCreate()"); 104 super.onCreate(savedInstanceState); 105 106 // This forces the HTTP request to check the users domain to be 107 // sent as early as possible. 108 QsbApplication.get(this).getSearchBaseUrlHelper(); 109 110 mSource = QsbApplication.get(this).getGoogleSource(); 111 112 mSearchActivityView = setupContentView(); 113 114 if (getConfig().showScrollingResults()) { 115 mSearchActivityView.setMaxPromotedResults(getConfig().getMaxPromotedResults()); 116 } else { 117 mSearchActivityView.limitResultsToViewHeight(); 118 } 119 120 mSearchActivityView.setSearchClickListener(new SearchActivityView.SearchClickListener() { 121 @Override 122 public boolean onSearchClicked(int method) { 123 return SearchActivity.this.onSearchClicked(method); 124 } 125 }); 126 127 mSearchActivityView.setQueryListener(new SearchActivityView.QueryListener() { 128 @Override 129 public void onQueryChanged() { 130 updateSuggestionsBuffered(); 131 } 132 }); 133 134 mSearchActivityView.setSuggestionClickListener(new ClickHandler()); 135 136 mSearchActivityView.setVoiceSearchButtonClickListener(new View.OnClickListener() { 137 @Override 138 public void onClick(View view) { 139 onVoiceSearchClicked(); 140 } 141 }); 142 143 View.OnClickListener finishOnClick = new View.OnClickListener() { 144 @Override 145 public void onClick(View v) { 146 finish(); 147 } 148 }; 149 mSearchActivityView.setExitClickListener(finishOnClick); 150 151 // First get setup from intent 152 Intent intent = getIntent(); 153 setupFromIntent(intent); 154 // Then restore any saved instance state 155 restoreInstanceState(savedInstanceState); 156 157 // Do this at the end, to avoid updating the list view when setSource() 158 // is called. 159 mSearchActivityView.start(); 160 161 recordOnCreateDone(); 162 } 163 164 protected SearchActivityView setupContentView() { 165 setContentView(R.layout.search_activity); 166 return (SearchActivityView) findViewById(R.id.search_activity_view); 167 } 168 169 protected SearchActivityView getSearchActivityView() { 170 return mSearchActivityView; 171 } 172 173 @Override 174 protected void onNewIntent(Intent intent) { 175 if (DBG) Log.d(TAG, "onNewIntent()"); 176 recordStartTime(); 177 setIntent(intent); 178 setupFromIntent(intent); 179 } 180 181 private void recordStartTime() { 182 mStartLatencyTracker = new LatencyTracker(); 183 mOnCreateTracker = new LatencyTracker(); 184 mStarting = true; 185 mTookAction = false; 186 } 187 188 private void recordOnCreateDone() { 189 mOnCreateLatency = mOnCreateTracker.getLatency(); 190 } 191 192 protected void restoreInstanceState(Bundle savedInstanceState) { 193 if (savedInstanceState == null) return; 194 String query = savedInstanceState.getString(INSTANCE_KEY_QUERY); 195 setQuery(query, false); 196 } 197 198 @Override 199 protected void onSaveInstanceState(Bundle outState) { 200 super.onSaveInstanceState(outState); 201 // We don't save appSearchData, since we always get the value 202 // from the intent and the user can't change it. 203 204 outState.putString(INSTANCE_KEY_QUERY, getQuery()); 205 } 206 207 private void setupFromIntent(Intent intent) { 208 if (DBG) Log.d(TAG, "setupFromIntent(" + intent.toUri(0) + ")"); 209 String corpusName = getCorpusNameFromUri(intent.getData()); 210 String query = intent.getStringExtra(SearchManager.QUERY); 211 Bundle appSearchData = intent.getBundleExtra(SearchManager.APP_DATA); 212 boolean selectAll = intent.getBooleanExtra(SearchManager.EXTRA_SELECT_QUERY, false); 213 214 setQuery(query, selectAll); 215 mAppSearchData = appSearchData; 216 217 } 218 219 private String getCorpusNameFromUri(Uri uri) { 220 if (uri == null) return null; 221 if (!SCHEME_CORPUS.equals(uri.getScheme())) return null; 222 return uri.getAuthority(); 223 } 224 225 private QsbApplication getQsbApplication() { 226 return QsbApplication.get(this); 227 } 228 229 private Config getConfig() { 230 return getQsbApplication().getConfig(); 231 } 232 233 protected SearchSettings getSettings() { 234 return getQsbApplication().getSettings(); 235 } 236 237 private SuggestionsProvider getSuggestionsProvider() { 238 return getQsbApplication().getSuggestionsProvider(); 239 } 240 241 private Logger getLogger() { 242 return getQsbApplication().getLogger(); 243 } 244 245 @VisibleForTesting 246 public void setOnDestroyListener(OnDestroyListener l) { 247 mDestroyListener = l; 248 } 249 250 @Override 251 protected void onDestroy() { 252 if (DBG) Log.d(TAG, "onDestroy()"); 253 mSearchActivityView.destroy(); 254 super.onDestroy(); 255 if (mDestroyListener != null) { 256 mDestroyListener.onDestroyed(); 257 } 258 } 259 260 @Override 261 protected void onStop() { 262 if (DBG) Log.d(TAG, "onStop()"); 263 if (!mTookAction) { 264 // TODO: This gets logged when starting other activities, e.g. by opening the search 265 // settings, or clicking a notification in the status bar. 266 // TODO we should log both sets of suggestions in 2-pane mode 267 getLogger().logExit(getCurrentSuggestions(), getQuery().length()); 268 } 269 // Close all open suggestion cursors. The query will be redone in onResume() 270 // if we come back to this activity. 271 mSearchActivityView.clearSuggestions(); 272 mSearchActivityView.onStop(); 273 super.onStop(); 274 } 275 276 @Override 277 protected void onPause() { 278 if (DBG) Log.d(TAG, "onPause()"); 279 mSearchActivityView.onPause(); 280 super.onPause(); 281 } 282 283 @Override 284 protected void onRestart() { 285 if (DBG) Log.d(TAG, "onRestart()"); 286 super.onRestart(); 287 } 288 289 @Override 290 protected void onResume() { 291 if (DBG) Log.d(TAG, "onResume()"); 292 super.onResume(); 293 updateSuggestionsBuffered(); 294 mSearchActivityView.onResume(); 295 if (mTraceStartUp) Debug.stopMethodTracing(); 296 } 297 298 @Override 299 public boolean onPrepareOptionsMenu(Menu menu) { 300 // Since the menu items are dynamic, we recreate the menu every time. 301 menu.clear(); 302 createMenuItems(menu, true); 303 return true; 304 } 305 306 public void createMenuItems(Menu menu, boolean showDisabled) { 307 getQsbApplication().getHelp().addHelpMenuItem(menu, ACTIVITY_HELP_CONTEXT); 308 } 309 310 @Override 311 public void onWindowFocusChanged(boolean hasFocus) { 312 super.onWindowFocusChanged(hasFocus); 313 if (hasFocus) { 314 // Launch the IME after a bit 315 mHandler.postDelayed(mShowInputMethodTask, 0); 316 } 317 } 318 319 protected String getQuery() { 320 return mSearchActivityView.getQuery(); 321 } 322 323 protected void setQuery(String query, boolean selectAll) { 324 mSearchActivityView.setQuery(query, selectAll); 325 } 326 327 /** 328 * @return true if a search was performed as a result of this click, false otherwise. 329 */ 330 protected boolean onSearchClicked(int method) { 331 String query = CharMatcher.WHITESPACE.trimAndCollapseFrom(getQuery(), ' '); 332 if (DBG) Log.d(TAG, "Search clicked, query=" + query); 333 334 // Don't do empty queries 335 if (TextUtils.getTrimmedLength(query) == 0) return false; 336 337 mTookAction = true; 338 339 // Log search start 340 getLogger().logSearch(method, query.length()); 341 342 // Start search 343 startSearch(mSource, query); 344 return true; 345 } 346 347 protected void startSearch(Source searchSource, String query) { 348 Intent intent = searchSource.createSearchIntent(query, mAppSearchData); 349 launchIntent(intent); 350 } 351 352 protected void onVoiceSearchClicked() { 353 if (DBG) Log.d(TAG, "Voice Search clicked"); 354 355 mTookAction = true; 356 357 // Log voice search start 358 getLogger().logVoiceSearch(); 359 360 // Start voice search 361 Intent intent = mSource.createVoiceSearchIntent(mAppSearchData); 362 launchIntent(intent); 363 } 364 365 protected Source getSearchSource() { 366 return mSource; 367 } 368 369 protected SuggestionCursor getCurrentSuggestions() { 370 Suggestions suggestions = mSearchActivityView.getSuggestions(); 371 if (suggestions == null) { 372 return null; 373 } 374 return suggestions.getResult(); 375 } 376 377 protected SuggestionPosition getCurrentSuggestions(SuggestionsAdapter<?> adapter, long id) { 378 SuggestionPosition pos = adapter.getSuggestion(id); 379 if (pos == null) { 380 return null; 381 } 382 SuggestionCursor suggestions = pos.getCursor(); 383 int position = pos.getPosition(); 384 if (suggestions == null) { 385 return null; 386 } 387 int count = suggestions.getCount(); 388 if (position < 0 || position >= count) { 389 Log.w(TAG, "Invalid suggestion position " + position + ", count = " + count); 390 return null; 391 } 392 suggestions.moveTo(position); 393 return pos; 394 } 395 396 protected void launchIntent(Intent intent) { 397 if (DBG) Log.d(TAG, "launchIntent " + intent); 398 if (intent == null) { 399 return; 400 } 401 try { 402 startActivity(intent); 403 } catch (RuntimeException ex) { 404 // Since the intents for suggestions specified by suggestion providers, 405 // guard against them not being handled, not allowed, etc. 406 Log.e(TAG, "Failed to start " + intent.toUri(0), ex); 407 } 408 } 409 410 private boolean launchSuggestion(SuggestionsAdapter<?> adapter, long id) { 411 SuggestionPosition suggestion = getCurrentSuggestions(adapter, id); 412 if (suggestion == null) return false; 413 414 if (DBG) Log.d(TAG, "Launching suggestion " + id); 415 mTookAction = true; 416 417 // Log suggestion click 418 getLogger().logSuggestionClick(id, suggestion.getCursor(), 419 Logger.SUGGESTION_CLICK_TYPE_LAUNCH); 420 421 // Launch intent 422 launchSuggestion(suggestion.getCursor(), suggestion.getPosition()); 423 424 return true; 425 } 426 427 protected void launchSuggestion(SuggestionCursor suggestions, int position) { 428 suggestions.moveTo(position); 429 Intent intent = SuggestionUtils.getSuggestionIntent(suggestions, mAppSearchData); 430 launchIntent(intent); 431 } 432 433 protected void refineSuggestion(SuggestionsAdapter<?> adapter, long id) { 434 if (DBG) Log.d(TAG, "query refine clicked, pos " + id); 435 SuggestionPosition suggestion = getCurrentSuggestions(adapter, id); 436 if (suggestion == null) { 437 return; 438 } 439 String query = suggestion.getSuggestionQuery(); 440 if (TextUtils.isEmpty(query)) { 441 return; 442 } 443 444 // Log refine click 445 getLogger().logSuggestionClick(id, suggestion.getCursor(), 446 Logger.SUGGESTION_CLICK_TYPE_REFINE); 447 448 // Put query + space in query text view 449 String queryWithSpace = query + ' '; 450 setQuery(queryWithSpace, false); 451 updateSuggestions(); 452 mSearchActivityView.focusQueryTextView(); 453 } 454 455 private void updateSuggestionsBuffered() { 456 if (DBG) Log.d(TAG, "updateSuggestionsBuffered()"); 457 mHandler.removeCallbacks(mUpdateSuggestionsTask); 458 long delay = getConfig().getTypingUpdateSuggestionsDelayMillis(); 459 mHandler.postDelayed(mUpdateSuggestionsTask, delay); 460 } 461 462 private void gotSuggestions(Suggestions suggestions) { 463 if (mStarting) { 464 mStarting = false; 465 String source = getIntent().getStringExtra(Search.SOURCE); 466 int latency = mStartLatencyTracker.getLatency(); 467 getLogger().logStart(mOnCreateLatency, latency, source); 468 getQsbApplication().onStartupComplete(); 469 } 470 } 471 472 public void updateSuggestions() { 473 if (DBG) Log.d(TAG, "updateSuggestions()"); 474 final String query = CharMatcher.WHITESPACE.trimLeadingFrom(getQuery()); 475 updateSuggestions(query, mSource); 476 } 477 478 protected void updateSuggestions(String query, Source source) { 479 if (DBG) Log.d(TAG, "updateSuggestions(\"" + query+"\"," + source + ")"); 480 Suggestions suggestions = getSuggestionsProvider().getSuggestions( 481 query, source); 482 483 // Log start latency if this is the first suggestions update 484 gotSuggestions(suggestions); 485 486 showSuggestions(suggestions); 487 } 488 489 protected void showSuggestions(Suggestions suggestions) { 490 mSearchActivityView.setSuggestions(suggestions); 491 } 492 493 private class ClickHandler implements SuggestionClickListener { 494 495 @Override 496 public void onSuggestionClicked(SuggestionsAdapter<?> adapter, long id) { 497 launchSuggestion(adapter, id); 498 } 499 500 @Override 501 public void onSuggestionQueryRefineClicked(SuggestionsAdapter<?> adapter, long id) { 502 refineSuggestion(adapter, id); 503 } 504 } 505 506 public interface OnDestroyListener { 507 void onDestroyed(); 508 } 509 510 } 511