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