SearchActivity.java revision 883c1bf364e38c5b133afb55f8493a14b65f4dd4
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.common.Search; 20import com.android.quicksearchbox.ui.CorpusViewFactory; 21import com.android.quicksearchbox.ui.SuggestionClickListener; 22import com.android.quicksearchbox.ui.SuggestionSelectionListener; 23import com.android.quicksearchbox.ui.SuggestionsAdapter; 24import com.android.quicksearchbox.ui.SuggestionsFooter; 25import com.android.quicksearchbox.ui.SuggestionsView; 26 27import android.app.Activity; 28import android.app.Dialog; 29import android.app.SearchManager; 30import android.content.Intent; 31import android.database.DataSetObserver; 32import android.graphics.drawable.Animatable; 33import android.graphics.drawable.Drawable; 34import android.net.Uri; 35import android.os.Bundle; 36import android.os.SystemClock; 37import android.text.Editable; 38import android.text.TextUtils; 39import android.text.TextWatcher; 40import android.util.Log; 41import android.view.KeyEvent; 42import android.view.Menu; 43import android.view.View; 44import android.view.ViewGroup; 45import android.view.View.OnFocusChangeListener; 46import android.view.inputmethod.InputMethodManager; 47import android.widget.EditText; 48import android.widget.ImageButton; 49 50import java.util.ArrayList; 51 52/** 53 * The main activity for Quick Search Box. Shows the search UI. 54 * 55 */ 56public class SearchActivity extends Activity { 57 58 private static final boolean DBG = true; 59 private static final String TAG = "QSB.SearchActivity"; 60 61 private static final String SCHEME_CORPUS = "qsb.corpus"; 62 63 public static final String INTENT_ACTION_QSB_AND_SELECT_CORPUS 64 = "com.android.quicksearchbox.action.QSB_AND_SELECT_CORPUS"; 65 66 // The string used for privateImeOptions to identify to the IME that it should not show 67 // a microphone button since one already exists in the search dialog. 68 // TODO: This should move to android-common or something. 69 private static final String IME_OPTION_NO_MICROPHONE = "nm"; 70 71 // Keys for the saved instance state. 72 private static final String INSTANCE_KEY_CORPUS = "corpus"; 73 private static final String INSTANCE_KEY_USER_QUERY = "query"; 74 75 // Dialog IDs 76 private static final int CORPUS_SELECTION_DIALOG = 0; 77 78 // Timestamp for last onCreate()/onNewIntent() call, as returned by SystemClock.uptimeMillis(). 79 private long mStartTime; 80 // Whether QSB is starting. True between the calls to onCreate()/onNewIntent() and onResume(). 81 private boolean mStarting; 82 // True if the user has taken some action, e.g. launching a search, voice search, 83 // or suggestions, since QSB was last started. 84 private boolean mTookAction; 85 86 protected SuggestionsAdapter mSuggestionsAdapter; 87 88 protected EditText mQueryTextView; 89 // True if the query was empty on the previous call to updateQuery() 90 protected boolean mQueryWasEmpty = true; 91 92 protected SuggestionsView mSuggestionsView; 93 protected SuggestionsFooter mSuggestionsFooter; 94 95 protected ImageButton mSearchGoButton; 96 protected ImageButton mVoiceSearchButton; 97 protected ImageButton mCorpusIndicator; 98 99 private Launcher mLauncher; 100 101 private Corpus mCorpus; 102 private Bundle mAppSearchData; 103 private boolean mUpdateSuggestions; 104 private String mUserQuery; 105 private boolean mSelectAll; 106 107 /** Called when the activity is first created. */ 108 @Override 109 public void onCreate(Bundle savedInstanceState) { 110 recordStartTime(); 111 if (DBG) Log.d(TAG, "onCreate()"); 112 super.onCreate(savedInstanceState); 113 114 setContentView(R.layout.search_activity); 115 116 mSuggestionsAdapter = getQsbApplication().createSuggestionsAdapter(); 117 118 mQueryTextView = (EditText) findViewById(R.id.search_src_text); 119 mSuggestionsView = (SuggestionsView) findViewById(R.id.suggestions); 120 mSuggestionsView.setSuggestionClickListener(new ClickHandler()); 121 mSuggestionsView.setSuggestionSelectionListener(new SelectionHandler()); 122 mSuggestionsView.setInteractionListener(new InputMethodCloser()); 123 mSuggestionsView.setOnKeyListener(new SuggestionsViewKeyListener()); 124 mSuggestionsView.setOnFocusChangeListener(new SuggestListFocusListener()); 125 126 mSuggestionsFooter = getQsbApplication().createSuggestionsFooter(); 127 if (mSuggestionsFooter != null) { 128 ViewGroup footerFrame = (ViewGroup) findViewById(R.id.footer); 129 mSuggestionsFooter.addView(footerFrame); 130 } 131 132 mSearchGoButton = (ImageButton) findViewById(R.id.search_go_btn); 133 mVoiceSearchButton = (ImageButton) findViewById(R.id.search_voice_btn); 134 mCorpusIndicator = (ImageButton) findViewById(R.id.corpus_indicator); 135 136 mLauncher = new Launcher(this); 137 138 mQueryTextView.addTextChangedListener(new SearchTextWatcher()); 139 mQueryTextView.setOnKeyListener(new QueryTextViewKeyListener()); 140 mQueryTextView.setOnFocusChangeListener(new QueryTextViewFocusListener()); 141 142 mCorpusIndicator.setOnClickListener(new CorpusIndicatorClickListener()); 143 144 mSearchGoButton.setOnClickListener(new SearchGoButtonClickListener()); 145 146 mVoiceSearchButton.setOnClickListener(new VoiceSearchButtonClickListener()); 147 148 ButtonsKeyListener buttonsKeyListener = new ButtonsKeyListener(); 149 mSearchGoButton.setOnKeyListener(buttonsKeyListener); 150 mVoiceSearchButton.setOnKeyListener(buttonsKeyListener); 151 mCorpusIndicator.setOnKeyListener(buttonsKeyListener); 152 153 mUpdateSuggestions = true; 154 155 // First get setup from intent 156 Intent intent = getIntent(); 157 setupFromIntent(intent); 158 // Then restore any saved instance state 159 restoreInstanceState(savedInstanceState); 160 161 // Do this at the end, to avoid updating the list view when setSource() 162 // is called. 163 mSuggestionsView.setAdapter(mSuggestionsAdapter); 164 if (mSuggestionsFooter != null) { 165 mSuggestionsFooter.setAdapter(mSuggestionsAdapter); 166 } 167 } 168 169 @Override 170 protected void onNewIntent(Intent intent) { 171 recordStartTime(); 172 setIntent(intent); 173 setupFromIntent(intent); 174 } 175 176 private void recordStartTime() { 177 mStartTime = SystemClock.uptimeMillis(); 178 mStarting = true; 179 mTookAction = false; 180 } 181 182 protected void restoreInstanceState(Bundle savedInstanceState) { 183 if (savedInstanceState == null) return; 184 String corpusName = savedInstanceState.getString(INSTANCE_KEY_CORPUS); 185 String query = savedInstanceState.getString(INSTANCE_KEY_USER_QUERY); 186 setCorpus(getCorpus(corpusName)); 187 setUserQuery(query); 188 } 189 190 @Override 191 protected void onSaveInstanceState(Bundle outState) { 192 super.onSaveInstanceState(outState); 193 // We don't save appSearchData, since we always get the value 194 // from the intent and the user can't change it. 195 196 String corpusName = mCorpus == null ? null : mCorpus.getName(); 197 outState.putString(INSTANCE_KEY_CORPUS, corpusName); 198 outState.putString(INSTANCE_KEY_USER_QUERY, mUserQuery); 199 } 200 201 private void setupFromIntent(Intent intent) { 202 if (DBG) Log.d(TAG, "setupFromIntent(" + intent.toUri(0) + ")"); 203 Corpus corpus = getCorpusFromUri(intent.getData()); 204 String query = intent.getStringExtra(SearchManager.QUERY); 205 Bundle appSearchData = intent.getBundleExtra(SearchManager.APP_DATA); 206 207 setCorpus(corpus); 208 setUserQuery(query); 209 mSelectAll = intent.getBooleanExtra(SearchManager.EXTRA_SELECT_QUERY, false); 210 setAppSearchData(appSearchData); 211 212 if (INTENT_ACTION_QSB_AND_SELECT_CORPUS.equals(intent.getAction())) { 213 showCorpusSelectionDialog(); 214 } 215 } 216 217 public static Uri getCorpusUri(Corpus corpus) { 218 if (corpus == null) return null; 219 return new Uri.Builder() 220 .scheme(SCHEME_CORPUS) 221 .authority(corpus.getName()) 222 .build(); 223 } 224 225 private Corpus getCorpusFromUri(Uri uri) { 226 if (uri == null) return null; 227 if (!SCHEME_CORPUS.equals(uri.getScheme())) return null; 228 String name = uri.getAuthority(); 229 return getCorpus(name); 230 } 231 232 private Corpus getCorpus(String sourceName) { 233 if (sourceName == null) return null; 234 Corpus corpus = getCorpora().getCorpus(sourceName); 235 if (corpus == null) { 236 Log.w(TAG, "Unknown corpus " + sourceName); 237 return null; 238 } 239 return corpus; 240 } 241 242 private void setCorpus(Corpus corpus) { 243 if (DBG) Log.d(TAG, "setCorpus(" + corpus + ")"); 244 mCorpus = corpus; 245 Drawable sourceIcon; 246 if (corpus == null) { 247 sourceIcon = getCorpusViewFactory().getGlobalSearchIcon(); 248 } else { 249 sourceIcon = corpus.getCorpusIcon(); 250 } 251 mSuggestionsAdapter.setCorpus(corpus); 252 mCorpusIndicator.setImageDrawable(sourceIcon); 253 254 updateVoiceSearchButton(getQuery().length() == 0); 255 } 256 257 private QsbApplication getQsbApplication() { 258 return (QsbApplication) getApplication(); 259 } 260 261 private Corpora getCorpora() { 262 return getQsbApplication().getCorpora(); 263 } 264 265 private CorpusRanker getCorpusRanker() { 266 return getQsbApplication().getCorpusRanker(); 267 } 268 269 private ShortcutRepository getShortcutRepository() { 270 return getQsbApplication().getShortcutRepository(); 271 } 272 273 private SuggestionsProvider getSuggestionsProvider() { 274 return getQsbApplication().getSuggestionsProvider(mCorpus); 275 } 276 277 private CorpusViewFactory getCorpusViewFactory() { 278 return getQsbApplication().getCorpusViewFactory(); 279 } 280 281 private Logger getLogger() { 282 return getQsbApplication().getLogger(); 283 } 284 285 @Override 286 protected void onDestroy() { 287 if (DBG) Log.d(TAG, "onDestroy()"); 288 super.onDestroy(); 289 if (mSuggestionsFooter != null) { 290 mSuggestionsFooter.setAdapter(null); 291 } 292 mSuggestionsView.setAdapter(null); // closes mSuggestionsAdapter 293 } 294 295 @Override 296 protected void onStop() { 297 if (DBG) Log.d(TAG, "onStop()"); 298 // Close all open suggestion cursors. The query will be redone in onResume() 299 // if we come back to this activity. 300 mSuggestionsAdapter.setSuggestions(null); 301 getQsbApplication().getShortcutRefresher().reset(); 302 super.onStop(); 303 } 304 305 @Override 306 protected void onResume() { 307 if (DBG) Log.d(TAG, "onResume()"); 308 super.onResume(); 309 setQuery(mUserQuery, mSelectAll); 310 // Only select everything the first time after creating the activity. 311 mSelectAll = false; 312 updateSuggestions(mUserQuery); 313 mQueryTextView.requestFocus(); 314 if (mStarting) { 315 mStarting = false; 316 // Start up latency should not exceed 2^31 ms (~ 25 days). Note that 317 // SystemClock.uptimeMillis() does not advance during deep sleep. 318 int latency = (int) (SystemClock.uptimeMillis() - mStartTime); 319 String source = getIntent().getStringExtra(Search.SOURCE); 320 getLogger().logStart(latency, source, mCorpus, 321 getSuggestionsProvider().getOrderedCorpora()); 322 } 323 } 324 325 @Override 326 public boolean onCreateOptionsMenu(Menu menu) { 327 super.onCreateOptionsMenu(menu); 328 329 Intent settings = new Intent(SearchManager.INTENT_ACTION_SEARCH_SETTINGS); 330 settings.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); 331 // Don't show activity chooser if there are multiple search settings activities, 332 // e.g. from different QSB implementations. 333 settings.setPackage(this.getPackageName()); 334 menu.add(Menu.NONE, Menu.NONE, 0, R.string.menu_settings) 335 .setIcon(android.R.drawable.ic_menu_preferences).setAlphabeticShortcut('P') 336 .setIntent(settings); 337 338 return true; 339 } 340 341 /** 342 * Sets the query as typed by the user. Does not update the suggestions 343 * or the text in the query box. 344 */ 345 protected void setUserQuery(String userQuery) { 346 if (userQuery == null) userQuery = ""; 347 mUserQuery = userQuery; 348 } 349 350 protected void setAppSearchData(Bundle appSearchData) { 351 mAppSearchData = appSearchData; 352 mLauncher.setAppSearchData(appSearchData); 353 } 354 355 protected String getQuery() { 356 CharSequence q = mQueryTextView.getText(); 357 return q == null ? "" : q.toString(); 358 } 359 360 /** 361 * Restores the query entered by the user. 362 */ 363 private void restoreUserQuery() { 364 if (DBG) Log.d(TAG, "Restoring query to '" + mUserQuery + "'"); 365 setQuery(mUserQuery, false); 366 } 367 368 /** 369 * Sets the text in the query box. Does not update the suggestions, 370 * and does not change the saved user-entered query. 371 * {@link #restoreUserQuery()} will restore the query to the last 372 * user-entered query. 373 */ 374 private void setQuery(String query, boolean selectAll) { 375 mUpdateSuggestions = false; 376 mQueryTextView.setText(query); 377 setTextSelection(selectAll); 378 mUpdateSuggestions = true; 379 } 380 381 /** 382 * Sets the text selection in the query text view. 383 * 384 * @param selectAll If {@code true}, selects the entire query. 385 * If {@false}, no characters are selected, and the cursor is placed 386 * at the end of the query. 387 */ 388 private void setTextSelection(boolean selectAll) { 389 if (selectAll) { 390 mQueryTextView.setSelection(0, mQueryTextView.length()); 391 } else { 392 mQueryTextView.setSelection(mQueryTextView.length()); 393 } 394 } 395 396 private void updateQueryTextView(boolean queryEmpty) { 397 if (queryEmpty) { 398 mQueryTextView.setBackgroundResource(R.drawable.textfield_search_empty); 399 mQueryTextView.setCompoundDrawablesWithIntrinsicBounds( 400 R.drawable.placeholder_google, 0, 0, 0); 401 mQueryTextView.setCursorVisible(false); 402 } else { 403 mQueryTextView.setBackgroundResource(R.drawable.textfield_search); 404 mQueryTextView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); 405 mQueryTextView.setCursorVisible(true); 406 } 407 } 408 409 private void updateSearchGoButton(boolean queryEmpty) { 410 if (queryEmpty) { 411 mSearchGoButton.setVisibility(View.GONE); 412 } else { 413 mSearchGoButton.setVisibility(View.VISIBLE); 414 } 415 } 416 417 protected void updateVoiceSearchButton(boolean queryEmpty) { 418 if (queryEmpty && Launcher.shouldShowVoiceSearch(this, mCorpus)) { 419 mVoiceSearchButton.setVisibility(View.VISIBLE); 420 mQueryTextView.setPrivateImeOptions(IME_OPTION_NO_MICROPHONE); 421 } else { 422 mVoiceSearchButton.setVisibility(View.GONE); 423 mQueryTextView.setPrivateImeOptions(null); 424 } 425 } 426 427 protected void showCorpusSelectionDialog() { 428 showDialog(CORPUS_SELECTION_DIALOG); 429 } 430 431 protected void dismissCorpusSelectionDialog() { 432 dismissDialog(CORPUS_SELECTION_DIALOG); 433 } 434 435 @Override 436 protected Dialog onCreateDialog(int id, Bundle args) { 437 switch (id) { 438 case CORPUS_SELECTION_DIALOG: 439 return createCorpusSelectionDialog(); 440 default: 441 throw new IllegalArgumentException("Unknown dialog: " + id); 442 } 443 } 444 445 @Override 446 protected void onPrepareDialog(int id, Dialog dialog, Bundle args) { 447 switch (id) { 448 case CORPUS_SELECTION_DIALOG: 449 prepareCorpusSelectionDialog((CorpusSelectionDialog) dialog); 450 break; 451 default: 452 throw new IllegalArgumentException("Unknown dialog: " + id); 453 } 454 } 455 456 protected CorpusSelectionDialog createCorpusSelectionDialog() { 457 CorpusSelectionDialog dialog = new CorpusSelectionDialog(this); 458 dialog.setOwnerActivity(this); 459 return dialog; 460 } 461 462 protected void prepareCorpusSelectionDialog(CorpusSelectionDialog dialog) { 463 dialog.setCorpus(mCorpus); 464 dialog.setQuery(getQuery()); 465 dialog.setAppData(mAppSearchData); 466 } 467 468 protected void onSearchClicked(int method) { 469 String query = getQuery(); 470 if (DBG) Log.d(TAG, "Search clicked, query=" + query); 471 mTookAction = true; 472 getLogger().logSearch(mCorpus, method, query.length()); 473 mLauncher.startSearch(mCorpus, query); 474 } 475 476 protected void onVoiceSearchClicked() { 477 if (DBG) Log.d(TAG, "Voice Search clicked"); 478 mTookAction = true; 479 getLogger().logVoiceSearch(mCorpus); 480 481 // TODO: should this start voice search in the current source? 482 mLauncher.startVoiceSearch(mCorpus); 483 } 484 485 protected SuggestionCursor getSuggestions() { 486 return mSuggestionsAdapter.getCurrentSuggestions(); 487 } 488 489 protected boolean launchSuggestion(int position) { 490 if (DBG) Log.d(TAG, "Launching suggestion " + position); 491 mTookAction = true; 492 SuggestionCursor suggestions = getSuggestions(); 493 // TODO: This should be just the queried sources, but currently 494 // all sources are queried 495 ArrayList<Corpus> corpora = getCorpusRanker().rankCorpora(getCorpora().getEnabledCorpora()); 496 getLogger().logSuggestionClick(position, suggestions, corpora); 497 498 mLauncher.launchSuggestion(suggestions, position); 499 getShortcutRepository().reportClick(suggestions, position); 500 return true; 501 } 502 503 protected boolean onSuggestionLongClicked(int position) { 504 if (DBG) Log.d(TAG, "Long clicked on suggestion " + position); 505 return false; 506 } 507 508 protected boolean onSuggestionKeyDown(int position, int keyCode, KeyEvent event) { 509 // Treat enter or search as a click 510 if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_SEARCH) { 511 return launchSuggestion(position); 512 } 513 514 if (keyCode == KeyEvent.KEYCODE_DPAD_UP 515 && mSuggestionsView.getSelectedItemPosition() == 0) { 516 // Moved up from the top suggestion, restore the user query and focus query box 517 if (DBG) Log.d(TAG, "Up and out"); 518 restoreUserQuery(); 519 return false; // let the framework handle the move 520 } 521 522 if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT 523 || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { 524 // Moved left / right from a suggestion, keep current query, move 525 // focus to query box, and move cursor to far left / right 526 if (DBG) Log.d(TAG, "Left/right on a suggestion"); 527 int cursorPos = (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) ? 0 : mQueryTextView.length(); 528 mQueryTextView.setSelection(cursorPos); 529 mQueryTextView.requestFocus(); 530 // TODO: should we modify the list selection? 531 return true; 532 } 533 534 return false; 535 } 536 537 protected void onSourceSelected() { 538 if (DBG) Log.d(TAG, "No suggestion selected"); 539 restoreUserQuery(); 540 } 541 542 protected int getSelectedPosition() { 543 return mSuggestionsView.getSelectedPosition(); 544 } 545 546 /** 547 * Hides the input method. 548 */ 549 protected void hideInputMethod() { 550 InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); 551 if (imm != null) { 552 imm.hideSoftInputFromWindow(mQueryTextView.getWindowToken(), 0); 553 } 554 } 555 556 protected void showInputMethodForQuery() { 557 InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); 558 if (imm != null) { 559 imm.showSoftInput(mQueryTextView, 0); 560 } 561 } 562 563 /** 564 * Hides the input method when the suggestions get focus. 565 */ 566 private class SuggestListFocusListener implements OnFocusChangeListener { 567 public void onFocusChange(View v, boolean focused) { 568 if (DBG) Log.d(TAG, "Suggestions focus change, now: " + focused); 569 if (focused) { 570 // The suggestions list got focus, hide the input method 571 hideInputMethod(); 572 } 573 } 574 } 575 576 private class QueryTextViewFocusListener implements OnFocusChangeListener { 577 public void onFocusChange(View v, boolean focused) { 578 if (DBG) Log.d(TAG, "Query focus change, now: " + focused); 579 if (focused) { 580 // The query box got focus, show the input method if the 581 // query box got focus? 582 showInputMethodForQuery(); 583 } 584 } 585 } 586 587 private void startSearchProgress() { 588 // TODO: Do we need a progress indicator? 589 } 590 591 private void stopSearchProgress() { 592 Drawable animation = mSearchGoButton.getDrawable(); 593 if (animation instanceof Animatable) { 594 // TODO: Is this needed, or is it done automatically when the 595 // animation is removed? 596 ((Animatable) animation).stop(); 597 } 598 mSearchGoButton.setImageResource(R.drawable.ic_btn_search); 599 } 600 601 private void updateSuggestions(String query) { 602 query = ltrim(query); 603 LatencyTracker latency = new LatencyTracker(TAG); 604 Suggestions suggestions = getSuggestionsProvider().getSuggestions(query); 605 latency.addEvent("getSuggestions_done"); 606 if (!suggestions.isDone()) { 607 suggestions.registerDataSetObserver(new ProgressUpdater(suggestions)); 608 startSearchProgress(); 609 } else { 610 stopSearchProgress(); 611 } 612 mSuggestionsAdapter.setSuggestions(suggestions); 613 latency.addEvent("shortcuts_shown"); 614 long userVisibleLatency = latency.getUserVisibleLatency(); 615 if (DBG) { 616 Log.d(TAG, "User visible latency (shortcuts): " + userVisibleLatency + " ms."); 617 } 618 } 619 620 private boolean forwardKeyToQueryTextView(int keyCode, KeyEvent event) { 621 if (!event.isSystem() && !isDpadKey(keyCode)) { 622 if (DBG) Log.d(TAG, "Forwarding key to query box: " + event); 623 if (mQueryTextView.requestFocus()) { 624 return mQueryTextView.dispatchKeyEvent(event); 625 } 626 } 627 return false; 628 } 629 630 private boolean isDpadKey(int keyCode) { 631 switch (keyCode) { 632 case KeyEvent.KEYCODE_DPAD_UP: 633 case KeyEvent.KEYCODE_DPAD_DOWN: 634 case KeyEvent.KEYCODE_DPAD_LEFT: 635 case KeyEvent.KEYCODE_DPAD_RIGHT: 636 case KeyEvent.KEYCODE_DPAD_CENTER: 637 return true; 638 default: 639 return false; 640 } 641 } 642 643 /** 644 * Filters the suggestions list when the search text changes. 645 */ 646 private class SearchTextWatcher implements TextWatcher { 647 public void afterTextChanged(Editable s) { 648 boolean empty = s.length() == 0; 649 if (empty != mQueryWasEmpty) { 650 mQueryWasEmpty = empty; 651 updateQueryTextView(empty); 652 updateSearchGoButton(empty); 653 updateVoiceSearchButton(empty); 654 } 655 if (mUpdateSuggestions) { 656 String query = s == null ? "" : s.toString(); 657 setUserQuery(query); 658 updateSuggestions(query); 659 } 660 } 661 662 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 663 } 664 665 public void onTextChanged(CharSequence s, int start, int before, int count) { 666 } 667 } 668 669 /** 670 * Handles non-text keys in the query text view. 671 */ 672 private class QueryTextViewKeyListener implements View.OnKeyListener { 673 public boolean onKey(View view, int keyCode, KeyEvent event) { 674 // Handle IME search action key 675 if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_UP) { 676 onSearchClicked(Logger.SEARCH_METHOD_KEYBOARD); 677 } 678 return false; 679 } 680 } 681 682 /** 683 * Handles key events on the search and voice search buttons, 684 * by refocusing to EditText. 685 */ 686 private class ButtonsKeyListener implements View.OnKeyListener { 687 public boolean onKey(View v, int keyCode, KeyEvent event) { 688 return forwardKeyToQueryTextView(keyCode, event); 689 } 690 } 691 692 /** 693 * Handles key events on the suggestions list view. 694 */ 695 private class SuggestionsViewKeyListener implements View.OnKeyListener { 696 public boolean onKey(View v, int keyCode, KeyEvent event) { 697 if (event.getAction() == KeyEvent.ACTION_DOWN) { 698 int position = getSelectedPosition(); 699 if (onSuggestionKeyDown(position, keyCode, event)) { 700 return true; 701 } 702 } 703 return forwardKeyToQueryTextView(keyCode, event); 704 } 705 } 706 707 private class InputMethodCloser implements SuggestionsView.InteractionListener { 708 public void onInteraction() { 709 hideInputMethod(); 710 } 711 } 712 713 private class ClickHandler implements SuggestionClickListener { 714 public void onSuggestionClicked(int position) { 715 launchSuggestion(position); 716 } 717 718 public boolean onSuggestionLongClicked(int position) { 719 return SearchActivity.this.onSuggestionLongClicked(position); 720 } 721 } 722 723 private class SelectionHandler implements SuggestionSelectionListener { 724 public void onSuggestionSelected(int position) { 725 SuggestionCursor suggestions = getSuggestions(); 726 suggestions.moveTo(position); 727 String displayQuery = suggestions.getSuggestionDisplayQuery(); 728 if (TextUtils.isEmpty(displayQuery)) { 729 restoreUserQuery(); 730 } else { 731 setQuery(displayQuery, false); 732 } 733 } 734 735 public void onNothingSelected() { 736 // This happens when a suggestion has been selected with the 737 // dpad / trackball and then a different UI element is touched. 738 // Do nothing, since we want to keep the query of the selection 739 // in the search box. 740 } 741 } 742 743 /** 744 * Listens for clicks on the source selector. 745 */ 746 private class SearchGoButtonClickListener implements View.OnClickListener { 747 public void onClick(View view) { 748 onSearchClicked(Logger.SEARCH_METHOD_BUTTON); 749 } 750 } 751 752 /** 753 * Listens for clicks on the search button. 754 */ 755 private class CorpusIndicatorClickListener implements View.OnClickListener { 756 public void onClick(View view) { 757 showCorpusSelectionDialog(); 758 } 759 } 760 761 /** 762 * Listens for clicks on the voice search button. 763 */ 764 private class VoiceSearchButtonClickListener implements View.OnClickListener { 765 public void onClick(View view) { 766 onVoiceSearchClicked(); 767 } 768 } 769 770 /** 771 * Updates the progress bar when the suggestions adapter changes its progress. 772 */ 773 private class ProgressUpdater extends DataSetObserver { 774 private final Suggestions mSuggestions; 775 776 public ProgressUpdater(Suggestions suggestions) { 777 mSuggestions = suggestions; 778 } 779 780 @Override 781 public void onChanged() { 782 if (mSuggestions.isDone()) { 783 stopSearchProgress(); 784 } 785 } 786 } 787 788 private static String ltrim(String text) { 789 int start = 0; 790 int length = text.length(); 791 while (start < length && Character.isWhitespace(text.charAt(start))) { 792 start++; 793 } 794 return start > 0 ? text.substring(start, length) : text; 795 } 796 797} 798