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