SearchActivity.java revision 782dd228e78e9294692d639597f96c26283968bb
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.quicksearchbox.ui.SuggestionClickListener; 20import com.android.quicksearchbox.ui.SuggestionViewFactory; 21import com.android.quicksearchbox.ui.SuggestionsAdapter; 22import com.android.quicksearchbox.ui.SuggestionsView; 23 24import android.app.Activity; 25import android.app.SearchManager; 26import android.content.Intent; 27import android.database.DataSetObserver; 28import android.graphics.Rect; 29import android.graphics.drawable.Animatable; 30import android.graphics.drawable.Drawable; 31import android.os.Bundle; 32import android.text.Editable; 33import android.text.TextUtils; 34import android.text.TextWatcher; 35import android.util.Log; 36import android.view.KeyEvent; 37import android.view.Menu; 38import android.view.View; 39import android.view.View.OnFocusChangeListener; 40import android.view.inputmethod.InputMethodManager; 41import android.widget.EditText; 42import android.widget.ImageButton; 43import android.widget.ProgressBar; 44import android.widget.ScrollView; 45 46// TODO: restore user query when using dpad to move up from top suggestion 47// TODO: focus tab handle on dpad down from query text view? 48// TODO: nicer progress animation. one per source? 49// TODO: handle long clicks 50// TODO: support action keys 51// TODO: support IME search action 52// TODO: allow typing everywhere in the UI 53// TODO: Show tabs at bottom too 54 55// TODO: don't show new results until there is at least one, or it's done 56// TODO: add timeout for source queries 57// TODO: group refreshes that happen close to each other. 58// TODO: use queryAfterZeroResults() 59 60// TODO: use permission to get extended Genie suggestions 61// TODO: support intent extras for source (e.g. launcher widget) 62// TODO: support intent extras for initial state, e.g. query, selection 63// TODO: make Config server-side configurable 64// TODO: log impressions 65// TODO: use source ranking 66 67/** 68 * The main activity for Quick Search Box. Shows the search UI. 69 * 70 */ 71public class SearchActivity extends Activity { 72 73 private static final boolean DBG = true; 74 private static final String TAG = "SearchActivity"; 75 76 // TODO: This is hidden in SearchManager 77 public final static String INTENT_ACTION_SEARCH_SETTINGS 78 = "android.search.action.SEARCH_SETTINGS"; 79 80 protected SuggestionsAdapter mSuggestionsAdapter; 81 82 protected EditText mQueryTextView; 83 84 protected ScrollView mSuggestionsScrollView; 85 protected SuggestionsView mSuggestionsView; 86 87 protected ImageButton mSearchGoButton; 88 protected ImageButton mVoiceSearchButton; 89 protected ProgressBar mProgressBar; 90 91 private Launcher mLauncher; 92 93 private boolean mUpdateSuggestions = true; 94 private String mUserQuery = ""; 95 private boolean mSelectAll = false; 96 97 /** Called when the activity is first created. */ 98 @Override 99 public void onCreate(Bundle savedInstanceState) { 100 if (DBG) Log.d(TAG, "onCreate()"); 101 // TODO: Use savedInstanceState to restore state 102 super.onCreate(savedInstanceState); 103 setContentView(R.layout.search_bar); 104 105 Config config = getConfig(); 106 SuggestionViewFactory viewFactory = getSuggestionViewFactory(); 107 mSuggestionsAdapter = new SuggestionsAdapter(viewFactory); 108 mSuggestionsAdapter 109 .setInitialSourceResultWaitMillis(config.getInitialSourceResultWaitMillis()); 110 mSuggestionsAdapter 111 .setSourceResultPublishDelayMillis(config.getSourceResultPublishDelayMillis()); 112 mSuggestionsAdapter.setSources(getSuggestionsProvider().getOrderedSources()); 113 114 mQueryTextView = (EditText) findViewById(R.id.search_src_text); 115 mSuggestionsScrollView = (ScrollView) findViewById(R.id.suggestions_scroll); 116 mSuggestionsView = (SuggestionsView) findViewById(R.id.suggestions); 117 mSuggestionsView.setSuggestionClickListener(new ClickHandler()); 118 mSuggestionsView.setInteractionListener(new InputMethodCloser()); 119 mSuggestionsView.setOnKeyListener(new SuggestionsListKeyListener()); 120 mSuggestionsView.setAdapter(mSuggestionsAdapter); 121 122 mSearchGoButton = (ImageButton) findViewById(R.id.search_go_btn); 123 mVoiceSearchButton = (ImageButton) findViewById(R.id.search_voice_btn); 124 125 Bundle appSearchData = null; 126 127 Intent intent = getIntent(); 128 // getIntent() currently always returns non-null, but the API does not guarantee 129 // that it always will. 130 if (intent != null) { 131 String initialQuery = intent.getStringExtra(SearchManager.QUERY); 132 if (!TextUtils.isEmpty(initialQuery)) { 133 mUserQuery = initialQuery; 134 } 135 // TODO: Declare an intent extra for selectAll 136 appSearchData = intent.getBundleExtra(SearchManager.APP_DATA); 137 } 138 mLauncher = new Launcher(this, appSearchData); 139 mVoiceSearchButton.setVisibility( 140 mLauncher.isVoiceSearchAvailable() ? View.VISIBLE : View.GONE); 141 142 mQueryTextView.addTextChangedListener(new SearchTextWatcher()); 143 mQueryTextView.setOnKeyListener(new QueryTextViewKeyListener()); 144 mQueryTextView.setOnFocusChangeListener(new SuggestListFocusListener()); 145 146 mSearchGoButton.setOnClickListener(new SearchGoButtonClickListener()); 147 mSearchGoButton.setOnKeyListener(new ButtonsKeyListener()); 148 149 mVoiceSearchButton.setOnClickListener(new VoiceSearchButtonClickListener()); 150 mVoiceSearchButton.setOnKeyListener(new ButtonsKeyListener()); 151 } 152 153 private QsbApplication getQsbApplication() { 154 return (QsbApplication) getApplication(); 155 } 156 157 private Config getConfig() { 158 return getQsbApplication().getConfig(); 159 } 160 161 private ShortcutRepository getShortcutRepository() { 162 return getQsbApplication().getShortcutRepository(); 163 } 164 165 private SuggestionsProvider getSuggestionsProvider() { 166 return getQsbApplication().getSuggestionsProvider(); 167 } 168 169 private SuggestionViewFactory getSuggestionViewFactory() { 170 return getQsbApplication().getSuggestionViewFactory(); 171 } 172 173 @Override 174 protected void onDestroy() { 175 if (DBG) Log.d(TAG, "onDestroy()"); 176 super.onDestroy(); 177 mSuggestionsView.setAdapter(null); // closes mSuggestionsAdapter 178 } 179 180 @Override 181 protected void onStart() { 182 if (DBG) Log.d(TAG, "onStart()"); 183 super.onStart(); 184 setQuery(mUserQuery, mSelectAll); 185 // Only select everything the first time after creating the activity. 186 mSelectAll = false; 187 updateSuggestions(mUserQuery); 188 } 189 190 @Override 191 protected void onStop() { 192 if (DBG) Log.d(TAG, "onResume()"); 193 // Close all open suggestion cursors. The query will be redone in onStart() 194 // if we come back to this activity. 195 mSuggestionsAdapter.setSuggestions(null); 196 super.onStop(); 197 } 198 199 @Override 200 protected void onResume() { 201 super.onResume(); 202 mQueryTextView.requestFocus(); 203 } 204 205 @Override 206 public boolean onCreateOptionsMenu(Menu menu) { 207 super.onCreateOptionsMenu(menu); 208 209 Intent settings = new Intent(INTENT_ACTION_SEARCH_SETTINGS); 210 settings.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); 211 // Don't show activity chooser if there are multiple search settings activities, 212 // e.g. from different QSB implementations. 213 settings.setPackage(this.getPackageName()); 214 menu.add(Menu.NONE, Menu.NONE, 0, R.string.menu_settings) 215 .setIcon(android.R.drawable.ic_menu_preferences).setAlphabeticShortcut('P') 216 .setIntent(settings); 217 218 return true; 219 } 220 221 private String getQuery() { 222 CharSequence q = mQueryTextView.getText(); 223 return q == null ? "" : q.toString(); 224 } 225 226 /** 227 * Restores the query entered by the user. 228 */ 229 private void restoreUserQuery() { 230 if (DBG) Log.d(TAG, "Restoring query to '" + mUserQuery + "'"); 231 setQuery(mUserQuery, false); 232 } 233 234 /** 235 * Sets the text in the query box. Does not update the suggestions, 236 * and does not change the saved user-entered query. 237 * {@link #restoreUserQuery()} will restore the query to the last 238 * user-entered query. 239 */ 240 private void setQuery(String query, boolean selectAll) { 241 mUpdateSuggestions = false; 242 mQueryTextView.setText(query); 243 setTextSelection(selectAll); 244 mUpdateSuggestions = true; 245 } 246 247 /** 248 * Sets the text selection in the query text view. 249 * 250 * @param selectAll If {@code true}, selects the entire query. 251 * If {@false}, no characters are selected, and the cursor is placed 252 * at the end of the query. 253 */ 254 private void setTextSelection(boolean selectAll) { 255 if (selectAll) { 256 mQueryTextView.setSelection(0, mQueryTextView.length()); 257 } else { 258 mQueryTextView.setSelection(mQueryTextView.length()); 259 } 260 } 261 262 protected void onSearchClicked() { 263 String query = getQuery(); 264 if (DBG) Log.d(TAG, "Search clicked, query=" + query); 265 mLauncher.startWebSearch(query); 266 } 267 268 protected void onVoiceSearchClicked() { 269 if (DBG) Log.d(TAG, "Voice Search clicked"); 270 mLauncher.startVoiceSearch(); 271 } 272 273 protected boolean launchSuggestion(SuggestionPosition suggestion) { 274 return launchSuggestion(suggestion, KeyEvent.KEYCODE_UNKNOWN, null); 275 } 276 277 protected boolean launchSuggestion(SuggestionPosition suggestion, 278 int actionKey, String actionMsg) { 279 if (DBG) Log.d(TAG, "Launching suggestion " + suggestion); 280 mLauncher.launchSuggestion(suggestion, actionKey, actionMsg); 281 getShortcutRepository().reportClick(suggestion); 282 // Update search widgets, since the top shortcuts can have changed. 283 SearchWidgetProvider.updateSearchWidgets(this); 284 return true; 285 } 286 287 protected boolean launchSuggestionSecondary(SuggestionPosition suggestion, Rect target) { 288 if (DBG) Log.d(TAG, "Clicked on suggestion icon " + suggestion); 289 mLauncher.launchSuggestionSecondary(suggestion, target); 290 getShortcutRepository().reportClick(suggestion); 291 return true; 292 } 293 294 protected boolean onSuggestionLongClicked(SuggestionPosition suggestion) { 295 SuggestionCursor sourceResult = suggestion.getSuggestion(); 296 if (DBG) Log.d(TAG, "Long clicked on suggestion " + sourceResult.getSuggestionText1()); 297 return false; 298 } 299 300 protected void onSuggestionSelected(SuggestionPosition suggestion) { 301 SuggestionCursor sourceResult = suggestion.getSuggestion(); 302 String displayQuery = sourceResult.getSuggestionDisplayQuery(); 303 if (DBG) { 304 Log.d(TAG, "Selected suggestion " + sourceResult.getSuggestionText1() 305 + ",displayQuery="+ displayQuery); 306 } 307 if (TextUtils.isEmpty(displayQuery)) { 308 restoreUserQuery(); 309 } else { 310 setQuery(displayQuery, false); 311 } 312 } 313 314 protected boolean onSuggestionKeyDown(SuggestionPosition suggestion, 315 int keyCode, KeyEvent event) { 316 // Treat enter or search as a click 317 if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_SEARCH) { 318 return launchSuggestion(suggestion); 319 } 320 321 // Handle source-specified action keys 322 String actionMsg = suggestion.getSuggestion().getActionKeyMsg(keyCode); 323 if (actionMsg != null) { 324 return launchSuggestion(suggestion, keyCode, actionMsg); 325 } 326 327 return false; 328 } 329 330 protected void onSourceSelected() { 331 if (DBG) Log.d(TAG, "No suggestion selected"); 332 restoreUserQuery(); 333 } 334 335 protected int getSelectedPosition() { 336 return mSuggestionsView.getSelectedPosition(); 337 } 338 339 protected SuggestionPosition getSelectedSuggestion() { 340 return mSuggestionsView.getSelectedSuggestion(); 341 } 342 343 /** 344 * Hides the input method. 345 */ 346 protected void hideInputMethod() { 347 InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); 348 if (imm != null) { 349 imm.hideSoftInputFromWindow(mQueryTextView.getWindowToken(), 0); 350 } 351 } 352 353 /** 354 * Hides the input method when the suggestions get focus. 355 */ 356 private class SuggestListFocusListener implements OnFocusChangeListener { 357 public void onFocusChange(View v, boolean focused) { 358 if (v == mQueryTextView) { 359 if (!focused) { 360 hideInputMethod(); 361 } else { 362 // TODO: clear list selection? 363 } 364 } 365 } 366 } 367 368 private void startSearchProgress() { 369 // TODO: Cache animation between calls? 370 mSearchGoButton.setImageResource(R.drawable.searching); 371 Animatable animation = (Animatable) mSearchGoButton.getDrawable(); 372 animation.start(); 373 } 374 375 private void stopSearchProgress() { 376 Drawable animation = mSearchGoButton.getDrawable(); 377 if (animation instanceof Animatable) { 378 // TODO: Is this needed, or is it done automatically when the 379 // animation is removed? 380 ((Animatable) animation).stop(); 381 } 382 mSearchGoButton.setImageResource(R.drawable.ic_btn_search); 383 } 384 385 private void scrollPastTabs() { 386 // TODO: Right after starting, the scroll view hasn't been measured, 387 // so it doesn't know whether its contents are tall enough to scroll. 388 int yOffset = mSuggestionsView.getTabHeight(); 389 mSuggestionsScrollView.scrollTo(0, yOffset); 390 if (DBG) { 391 Log.d(TAG, "After scrollTo(0," + yOffset + "), scrollY=" 392 + mSuggestionsScrollView.getScrollY()); 393 } 394 } 395 396 private void updateSuggestions(String query) { 397 LatencyTracker latency = new LatencyTracker(TAG); 398 Suggestions suggestions = getSuggestionsProvider().getSuggestions(query); 399 latency.addEvent("getSuggestions_done"); 400 if (!suggestions.isDone()) { 401 suggestions.registerDataSetObserver(new ProgressUpdater(suggestions)); 402 startSearchProgress(); 403 } else { 404 stopSearchProgress(); 405 } 406 mSuggestionsAdapter.setSuggestions(suggestions); 407 scrollPastTabs(); 408 latency.addEvent("shortcuts_shown"); 409 long userVisibleLatency = latency.getUserVisibleLatency(); 410 if (DBG) { 411 Log.d(TAG, "User visible latency (shortcuts): " + userVisibleLatency + " ms."); 412 } 413 } 414 415 /** 416 * Filters the suggestions list when the search text changes. 417 */ 418 private class SearchTextWatcher implements TextWatcher { 419 public void afterTextChanged(Editable s) { 420 if (mUpdateSuggestions) { 421 String query = s == null ? "" : s.toString(); 422 mUserQuery = query; 423 updateSuggestions(query); 424 } 425 } 426 427 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 428 } 429 430 public void onTextChanged(CharSequence s, int start, int before, int count) { 431 } 432 } 433 434 /** 435 * Handles non-text keys in the query text view. 436 */ 437 private class QueryTextViewKeyListener implements View.OnKeyListener { 438 public boolean onKey(View view, int keyCode, KeyEvent event) { 439 // Handle IME search action key 440 if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_UP) { 441 onSearchClicked(); 442 } 443 return false; 444 } 445 } 446 447 /** 448 * Handles key events on the search and voice search buttons, 449 * by refocusing to EditText. 450 */ 451 private class ButtonsKeyListener implements View.OnKeyListener { 452 public boolean onKey(View v, int keyCode, KeyEvent event) { 453 if (!event.isSystem() && 454 (keyCode != KeyEvent.KEYCODE_DPAD_UP) && 455 (keyCode != KeyEvent.KEYCODE_DPAD_LEFT) && 456 (keyCode != KeyEvent.KEYCODE_DPAD_RIGHT) && 457 (keyCode != KeyEvent.KEYCODE_DPAD_CENTER)) { 458 if (mQueryTextView.requestFocus()) { 459 return mQueryTextView.dispatchKeyEvent(event); 460 } 461 } 462 return false; 463 } 464 } 465 466 /** 467 * Handles key events on the suggestions list view. 468 * 469 * TODO: This actually never gets called, don't know why yet. 470 */ 471 private class SuggestionsListKeyListener implements View.OnKeyListener { 472 public boolean onKey(View v, int keyCode, KeyEvent event) { 473 if (event.getAction() == KeyEvent.ACTION_DOWN) { 474 SuggestionPosition suggestion = getSelectedSuggestion(); 475 if (suggestion != null) { 476 return onSuggestionKeyDown(suggestion, keyCode, event); 477 } 478 } 479 return false; 480 } 481 } 482 483 private class InputMethodCloser implements SuggestionsView.InteractionListener { 484 public void onInteraction() { 485 hideInputMethod(); 486 } 487 } 488 489 private class ClickHandler implements SuggestionClickListener { 490 public void onIconClicked(SuggestionPosition suggestion, Rect rect) { 491 launchSuggestionSecondary(suggestion, rect); 492 } 493 494 public void onItemClicked(SuggestionPosition suggestion) { 495 launchSuggestion(suggestion); 496 } 497 498 public void onItemSelected(SuggestionPosition suggestion) { 499 onSuggestionSelected(suggestion); 500 } 501 } 502 503 /** 504 * Listens for clicks on the search button. 505 */ 506 private class SearchGoButtonClickListener implements View.OnClickListener { 507 public void onClick(View view) { 508 onSearchClicked(); 509 } 510 } 511 512 /** 513 * Listens for clicks on the voice search button. 514 */ 515 private class VoiceSearchButtonClickListener implements View.OnClickListener { 516 public void onClick(View view) { 517 onVoiceSearchClicked(); 518 } 519 } 520 521 /** 522 * Updates the progress bar when the suggestions adapter changes its progress. 523 */ 524 private class ProgressUpdater extends DataSetObserver { 525 private final Suggestions mSuggestions; 526 527 public ProgressUpdater(Suggestions suggestions) { 528 mSuggestions = suggestions; 529 } 530 531 @Override 532 public void onChanged() { 533 if (mSuggestions.isDone()) { 534 stopSearchProgress(); 535 } 536 } 537 } 538} 539