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