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 onCreate(Bundle savedInstanceState)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 setupContentView()164 protected SearchActivityView setupContentView() { 165 setContentView(R.layout.search_activity); 166 return (SearchActivityView) findViewById(R.id.search_activity_view); 167 } 168 getSearchActivityView()169 protected SearchActivityView getSearchActivityView() { 170 return mSearchActivityView; 171 } 172 173 @Override onNewIntent(Intent intent)174 protected void onNewIntent(Intent intent) { 175 if (DBG) Log.d(TAG, "onNewIntent()"); 176 recordStartTime(); 177 setIntent(intent); 178 setupFromIntent(intent); 179 } 180 recordStartTime()181 private void recordStartTime() { 182 mStartLatencyTracker = new LatencyTracker(); 183 mOnCreateTracker = new LatencyTracker(); 184 mStarting = true; 185 mTookAction = false; 186 } 187 recordOnCreateDone()188 private void recordOnCreateDone() { 189 mOnCreateLatency = mOnCreateTracker.getLatency(); 190 } 191 restoreInstanceState(Bundle savedInstanceState)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 onSaveInstanceState(Bundle outState)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 setupFromIntent(Intent intent)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 getCorpusNameFromUri(Uri uri)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 getQsbApplication()225 private QsbApplication getQsbApplication() { 226 return QsbApplication.get(this); 227 } 228 getConfig()229 private Config getConfig() { 230 return getQsbApplication().getConfig(); 231 } 232 getSettings()233 protected SearchSettings getSettings() { 234 return getQsbApplication().getSettings(); 235 } 236 getSuggestionsProvider()237 private SuggestionsProvider getSuggestionsProvider() { 238 return getQsbApplication().getSuggestionsProvider(); 239 } 240 getLogger()241 private Logger getLogger() { 242 return getQsbApplication().getLogger(); 243 } 244 245 @VisibleForTesting setOnDestroyListener(OnDestroyListener l)246 public void setOnDestroyListener(OnDestroyListener l) { 247 mDestroyListener = l; 248 } 249 250 @Override onDestroy()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 onStop()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 onPause()277 protected void onPause() { 278 if (DBG) Log.d(TAG, "onPause()"); 279 mSearchActivityView.onPause(); 280 super.onPause(); 281 } 282 283 @Override onRestart()284 protected void onRestart() { 285 if (DBG) Log.d(TAG, "onRestart()"); 286 super.onRestart(); 287 } 288 289 @Override onResume()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 onPrepareOptionsMenu(Menu menu)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 createMenuItems(Menu menu, boolean showDisabled)306 public void createMenuItems(Menu menu, boolean showDisabled) { 307 getQsbApplication().getHelp().addHelpMenuItem(menu, ACTIVITY_HELP_CONTEXT); 308 } 309 310 @Override onWindowFocusChanged(boolean hasFocus)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 getQuery()319 protected String getQuery() { 320 return mSearchActivityView.getQuery(); 321 } 322 setQuery(String query, boolean selectAll)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 */ onSearchClicked(int method)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 startSearch(Source searchSource, String query)347 protected void startSearch(Source searchSource, String query) { 348 Intent intent = searchSource.createSearchIntent(query, mAppSearchData); 349 launchIntent(intent); 350 } 351 onVoiceSearchClicked()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 getSearchSource()365 protected Source getSearchSource() { 366 return mSource; 367 } 368 getCurrentSuggestions()369 protected SuggestionCursor getCurrentSuggestions() { 370 return mSearchActivityView.getSuggestions().getResult(); 371 } 372 getCurrentSuggestions(SuggestionsAdapter<?> adapter, long id)373 protected SuggestionPosition getCurrentSuggestions(SuggestionsAdapter<?> adapter, long id) { 374 SuggestionPosition pos = adapter.getSuggestion(id); 375 if (pos == null) { 376 return null; 377 } 378 SuggestionCursor suggestions = pos.getCursor(); 379 int position = pos.getPosition(); 380 if (suggestions == null) { 381 return null; 382 } 383 int count = suggestions.getCount(); 384 if (position < 0 || position >= count) { 385 Log.w(TAG, "Invalid suggestion position " + position + ", count = " + count); 386 return null; 387 } 388 suggestions.moveTo(position); 389 return pos; 390 } 391 launchIntent(Intent intent)392 protected void launchIntent(Intent intent) { 393 if (DBG) Log.d(TAG, "launchIntent " + intent); 394 if (intent == null) { 395 return; 396 } 397 try { 398 startActivity(intent); 399 } catch (RuntimeException ex) { 400 // Since the intents for suggestions specified by suggestion providers, 401 // guard against them not being handled, not allowed, etc. 402 Log.e(TAG, "Failed to start " + intent.toUri(0), ex); 403 } 404 } 405 launchSuggestion(SuggestionsAdapter<?> adapter, long id)406 private boolean launchSuggestion(SuggestionsAdapter<?> adapter, long id) { 407 SuggestionPosition suggestion = getCurrentSuggestions(adapter, id); 408 if (suggestion == null) return false; 409 410 if (DBG) Log.d(TAG, "Launching suggestion " + id); 411 mTookAction = true; 412 413 // Log suggestion click 414 getLogger().logSuggestionClick(id, suggestion.getCursor(), 415 Logger.SUGGESTION_CLICK_TYPE_LAUNCH); 416 417 // Launch intent 418 launchSuggestion(suggestion.getCursor(), suggestion.getPosition()); 419 420 return true; 421 } 422 launchSuggestion(SuggestionCursor suggestions, int position)423 protected void launchSuggestion(SuggestionCursor suggestions, int position) { 424 suggestions.moveTo(position); 425 Intent intent = SuggestionUtils.getSuggestionIntent(suggestions, mAppSearchData); 426 launchIntent(intent); 427 } 428 refineSuggestion(SuggestionsAdapter<?> adapter, long id)429 protected void refineSuggestion(SuggestionsAdapter<?> adapter, long id) { 430 if (DBG) Log.d(TAG, "query refine clicked, pos " + id); 431 SuggestionPosition suggestion = getCurrentSuggestions(adapter, id); 432 if (suggestion == null) { 433 return; 434 } 435 String query = suggestion.getSuggestionQuery(); 436 if (TextUtils.isEmpty(query)) { 437 return; 438 } 439 440 // Log refine click 441 getLogger().logSuggestionClick(id, suggestion.getCursor(), 442 Logger.SUGGESTION_CLICK_TYPE_REFINE); 443 444 // Put query + space in query text view 445 String queryWithSpace = query + ' '; 446 setQuery(queryWithSpace, false); 447 updateSuggestions(); 448 mSearchActivityView.focusQueryTextView(); 449 } 450 updateSuggestionsBuffered()451 private void updateSuggestionsBuffered() { 452 if (DBG) Log.d(TAG, "updateSuggestionsBuffered()"); 453 mHandler.removeCallbacks(mUpdateSuggestionsTask); 454 long delay = getConfig().getTypingUpdateSuggestionsDelayMillis(); 455 mHandler.postDelayed(mUpdateSuggestionsTask, delay); 456 } 457 gotSuggestions(Suggestions suggestions)458 private void gotSuggestions(Suggestions suggestions) { 459 if (mStarting) { 460 mStarting = false; 461 String source = getIntent().getStringExtra(Search.SOURCE); 462 int latency = mStartLatencyTracker.getLatency(); 463 getLogger().logStart(mOnCreateLatency, latency, source); 464 getQsbApplication().onStartupComplete(); 465 } 466 } 467 updateSuggestions()468 public void updateSuggestions() { 469 if (DBG) Log.d(TAG, "updateSuggestions()"); 470 final String query = CharMatcher.WHITESPACE.trimLeadingFrom(getQuery()); 471 updateSuggestions(query, mSource); 472 } 473 updateSuggestions(String query, Source source)474 protected void updateSuggestions(String query, Source source) { 475 if (DBG) Log.d(TAG, "updateSuggestions(\"" + query+"\"," + source + ")"); 476 Suggestions suggestions = getSuggestionsProvider().getSuggestions( 477 query, source); 478 479 // Log start latency if this is the first suggestions update 480 gotSuggestions(suggestions); 481 482 showSuggestions(suggestions); 483 } 484 showSuggestions(Suggestions suggestions)485 protected void showSuggestions(Suggestions suggestions) { 486 mSearchActivityView.setSuggestions(suggestions); 487 } 488 489 private class ClickHandler implements SuggestionClickListener { 490 491 @Override onSuggestionClicked(SuggestionsAdapter<?> adapter, long id)492 public void onSuggestionClicked(SuggestionsAdapter<?> adapter, long id) { 493 launchSuggestion(adapter, id); 494 } 495 496 @Override onSuggestionQueryRefineClicked(SuggestionsAdapter<?> adapter, long id)497 public void onSuggestionQueryRefineClicked(SuggestionsAdapter<?> adapter, long id) { 498 refineSuggestion(adapter, id); 499 } 500 } 501 502 public interface OnDestroyListener { onDestroyed()503 void onDestroyed(); 504 } 505 506 } 507