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