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