SearchActivity.java revision f5e1499b48ac06de4e6e6460a7f14c3a7ac4eadd
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.SuggestionsAdapter; 23import com.android.quicksearchbox.ui.SuggestionsView; 24 25import android.app.Activity; 26import android.app.SearchManager; 27import android.content.DialogInterface; 28import android.content.Intent; 29import android.database.DataSetObserver; 30import android.graphics.drawable.Drawable; 31import android.net.Uri; 32import android.os.Bundle; 33import android.os.Debug; 34import android.os.Handler; 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.View.OnFocusChangeListener; 43import android.view.inputmethod.InputMethodManager; 44import android.widget.AbsListView; 45import android.widget.EditText; 46import android.widget.ImageButton; 47 48import java.io.File; 49import java.util.Collection; 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 private static final boolean TRACE = false; 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_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 private CorporaObserver mCorporaObserver; 88 89 protected EditText mQueryTextView; 90 // True if the query was empty on the previous call to updateQuery() 91 protected boolean mQueryWasEmpty = true; 92 93 protected SuggestionsView mSuggestionsView; 94 95 protected ImageButton mSearchGoButton; 96 protected ImageButton mVoiceSearchButton; 97 protected ImageButton mCorpusIndicator; 98 99 private VoiceSearch mVoiceSearch; 100 101 private Corpus mCorpus; 102 private Bundle mAppSearchData; 103 private boolean mUpdateSuggestions; 104 105 private final Handler mHandler = new Handler(); 106 private final Runnable mUpdateSuggestionsTask = new Runnable() { 107 public void run() { 108 updateSuggestions(getQuery()); 109 } 110 }; 111 112 private final Runnable mShowInputMethodTask = new Runnable() { 113 public void run() { 114 showInputMethodForQuery(); 115 } 116 }; 117 118 /** Called when the activity is first created. */ 119 @Override 120 public void onCreate(Bundle savedInstanceState) { 121 if (TRACE) startMethodTracing(); 122 recordStartTime(); 123 if (DBG) Log.d(TAG, "onCreate()"); 124 super.onCreate(savedInstanceState); 125 126 setContentView(); 127 mSuggestionsAdapter = getQsbApplication().createSuggestionsAdapter(); 128 129 mQueryTextView = (EditText) findViewById(R.id.search_src_text); 130 mSuggestionsView = (SuggestionsView) findViewById(R.id.suggestions); 131 mSuggestionsView.setSuggestionClickListener(new ClickHandler()); 132 mSuggestionsView.setOnScrollListener(new InputMethodCloser()); 133 mSuggestionsView.setOnKeyListener(new SuggestionsViewKeyListener()); 134 mSuggestionsView.setOnFocusChangeListener(new SuggestListFocusListener()); 135 136 mSearchGoButton = (ImageButton) findViewById(R.id.search_go_btn); 137 mVoiceSearchButton = (ImageButton) findViewById(R.id.search_voice_btn); 138 mCorpusIndicator = (ImageButton) findViewById(R.id.corpus_indicator); 139 140 mVoiceSearch = new VoiceSearch(this); 141 142 mQueryTextView.addTextChangedListener(new SearchTextWatcher()); 143 mQueryTextView.setOnKeyListener(new QueryTextViewKeyListener()); 144 mQueryTextView.setOnFocusChangeListener(new QueryTextViewFocusListener()); 145 146 mCorpusIndicator.setOnClickListener(new CorpusIndicatorClickListener()); 147 148 mSearchGoButton.setOnClickListener(new SearchGoButtonClickListener()); 149 150 mVoiceSearchButton.setOnClickListener(new VoiceSearchButtonClickListener()); 151 152 ButtonsKeyListener buttonsKeyListener = new ButtonsKeyListener(); 153 mSearchGoButton.setOnKeyListener(buttonsKeyListener); 154 mVoiceSearchButton.setOnKeyListener(buttonsKeyListener); 155 mCorpusIndicator.setOnKeyListener(buttonsKeyListener); 156 157 mUpdateSuggestions = true; 158 159 // First get setup from intent 160 Intent intent = getIntent(); 161 setupFromIntent(intent); 162 // Then restore any saved instance state 163 restoreInstanceState(savedInstanceState); 164 165 // Do this at the end, to avoid updating the list view when setSource() 166 // is called. 167 mSuggestionsView.setAdapter(mSuggestionsAdapter); 168 169 mCorporaObserver = new CorporaObserver(); 170 getCorpora().registerDataSetObserver(mCorporaObserver); 171 } 172 173 protected void setContentView() { 174 setContentView(R.layout.search_activity); 175 } 176 177 private void startMethodTracing() { 178 File traceDir = getDir("traces", 0); 179 String traceFile = new File(traceDir, "qsb.trace").getAbsolutePath(); 180 Debug.startMethodTracing(traceFile); 181 } 182 183 @Override 184 protected void onNewIntent(Intent intent) { 185 if (DBG) Log.d(TAG, "onNewIntent()"); 186 recordStartTime(); 187 setIntent(intent); 188 setupFromIntent(intent); 189 } 190 191 private void recordStartTime() { 192 mStartLatencyTracker = new LatencyTracker(); 193 mStarting = true; 194 mTookAction = false; 195 } 196 197 protected void restoreInstanceState(Bundle savedInstanceState) { 198 if (savedInstanceState == null) return; 199 String corpusName = savedInstanceState.getString(INSTANCE_KEY_CORPUS); 200 String query = savedInstanceState.getString(INSTANCE_KEY_QUERY); 201 setCorpus(corpusName); 202 setQuery(query, false); 203 } 204 205 @Override 206 protected void onSaveInstanceState(Bundle outState) { 207 super.onSaveInstanceState(outState); 208 // We don't save appSearchData, since we always get the value 209 // from the intent and the user can't change it. 210 211 outState.putString(INSTANCE_KEY_CORPUS, getCorpusName()); 212 outState.putString(INSTANCE_KEY_QUERY, getQuery()); 213 } 214 215 private void setupFromIntent(Intent intent) { 216 if (DBG) Log.d(TAG, "setupFromIntent(" + intent.toUri(0) + ")"); 217 String corpusName = getCorpusNameFromUri(intent.getData()); 218 String query = intent.getStringExtra(SearchManager.QUERY); 219 Bundle appSearchData = intent.getBundleExtra(SearchManager.APP_DATA); 220 boolean selectAll = intent.getBooleanExtra(SearchManager.EXTRA_SELECT_QUERY, false); 221 222 setCorpus(corpusName); 223 setQuery(query, selectAll); 224 mAppSearchData = appSearchData; 225 226 if (startedIntoCorpusSelectionDialog()) { 227 showCorpusSelectionDialog(); 228 } 229 } 230 231 public boolean startedIntoCorpusSelectionDialog() { 232 return INTENT_ACTION_QSB_AND_SELECT_CORPUS.equals(getIntent().getAction()); 233 } 234 235 /** 236 * Removes corpus selector intent action, so that BACK works normally after 237 * dismissing and reopening the corpus selector. 238 */ 239 private void clearStartedIntoCorpusSelectionDialog() { 240 Intent oldIntent = getIntent(); 241 if (SearchActivity.INTENT_ACTION_QSB_AND_SELECT_CORPUS.equals(oldIntent.getAction())) { 242 Intent newIntent = new Intent(oldIntent); 243 newIntent.setAction(SearchManager.INTENT_ACTION_GLOBAL_SEARCH); 244 setIntent(newIntent); 245 } 246 } 247 248 public static Uri getCorpusUri(Corpus corpus) { 249 if (corpus == null) return null; 250 return new Uri.Builder() 251 .scheme(SCHEME_CORPUS) 252 .authority(corpus.getName()) 253 .build(); 254 } 255 256 private String getCorpusNameFromUri(Uri uri) { 257 if (uri == null) return null; 258 if (!SCHEME_CORPUS.equals(uri.getScheme())) return null; 259 return uri.getAuthority(); 260 } 261 262 private Corpus getCorpus(String sourceName) { 263 if (sourceName == null) return null; 264 Corpus corpus = getCorpora().getCorpus(sourceName); 265 if (corpus == null) { 266 Log.w(TAG, "Unknown corpus " + sourceName); 267 return null; 268 } 269 return corpus; 270 } 271 272 private void setCorpus(String corpusName) { 273 if (DBG) Log.d(TAG, "setCorpus(" + corpusName + ")"); 274 mCorpus = getCorpus(corpusName); 275 Drawable sourceIcon; 276 if (mCorpus == null) { 277 sourceIcon = getCorpusViewFactory().getGlobalSearchIcon(); 278 } else { 279 sourceIcon = mCorpus.getCorpusIcon(); 280 } 281 mSuggestionsAdapter.setCorpus(mCorpus); 282 mCorpusIndicator.setImageDrawable(sourceIcon); 283 284 updateUi(getQuery().length() == 0); 285 } 286 287 private String getCorpusName() { 288 return mCorpus == null ? null : mCorpus.getName(); 289 } 290 291 private QsbApplication getQsbApplication() { 292 return (QsbApplication) getApplication(); 293 } 294 295 private Config getConfig() { 296 return getQsbApplication().getConfig(); 297 } 298 299 private Corpora getCorpora() { 300 return getQsbApplication().getCorpora(); 301 } 302 303 private ShortcutRepository getShortcutRepository() { 304 return getQsbApplication().getShortcutRepository(); 305 } 306 307 private SuggestionsProvider getSuggestionsProvider() { 308 return getQsbApplication().getSuggestionsProvider(); 309 } 310 311 private CorpusViewFactory getCorpusViewFactory() { 312 return getQsbApplication().getCorpusViewFactory(); 313 } 314 315 private Logger getLogger() { 316 return getQsbApplication().getLogger(); 317 } 318 319 @Override 320 protected void onDestroy() { 321 if (DBG) Log.d(TAG, "onDestroy()"); 322 super.onDestroy(); 323 getCorpora().unregisterDataSetObserver(mCorporaObserver); 324 mSuggestionsView.setAdapter(null); // closes mSuggestionsAdapter 325 } 326 327 @Override 328 protected void onStop() { 329 if (DBG) Log.d(TAG, "onStop()"); 330 if (!mTookAction) { 331 // TODO: This gets logged when starting other activities, e.g. by opening he search 332 // settings, or clicking a notification in the status bar. 333 getLogger().logExit(getCurrentSuggestions(), getQuery().length()); 334 } 335 // Close all open suggestion cursors. The query will be redone in onResume() 336 // if we come back to this activity. 337 mSuggestionsAdapter.setSuggestions(null); 338 getQsbApplication().getShortcutRefresher().reset(); 339 dismissCorpusSelectionDialog(); 340 super.onStop(); 341 } 342 343 @Override 344 protected void onRestart() { 345 if (DBG) Log.d(TAG, "onRestart()"); 346 super.onRestart(); 347 } 348 349 @Override 350 protected void onResume() { 351 if (DBG) Log.d(TAG, "onResume()"); 352 super.onResume(); 353 updateSuggestionsBuffered(); 354 if (!isCorpusSelectionDialogShowing()) { 355 mQueryTextView.requestFocus(); 356 } 357 if (TRACE) Debug.stopMethodTracing(); 358 } 359 360 @Override 361 public boolean onCreateOptionsMenu(Menu menu) { 362 super.onCreateOptionsMenu(menu); 363 SearchSettings.addSearchSettingsMenuItem(this, menu); 364 return true; 365 } 366 367 @Override 368 public void onWindowFocusChanged(boolean hasFocus) { 369 super.onWindowFocusChanged(hasFocus); 370 if (hasFocus) { 371 // Launch the IME after a bit 372 mHandler.postDelayed(mShowInputMethodTask, 0); 373 } 374 } 375 376 protected String getQuery() { 377 CharSequence q = mQueryTextView.getText(); 378 return q == null ? "" : q.toString(); 379 } 380 381 /** 382 * Sets the text in the query box. Does not update the suggestions, 383 * and does not change the saved user-entered query. 384 * {@link #restoreUserQuery()} will restore the query to the last 385 * user-entered query. 386 */ 387 private void setQuery(String query, boolean selectAll) { 388 mUpdateSuggestions = false; 389 mQueryTextView.setText(query); 390 setTextSelection(selectAll); 391 mUpdateSuggestions = true; 392 } 393 394 /** 395 * Sets the text selection in the query text view. 396 * 397 * @param selectAll If {@code true}, selects the entire query. 398 * If {@false}, no characters are selected, and the cursor is placed 399 * at the end of the query. 400 */ 401 private void setTextSelection(boolean selectAll) { 402 if (selectAll) { 403 mQueryTextView.selectAll(); 404 } else { 405 mQueryTextView.setSelection(mQueryTextView.length()); 406 } 407 } 408 409 protected void updateUi(boolean queryEmpty) { 410 updateQueryTextView(queryEmpty); 411 updateSearchGoButton(queryEmpty); 412 updateVoiceSearchButton(queryEmpty); 413 } 414 415 private void updateQueryTextView(boolean queryEmpty) { 416 if (queryEmpty) { 417 if (mCorpus == null || mCorpus.isWebCorpus()) { 418 mQueryTextView.setBackgroundResource(R.drawable.textfield_search_empty_google); 419 mQueryTextView.setHint(null); 420 } else { 421 mQueryTextView.setBackgroundResource(R.drawable.textfield_search_empty); 422 mQueryTextView.setHint(mCorpus.getHint()); 423 } 424 } else { 425 mQueryTextView.setBackgroundResource(R.drawable.textfield_search); 426 } 427 } 428 429 private void updateSearchGoButton(boolean queryEmpty) { 430 if (queryEmpty) { 431 mSearchGoButton.setVisibility(View.GONE); 432 } else { 433 mSearchGoButton.setVisibility(View.VISIBLE); 434 } 435 } 436 437 protected void updateVoiceSearchButton(boolean queryEmpty) { 438 if (queryEmpty && mVoiceSearch.shouldShowVoiceSearch(mCorpus)) { 439 mVoiceSearchButton.setVisibility(View.VISIBLE); 440 mQueryTextView.setPrivateImeOptions(IME_OPTION_NO_MICROPHONE); 441 } else { 442 mVoiceSearchButton.setVisibility(View.GONE); 443 mQueryTextView.setPrivateImeOptions(null); 444 } 445 } 446 447 protected void showCorpusSelectionDialog() { 448 if (mCorpusSelectionDialog == null) { 449 mCorpusSelectionDialog = new CorpusSelectionDialog(this); 450 mCorpusSelectionDialog.setOwnerActivity(this); 451 mCorpusSelectionDialog.setOnDismissListener(new CorpusSelectorDismissListener()); 452 mCorpusSelectionDialog.setOnCorpusSelectedListener(new CorpusSelectionListener()); 453 } 454 mCorpusSelectionDialog.show(mCorpus); 455 } 456 457 protected boolean isCorpusSelectionDialogShowing() { 458 return mCorpusSelectionDialog != null && mCorpusSelectionDialog.isShowing(); 459 } 460 461 protected void dismissCorpusSelectionDialog() { 462 if (mCorpusSelectionDialog != null) { 463 mCorpusSelectionDialog.dismiss(); 464 } 465 } 466 467 /** 468 * @return true if a search was performed as a result of this click, false otherwise. 469 */ 470 protected boolean onSearchClicked(int method) { 471 String query = ltrim(getQuery()); 472 if (DBG) Log.d(TAG, "Search clicked, query=" + query); 473 474 // Don't do empty queries 475 if (TextUtils.getTrimmedLength(query) == 0) return false; 476 477 Corpus searchCorpus = getSearchCorpus(); 478 if (searchCorpus == null) return false; 479 480 mTookAction = true; 481 482 // Log search start 483 getLogger().logSearch(mCorpus, method, query.length()); 484 485 // Create shortcut 486 SuggestionData searchShortcut = searchCorpus.createSearchShortcut(query); 487 if (searchShortcut != null) { 488 DataSuggestionCursor cursor = new DataSuggestionCursor(query); 489 cursor.add(searchShortcut); 490 getShortcutRepository().reportClick(cursor, 0); 491 } 492 493 // Start search 494 Intent intent = searchCorpus.createSearchIntent(query, mAppSearchData); 495 launchIntent(intent); 496 return true; 497 } 498 499 protected void onVoiceSearchClicked() { 500 if (DBG) Log.d(TAG, "Voice Search clicked"); 501 Corpus searchCorpus = getSearchCorpus(); 502 if (searchCorpus == null) return; 503 504 mTookAction = true; 505 506 // Log voice search start 507 getLogger().logVoiceSearch(searchCorpus); 508 509 // Start voice search 510 Intent intent = searchCorpus.createVoiceSearchIntent(mAppSearchData); 511 launchIntent(intent); 512 } 513 514 /** 515 * Gets the corpus to use for any searches. This is the web corpus in "All" mode, 516 * and the selected corpus otherwise. 517 */ 518 protected Corpus getSearchCorpus() { 519 if (mCorpus != null) { 520 return mCorpus; 521 } else { 522 Corpus webCorpus = getCorpora().getWebCorpus(); 523 if (webCorpus == null) { 524 Log.e(TAG, "No web corpus"); 525 } 526 return webCorpus; 527 } 528 } 529 530 protected SuggestionCursor getCurrentSuggestions() { 531 return mSuggestionsAdapter.getCurrentSuggestions(); 532 } 533 534 protected void launchIntent(Intent intent) { 535 if (intent == null) { 536 return; 537 } 538 try { 539 startActivity(intent); 540 } catch (RuntimeException ex) { 541 // Since the intents for suggestions specified by suggestion providers, 542 // guard against them not being handled, not allowed, etc. 543 Log.e(TAG, "Failed to start " + intent.toUri(0), ex); 544 } 545 } 546 547 protected boolean launchSuggestion(int position) { 548 SuggestionCursor suggestions = getCurrentSuggestions(); 549 if (position < 0 || position >= suggestions.getCount()) { 550 Log.w(TAG, "Tried to launch invalid suggestion " + position); 551 return false; 552 } 553 554 if (DBG) Log.d(TAG, "Launching suggestion " + position); 555 mTookAction = true; 556 557 // Log suggestion click 558 Collection<Corpus> corpora = mSuggestionsAdapter.getSuggestions().getIncludedCorpora(); 559 getLogger().logSuggestionClick(position, suggestions, corpora); 560 561 // Create shortcut 562 getShortcutRepository().reportClick(suggestions, position); 563 564 // Launch intent 565 suggestions.moveTo(position); 566 Intent intent = suggestions.getSuggestionIntent(mAppSearchData); 567 launchIntent(intent); 568 569 return true; 570 } 571 572 protected boolean onSuggestionLongClicked(int position) { 573 if (DBG) Log.d(TAG, "Long clicked on suggestion " + position); 574 return false; 575 } 576 577 protected boolean onSuggestionKeyDown(int position, int keyCode, KeyEvent event) { 578 // Treat enter or search as a click 579 if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_SEARCH) { 580 return launchSuggestion(position); 581 } 582 583 return false; 584 } 585 586 protected int getSelectedPosition() { 587 return mSuggestionsView.getSelectedPosition(); 588 } 589 590 /** 591 * Hides the input method. 592 */ 593 protected void hideInputMethod() { 594 InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); 595 if (imm != null) { 596 imm.hideSoftInputFromWindow(mQueryTextView.getWindowToken(), 0); 597 } 598 } 599 600 protected void showInputMethodForQuery() { 601 InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); 602 if (imm != null) { 603 imm.showSoftInput(mQueryTextView, 0); 604 } 605 } 606 607 protected void onSuggestionListFocusChange(boolean focused) { 608 } 609 610 protected void onQueryTextViewFocusChange(boolean focused) { 611 } 612 613 /** 614 * Hides the input method when the suggestions get focus. 615 */ 616 private class SuggestListFocusListener implements OnFocusChangeListener { 617 public void onFocusChange(View v, boolean focused) { 618 if (DBG) Log.d(TAG, "Suggestions focus change, now: " + focused); 619 if (focused) { 620 // The suggestions list got focus, hide the input method 621 hideInputMethod(); 622 } 623 onSuggestionListFocusChange(focused); 624 } 625 } 626 627 private class QueryTextViewFocusListener implements OnFocusChangeListener { 628 public void onFocusChange(View v, boolean focused) { 629 if (DBG) Log.d(TAG, "Query focus change, now: " + focused); 630 if (focused) { 631 // The query box got focus, show the input method 632 showInputMethodForQuery(); 633 } 634 onQueryTextViewFocusChange(focused); 635 } 636 } 637 638 private int getMaxSuggestions() { 639 Config config = getConfig(); 640 return mCorpus == null 641 ? config.getMaxPromotedSuggestions() 642 : config.getMaxResultsPerSource(); 643 } 644 645 private void updateSuggestionsBuffered() { 646 mHandler.removeCallbacks(mUpdateSuggestionsTask); 647 long delay = getConfig().getTypingUpdateSuggestionsDelayMillis(); 648 mHandler.postDelayed(mUpdateSuggestionsTask, delay); 649 } 650 651 protected void updateSuggestions(String query) { 652 653 query = ltrim(query); 654 Suggestions suggestions = getSuggestionsProvider().getSuggestions( 655 query, mCorpus, getMaxSuggestions()); 656 657 // Log start latency if this is the first suggestions update 658 if (mStarting) { 659 mStarting = false; 660 String source = getIntent().getStringExtra(Search.SOURCE); 661 int latency = mStartLatencyTracker.getLatency(); 662 getLogger().logStart(latency, source, mCorpus, suggestions.getExpectedCorpora()); 663 } 664 665 mSuggestionsAdapter.setSuggestions(suggestions); 666 } 667 668 private boolean forwardKeyToQueryTextView(int keyCode, KeyEvent event) { 669 if (!event.isSystem() && !isDpadKey(keyCode)) { 670 if (DBG) Log.d(TAG, "Forwarding key to query box: " + event); 671 if (mQueryTextView.requestFocus()) { 672 return mQueryTextView.dispatchKeyEvent(event); 673 } 674 } 675 return false; 676 } 677 678 private boolean isDpadKey(int keyCode) { 679 switch (keyCode) { 680 case KeyEvent.KEYCODE_DPAD_UP: 681 case KeyEvent.KEYCODE_DPAD_DOWN: 682 case KeyEvent.KEYCODE_DPAD_LEFT: 683 case KeyEvent.KEYCODE_DPAD_RIGHT: 684 case KeyEvent.KEYCODE_DPAD_CENTER: 685 return true; 686 default: 687 return false; 688 } 689 } 690 691 /** 692 * Filters the suggestions list when the search text changes. 693 */ 694 private class SearchTextWatcher implements TextWatcher { 695 public void afterTextChanged(Editable s) { 696 boolean empty = s.length() == 0; 697 if (empty != mQueryWasEmpty) { 698 mQueryWasEmpty = empty; 699 updateUi(empty); 700 } 701 if (mUpdateSuggestions) { 702 updateSuggestionsBuffered(); 703 } 704 } 705 706 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 707 } 708 709 public void onTextChanged(CharSequence s, int start, int before, int count) { 710 } 711 } 712 713 /** 714 * Handles non-text keys in the query text view. 715 */ 716 private class QueryTextViewKeyListener implements View.OnKeyListener { 717 public boolean onKey(View view, int keyCode, KeyEvent event) { 718 // Handle IME search action key 719 if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_UP) { 720 // if no action was taken, consume the key event so that the keyboard 721 // remains on screen. 722 return !onSearchClicked(Logger.SEARCH_METHOD_KEYBOARD); 723 } 724 return false; 725 } 726 } 727 728 /** 729 * Handles key events on the search and voice search buttons, 730 * by refocusing to EditText. 731 */ 732 private class ButtonsKeyListener implements View.OnKeyListener { 733 public boolean onKey(View v, int keyCode, KeyEvent event) { 734 return forwardKeyToQueryTextView(keyCode, event); 735 } 736 } 737 738 /** 739 * Handles key events on the suggestions list view. 740 */ 741 private class SuggestionsViewKeyListener implements View.OnKeyListener { 742 public boolean onKey(View v, int keyCode, KeyEvent event) { 743 if (event.getAction() == KeyEvent.ACTION_DOWN) { 744 int position = getSelectedPosition(); 745 if (onSuggestionKeyDown(position, keyCode, event)) { 746 return true; 747 } 748 } 749 return forwardKeyToQueryTextView(keyCode, event); 750 } 751 } 752 753 private class InputMethodCloser implements SuggestionsView.OnScrollListener { 754 755 public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, 756 int totalItemCount) { 757 } 758 759 public void onScrollStateChanged(AbsListView view, int scrollState) { 760 hideInputMethod(); 761 } 762 } 763 764 private class ClickHandler implements SuggestionClickListener { 765 public void onSuggestionClicked(int position) { 766 launchSuggestion(position); 767 } 768 769 public boolean onSuggestionLongClicked(int position) { 770 return SearchActivity.this.onSuggestionLongClicked(position); 771 } 772 773 public void onSuggestionQueryRefineClicked(int position) { 774 if (DBG) Log.d(TAG, "query refine clicked, pos " + position); 775 SuggestionCursor suggestions = getCurrentSuggestions(); 776 if (suggestions != null) { 777 suggestions.moveTo(position); 778 String query = suggestions.getSuggestionQuery(); 779 if (!TextUtils.isEmpty(query)) { 780 query += " "; 781 setQuery(query, false); 782 updateSuggestions(query); 783 } 784 } 785 } 786 } 787 788 /** 789 * Listens for clicks on the source selector. 790 */ 791 private class SearchGoButtonClickListener implements View.OnClickListener { 792 public void onClick(View view) { 793 onSearchClicked(Logger.SEARCH_METHOD_BUTTON); 794 } 795 } 796 797 /** 798 * Listens for clicks on the search button. 799 */ 800 private class CorpusIndicatorClickListener implements View.OnClickListener { 801 public void onClick(View view) { 802 showCorpusSelectionDialog(); 803 } 804 } 805 806 private class CorpusSelectorDismissListener implements DialogInterface.OnDismissListener { 807 public void onDismiss(DialogInterface dialog) { 808 if (DBG) Log.d(TAG, "Corpus selector dismissed"); 809 clearStartedIntoCorpusSelectionDialog(); 810 } 811 } 812 813 private class CorpusSelectionListener 814 implements CorpusSelectionDialog.OnCorpusSelectedListener { 815 public void onCorpusSelected(String corpusName) { 816 setCorpus(corpusName); 817 updateSuggestions(getQuery()); 818 mQueryTextView.requestFocus(); 819 showInputMethodForQuery(); 820 } 821 } 822 823 /** 824 * Listens for clicks on the voice search button. 825 */ 826 private class VoiceSearchButtonClickListener implements View.OnClickListener { 827 public void onClick(View view) { 828 onVoiceSearchClicked(); 829 } 830 } 831 832 private class CorporaObserver extends DataSetObserver { 833 @Override 834 public void onChanged() { 835 setCorpus(getCorpusName()); 836 updateSuggestions(getQuery()); 837 } 838 } 839 840 private static String ltrim(String text) { 841 int start = 0; 842 int length = text.length(); 843 while (start < length && Character.isWhitespace(text.charAt(start))) { 844 start++; 845 } 846 return start > 0 ? text.substring(start, length) : text; 847 } 848 849} 850