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