SearchActivity.java revision 6c8a8cb66ccf57347c7aed4260132d342b242b0f
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.ArrayList; 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 updateVoiceSearchButton(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 CorpusRanker getCorpusRanker() { 262 return getQsbApplication().getCorpusRanker(); 263 } 264 265 private ShortcutRepository getShortcutRepository() { 266 return getQsbApplication().getShortcutRepository(); 267 } 268 269 private SuggestionsProvider getSuggestionsProvider() { 270 return getQsbApplication().getSuggestionsProvider(mCorpus); 271 } 272 273 private CorpusViewFactory getCorpusViewFactory() { 274 return getQsbApplication().getCorpusViewFactory(); 275 } 276 277 private Logger getLogger() { 278 return getQsbApplication().getLogger(); 279 } 280 281 @Override 282 protected void onDestroy() { 283 if (DBG) Log.d(TAG, "onDestroy()"); 284 super.onDestroy(); 285 mSuggestionsFooter.setAdapter(null); 286 mSuggestionsView.setAdapter(null); // closes mSuggestionsAdapter 287 } 288 289 @Override 290 protected void onStop() { 291 if (DBG) Log.d(TAG, "onStop()"); 292 // Close all open suggestion cursors. The query will be redone in onResume() 293 // if we come back to this activity. 294 mSuggestionsAdapter.setSuggestions(null); 295 getQsbApplication().getShortcutRefresher().reset(); 296 super.onStop(); 297 } 298 299 @Override 300 protected void onResume() { 301 if (DBG) Log.d(TAG, "onResume()"); 302 super.onResume(); 303 setQuery(mUserQuery, mSelectAll); 304 // Only select everything the first time after creating the activity. 305 mSelectAll = false; 306 updateSuggestions(mUserQuery); 307 mQueryTextView.requestFocus(); 308 if (mStarting) { 309 mStarting = false; 310 // Start up latency should not exceed 2^31 ms (~ 25 days). Note that 311 // SystemClock.uptimeMillis() does not advance during deep sleep. 312 int latency = (int) (SystemClock.uptimeMillis() - mStartTime); 313 String source = getIntent().getStringExtra(Search.SOURCE); 314 getLogger().logStart(latency, source, mCorpus, 315 getSuggestionsProvider().getOrderedCorpora()); 316 } 317 } 318 319 @Override 320 public boolean onCreateOptionsMenu(Menu menu) { 321 super.onCreateOptionsMenu(menu); 322 SearchSettings.addSearchSettingsMenuItem(this, menu); 323 return true; 324 } 325 326 /** 327 * Sets the query as typed by the user. Does not update the suggestions 328 * or the text in the query box. 329 */ 330 protected void setUserQuery(String userQuery) { 331 if (userQuery == null) userQuery = ""; 332 mUserQuery = userQuery; 333 } 334 335 protected String getQuery() { 336 CharSequence q = mQueryTextView.getText(); 337 return q == null ? "" : q.toString(); 338 } 339 340 /** 341 * Restores the query entered by the user. 342 */ 343 private void restoreUserQuery() { 344 if (DBG) Log.d(TAG, "Restoring query to '" + mUserQuery + "'"); 345 setQuery(mUserQuery, false); 346 } 347 348 /** 349 * Sets the text in the query box. Does not update the suggestions, 350 * and does not change the saved user-entered query. 351 * {@link #restoreUserQuery()} will restore the query to the last 352 * user-entered query. 353 */ 354 private void setQuery(String query, boolean selectAll) { 355 mUpdateSuggestions = false; 356 mQueryTextView.setText(query); 357 setTextSelection(selectAll); 358 mUpdateSuggestions = true; 359 } 360 361 /** 362 * Sets the text selection in the query text view. 363 * 364 * @param selectAll If {@code true}, selects the entire query. 365 * If {@false}, no characters are selected, and the cursor is placed 366 * at the end of the query. 367 */ 368 private void setTextSelection(boolean selectAll) { 369 if (selectAll) { 370 mQueryTextView.setSelection(0, mQueryTextView.length()); 371 } else { 372 mQueryTextView.setSelection(mQueryTextView.length()); 373 } 374 } 375 376 private void updateQueryTextView(boolean queryEmpty) { 377 if (queryEmpty) { 378 mQueryTextView.setBackgroundResource(R.drawable.textfield_search_empty); 379 mQueryTextView.setCompoundDrawablesWithIntrinsicBounds( 380 R.drawable.placeholder_google, 0, 0, 0); 381 mQueryTextView.setCursorVisible(false); 382 } else { 383 mQueryTextView.setBackgroundResource(R.drawable.textfield_search); 384 mQueryTextView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); 385 mQueryTextView.setCursorVisible(true); 386 } 387 } 388 389 private void updateSearchGoButton(boolean queryEmpty) { 390 if (queryEmpty) { 391 mSearchGoButton.setVisibility(View.GONE); 392 } else { 393 mSearchGoButton.setVisibility(View.VISIBLE); 394 } 395 } 396 397 protected void updateVoiceSearchButton(boolean queryEmpty) { 398 if (queryEmpty && mLauncher.shouldShowVoiceSearch(mCorpus)) { 399 mVoiceSearchButton.setVisibility(View.VISIBLE); 400 mQueryTextView.setPrivateImeOptions(IME_OPTION_NO_MICROPHONE); 401 } else { 402 mVoiceSearchButton.setVisibility(View.GONE); 403 mQueryTextView.setPrivateImeOptions(null); 404 } 405 } 406 407 protected void showCorpusSelectionDialog() { 408 showDialog(CORPUS_SELECTION_DIALOG); 409 } 410 411 protected void dismissCorpusSelectionDialog() { 412 dismissDialog(CORPUS_SELECTION_DIALOG); 413 } 414 415 @Override 416 protected Dialog onCreateDialog(int id, Bundle args) { 417 switch (id) { 418 case CORPUS_SELECTION_DIALOG: 419 return createCorpusSelectionDialog(); 420 default: 421 throw new IllegalArgumentException("Unknown dialog: " + id); 422 } 423 } 424 425 @Override 426 protected void onPrepareDialog(int id, Dialog dialog, Bundle args) { 427 super.onPrepareDialog(id, dialog, args); 428 switch (id) { 429 case CORPUS_SELECTION_DIALOG: 430 prepareCorpusSelectionDialog((CorpusSelectionDialog) dialog); 431 break; 432 default: 433 throw new IllegalArgumentException("Unknown dialog: " + id); 434 } 435 } 436 437 protected CorpusSelectionDialog createCorpusSelectionDialog() { 438 return new CorpusSelectionDialog(this); 439 } 440 441 protected void prepareCorpusSelectionDialog(CorpusSelectionDialog dialog) { 442 dialog.setCorpus(mCorpus); 443 dialog.setQuery(getQuery()); 444 dialog.setAppData(mAppSearchData); 445 } 446 447 protected void onSearchClicked(int method) { 448 String query = getQuery(); 449 if (DBG) Log.d(TAG, "Search clicked, query=" + query); 450 Corpus searchCorpus = getSearchCorpus(); 451 if (searchCorpus == null) return; 452 453 mTookAction = true; 454 455 // Log search start 456 getLogger().logSearch(mCorpus, method, query.length()); 457 458 // Create shortcut 459 SuggestionData searchShortcut = searchCorpus.createSearchShortcut(query); 460 if (searchShortcut != null) { 461 DataSuggestionCursor cursor = new DataSuggestionCursor(query); 462 cursor.add(searchShortcut); 463 getShortcutRepository().reportClick(cursor, 0); 464 } 465 466 // Start search 467 Intent intent = searchCorpus.createSearchIntent(query, mAppSearchData); 468 mLauncher.launchIntent(intent); 469 } 470 471 protected void onVoiceSearchClicked() { 472 if (DBG) Log.d(TAG, "Voice Search clicked"); 473 mTookAction = true; 474 Corpus searchCorpus = getSearchCorpus(); 475 if (searchCorpus == null) return; 476 477 // Log voice search start 478 getLogger().logVoiceSearch(searchCorpus); 479 480 // Start voice search 481 Intent intent = searchCorpus.createVoiceSearchIntent(mAppSearchData); 482 mLauncher.launchIntent(intent); 483 } 484 485 /** 486 * Gets the corpus to use for any searches. This is the web corpus in "All" mode, 487 * and the selected corpus otherwise. 488 */ 489 protected Corpus getSearchCorpus() { 490 if (mCorpus != null) { 491 return mCorpus; 492 } else { 493 Corpus corpus = getCorpora().getWebCorpus(); 494 if (corpus == null) { 495 Log.e(TAG, "No web corpus"); 496 } 497 return corpus; 498 } 499 } 500 501 protected SuggestionCursor getSuggestions() { 502 return mSuggestionsAdapter.getCurrentSuggestions(); 503 } 504 505 protected boolean launchSuggestion(int position) { 506 if (DBG) Log.d(TAG, "Launching suggestion " + position); 507 mTookAction = true; 508 SuggestionCursor suggestions = getSuggestions(); 509 510 // Log suggestion click 511 // TODO: This should be just the queried sources, but currently 512 // all sources are queried 513 ArrayList<Corpus> corpora = getCorpusRanker().rankCorpora(getCorpora().getEnabledCorpora()); 514 getLogger().logSuggestionClick(position, suggestions, corpora); 515 516 // Create shortcut 517 getShortcutRepository().reportClick(suggestions, position); 518 519 // Launch intent 520 Intent intent = mLauncher.getSuggestionIntent(suggestions, position, mAppSearchData); 521 mLauncher.launchIntent(intent); 522 523 return true; 524 } 525 526 protected boolean onSuggestionLongClicked(int position) { 527 if (DBG) Log.d(TAG, "Long clicked on suggestion " + position); 528 return false; 529 } 530 531 protected boolean onSuggestionKeyDown(int position, int keyCode, KeyEvent event) { 532 // Treat enter or search as a click 533 if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_SEARCH) { 534 return launchSuggestion(position); 535 } 536 537 if (keyCode == KeyEvent.KEYCODE_DPAD_UP 538 && mSuggestionsView.getSelectedItemPosition() == 0) { 539 // Moved up from the top suggestion, restore the user query and focus query box 540 if (DBG) Log.d(TAG, "Up and out"); 541 restoreUserQuery(); 542 return false; // let the framework handle the move 543 } 544 545 if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT 546 || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { 547 // Moved left / right from a suggestion, keep current query, move 548 // focus to query box, and move cursor to far left / right 549 if (DBG) Log.d(TAG, "Left/right on a suggestion"); 550 int cursorPos = (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) ? 0 : mQueryTextView.length(); 551 mQueryTextView.setSelection(cursorPos); 552 mQueryTextView.requestFocus(); 553 // TODO: should we modify the list selection? 554 return true; 555 } 556 557 return false; 558 } 559 560 protected void onSourceSelected() { 561 if (DBG) Log.d(TAG, "No suggestion selected"); 562 restoreUserQuery(); 563 } 564 565 protected int getSelectedPosition() { 566 return mSuggestionsView.getSelectedPosition(); 567 } 568 569 /** 570 * Hides the input method. 571 */ 572 protected void hideInputMethod() { 573 InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); 574 if (imm != null) { 575 imm.hideSoftInputFromWindow(mQueryTextView.getWindowToken(), 0); 576 } 577 } 578 579 protected void showInputMethodForQuery() { 580 InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); 581 if (imm != null) { 582 imm.showSoftInput(mQueryTextView, InputMethodManager.SHOW_IMPLICIT); 583 } 584 } 585 586 /** 587 * Hides the input method when the suggestions get focus. 588 */ 589 private class SuggestListFocusListener implements OnFocusChangeListener { 590 public void onFocusChange(View v, boolean focused) { 591 if (DBG) Log.d(TAG, "Suggestions focus change, now: " + focused); 592 if (focused) { 593 // The suggestions list got focus, hide the input method 594 hideInputMethod(); 595 } 596 } 597 } 598 599 private class QueryTextViewFocusListener implements OnFocusChangeListener { 600 public void onFocusChange(View v, boolean focused) { 601 if (DBG) Log.d(TAG, "Query focus change, now: " + focused); 602 if (focused) { 603 // The query box got focus, show the input method if the 604 // query box got focus? 605 showInputMethodForQuery(); 606 } 607 } 608 } 609 610 private void startSearchProgress() { 611 // TODO: Do we need a progress indicator? 612 } 613 614 private void stopSearchProgress() { 615 Drawable animation = mSearchGoButton.getDrawable(); 616 if (animation instanceof Animatable) { 617 // TODO: Is this needed, or is it done automatically when the 618 // animation is removed? 619 ((Animatable) animation).stop(); 620 } 621 mSearchGoButton.setImageResource(R.drawable.ic_btn_search); 622 } 623 624 private void updateSuggestions(String query) { 625 query = ltrim(query); 626 LatencyTracker latency = new LatencyTracker(TAG); 627 Suggestions suggestions = getSuggestionsProvider().getSuggestions(query); 628 latency.addEvent("getSuggestions_done"); 629 if (!suggestions.isDone()) { 630 suggestions.registerDataSetObserver(new ProgressUpdater(suggestions)); 631 startSearchProgress(); 632 } else { 633 stopSearchProgress(); 634 } 635 mSuggestionsAdapter.setSuggestions(suggestions); 636 latency.addEvent("shortcuts_shown"); 637 long userVisibleLatency = latency.getUserVisibleLatency(); 638 if (DBG) { 639 Log.d(TAG, "User visible latency (shortcuts): " + userVisibleLatency + " ms."); 640 } 641 } 642 643 private boolean forwardKeyToQueryTextView(int keyCode, KeyEvent event) { 644 if (!event.isSystem() && !isDpadKey(keyCode)) { 645 if (DBG) Log.d(TAG, "Forwarding key to query box: " + event); 646 if (mQueryTextView.requestFocus()) { 647 return mQueryTextView.dispatchKeyEvent(event); 648 } 649 } 650 return false; 651 } 652 653 private boolean isDpadKey(int keyCode) { 654 switch (keyCode) { 655 case KeyEvent.KEYCODE_DPAD_UP: 656 case KeyEvent.KEYCODE_DPAD_DOWN: 657 case KeyEvent.KEYCODE_DPAD_LEFT: 658 case KeyEvent.KEYCODE_DPAD_RIGHT: 659 case KeyEvent.KEYCODE_DPAD_CENTER: 660 return true; 661 default: 662 return false; 663 } 664 } 665 666 /** 667 * Filters the suggestions list when the search text changes. 668 */ 669 private class SearchTextWatcher implements TextWatcher { 670 public void afterTextChanged(Editable s) { 671 boolean empty = s.length() == 0; 672 if (empty != mQueryWasEmpty) { 673 mQueryWasEmpty = empty; 674 updateQueryTextView(empty); 675 updateSearchGoButton(empty); 676 updateVoiceSearchButton(empty); 677 } 678 if (mUpdateSuggestions) { 679 String query = s == null ? "" : s.toString(); 680 setUserQuery(query); 681 updateSuggestions(query); 682 } 683 } 684 685 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 686 } 687 688 public void onTextChanged(CharSequence s, int start, int before, int count) { 689 } 690 } 691 692 /** 693 * Handles non-text keys in the query text view. 694 */ 695 private class QueryTextViewKeyListener implements View.OnKeyListener { 696 public boolean onKey(View view, int keyCode, KeyEvent event) { 697 // Handle IME search action key 698 if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_UP) { 699 onSearchClicked(Logger.SEARCH_METHOD_KEYBOARD); 700 } 701 return false; 702 } 703 } 704 705 /** 706 * Handles key events on the search and voice search buttons, 707 * by refocusing to EditText. 708 */ 709 private class ButtonsKeyListener implements View.OnKeyListener { 710 public boolean onKey(View v, int keyCode, KeyEvent event) { 711 return forwardKeyToQueryTextView(keyCode, event); 712 } 713 } 714 715 /** 716 * Handles key events on the suggestions list view. 717 */ 718 private class SuggestionsViewKeyListener implements View.OnKeyListener { 719 public boolean onKey(View v, int keyCode, KeyEvent event) { 720 if (event.getAction() == KeyEvent.ACTION_DOWN) { 721 int position = getSelectedPosition(); 722 if (onSuggestionKeyDown(position, keyCode, event)) { 723 return true; 724 } 725 } 726 return forwardKeyToQueryTextView(keyCode, event); 727 } 728 } 729 730 private class InputMethodCloser implements SuggestionsView.InteractionListener { 731 public void onInteraction() { 732 hideInputMethod(); 733 } 734 } 735 736 private class ClickHandler implements SuggestionClickListener { 737 public void onSuggestionClicked(int position) { 738 launchSuggestion(position); 739 } 740 741 public boolean onSuggestionLongClicked(int position) { 742 return SearchActivity.this.onSuggestionLongClicked(position); 743 } 744 } 745 746 private class SelectionHandler implements SuggestionSelectionListener { 747 public void onSuggestionSelected(int position) { 748 SuggestionCursor suggestions = getSuggestions(); 749 suggestions.moveTo(position); 750 String displayQuery = suggestions.getSuggestionDisplayQuery(); 751 if (TextUtils.isEmpty(displayQuery)) { 752 restoreUserQuery(); 753 } else { 754 setQuery(displayQuery, false); 755 } 756 } 757 758 public void onNothingSelected() { 759 // This happens when a suggestion has been selected with the 760 // dpad / trackball and then a different UI element is touched. 761 // Do nothing, since we want to keep the query of the selection 762 // in the search box. 763 } 764 } 765 766 /** 767 * Listens for clicks on the source selector. 768 */ 769 private class SearchGoButtonClickListener implements View.OnClickListener { 770 public void onClick(View view) { 771 onSearchClicked(Logger.SEARCH_METHOD_BUTTON); 772 } 773 } 774 775 /** 776 * Listens for clicks on the search button. 777 */ 778 private class CorpusIndicatorClickListener implements View.OnClickListener { 779 public void onClick(View view) { 780 showCorpusSelectionDialog(); 781 } 782 } 783 784 /** 785 * Listens for clicks on the voice search button. 786 */ 787 private class VoiceSearchButtonClickListener implements View.OnClickListener { 788 public void onClick(View view) { 789 onVoiceSearchClicked(); 790 } 791 } 792 793 /** 794 * Updates the progress bar when the suggestions adapter changes its progress. 795 */ 796 private class ProgressUpdater extends DataSetObserver { 797 private final Suggestions mSuggestions; 798 799 public ProgressUpdater(Suggestions suggestions) { 800 mSuggestions = suggestions; 801 } 802 803 @Override 804 public void onChanged() { 805 if (mSuggestions.isDone()) { 806 stopSearchProgress(); 807 } 808 } 809 } 810 811 private static String ltrim(String text) { 812 int start = 0; 813 int length = text.length(); 814 while (start < length && Character.isWhitespace(text.charAt(start))) { 815 start++; 816 } 817 return start > 0 ? text.substring(start, length) : text; 818 } 819 820} 821