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