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