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