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