SearchActivity.java revision dbdffd8316e75bc2813dbbcbef13d357970e8c84
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 setAppSearchData(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 323 Intent settings = new Intent(SearchManager.INTENT_ACTION_SEARCH_SETTINGS); 324 settings.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); 325 // Don't show activity chooser if there are multiple search settings activities, 326 // e.g. from different QSB implementations. 327 settings.setPackage(this.getPackageName()); 328 menu.add(Menu.NONE, Menu.NONE, 0, R.string.menu_settings) 329 .setIcon(android.R.drawable.ic_menu_preferences).setAlphabeticShortcut('P') 330 .setIntent(settings); 331 332 return true; 333 } 334 335 /** 336 * Sets the query as typed by the user. Does not update the suggestions 337 * or the text in the query box. 338 */ 339 protected void setUserQuery(String userQuery) { 340 if (userQuery == null) userQuery = ""; 341 mUserQuery = userQuery; 342 } 343 344 protected void setAppSearchData(Bundle appSearchData) { 345 mAppSearchData = appSearchData; 346 mLauncher.setAppSearchData(appSearchData); 347 } 348 349 protected String getQuery() { 350 CharSequence q = mQueryTextView.getText(); 351 return q == null ? "" : q.toString(); 352 } 353 354 /** 355 * Restores the query entered by the user. 356 */ 357 private void restoreUserQuery() { 358 if (DBG) Log.d(TAG, "Restoring query to '" + mUserQuery + "'"); 359 setQuery(mUserQuery, false); 360 } 361 362 /** 363 * Sets the text in the query box. Does not update the suggestions, 364 * and does not change the saved user-entered query. 365 * {@link #restoreUserQuery()} will restore the query to the last 366 * user-entered query. 367 */ 368 private void setQuery(String query, boolean selectAll) { 369 mUpdateSuggestions = false; 370 mQueryTextView.setText(query); 371 setTextSelection(selectAll); 372 mUpdateSuggestions = true; 373 } 374 375 /** 376 * Sets the text selection in the query text view. 377 * 378 * @param selectAll If {@code true}, selects the entire query. 379 * If {@false}, no characters are selected, and the cursor is placed 380 * at the end of the query. 381 */ 382 private void setTextSelection(boolean selectAll) { 383 if (selectAll) { 384 mQueryTextView.setSelection(0, mQueryTextView.length()); 385 } else { 386 mQueryTextView.setSelection(mQueryTextView.length()); 387 } 388 } 389 390 private void updateQueryTextView(boolean queryEmpty) { 391 if (queryEmpty) { 392 mQueryTextView.setBackgroundResource(R.drawable.textfield_search_empty); 393 mQueryTextView.setCompoundDrawablesWithIntrinsicBounds( 394 R.drawable.placeholder_google, 0, 0, 0); 395 mQueryTextView.setCursorVisible(false); 396 } else { 397 mQueryTextView.setBackgroundResource(R.drawable.textfield_search); 398 mQueryTextView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); 399 mQueryTextView.setCursorVisible(true); 400 } 401 } 402 403 private void updateSearchGoButton(boolean queryEmpty) { 404 if (queryEmpty) { 405 mSearchGoButton.setVisibility(View.GONE); 406 } else { 407 mSearchGoButton.setVisibility(View.VISIBLE); 408 } 409 } 410 411 protected void updateVoiceSearchButton(boolean queryEmpty) { 412 if (queryEmpty && Launcher.shouldShowVoiceSearch(this, mCorpus)) { 413 mVoiceSearchButton.setVisibility(View.VISIBLE); 414 mQueryTextView.setPrivateImeOptions(IME_OPTION_NO_MICROPHONE); 415 } else { 416 mVoiceSearchButton.setVisibility(View.GONE); 417 mQueryTextView.setPrivateImeOptions(null); 418 } 419 } 420 421 protected void showCorpusSelectionDialog() { 422 showDialog(CORPUS_SELECTION_DIALOG); 423 } 424 425 protected void dismissCorpusSelectionDialog() { 426 dismissDialog(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 switch (id) { 442 case CORPUS_SELECTION_DIALOG: 443 prepareCorpusSelectionDialog((CorpusSelectionDialog) dialog); 444 break; 445 default: 446 throw new IllegalArgumentException("Unknown dialog: " + id); 447 } 448 } 449 450 protected CorpusSelectionDialog createCorpusSelectionDialog() { 451 CorpusSelectionDialog dialog = new CorpusSelectionDialog(this); 452 dialog.setOwnerActivity(this); 453 return dialog; 454 } 455 456 protected void prepareCorpusSelectionDialog(CorpusSelectionDialog dialog) { 457 dialog.setCorpus(mCorpus); 458 dialog.setQuery(getQuery()); 459 dialog.setAppData(mAppSearchData); 460 } 461 462 protected void onSearchClicked(int method) { 463 String query = getQuery(); 464 if (DBG) Log.d(TAG, "Search clicked, query=" + query); 465 mTookAction = true; 466 getLogger().logSearch(mCorpus, method, query.length()); 467 mLauncher.startSearch(mCorpus, query); 468 } 469 470 protected void onVoiceSearchClicked() { 471 if (DBG) Log.d(TAG, "Voice Search clicked"); 472 mTookAction = true; 473 getLogger().logVoiceSearch(mCorpus); 474 475 // TODO: should this start voice search in the current source? 476 mLauncher.startVoiceSearch(mCorpus); 477 } 478 479 protected SuggestionCursor getSuggestions() { 480 return mSuggestionsAdapter.getCurrentSuggestions(); 481 } 482 483 protected boolean launchSuggestion(int position) { 484 if (DBG) Log.d(TAG, "Launching suggestion " + position); 485 mTookAction = true; 486 SuggestionCursor suggestions = getSuggestions(); 487 // TODO: This should be just the queried sources, but currently 488 // all sources are queried 489 ArrayList<Corpus> corpora = getCorpusRanker().rankCorpora(getCorpora().getEnabledCorpora()); 490 getLogger().logSuggestionClick(position, suggestions, corpora); 491 492 mLauncher.launchSuggestion(suggestions, position); 493 getShortcutRepository().reportClick(suggestions, position); 494 return true; 495 } 496 497 protected boolean onSuggestionLongClicked(int position) { 498 if (DBG) Log.d(TAG, "Long clicked on suggestion " + position); 499 return false; 500 } 501 502 protected boolean onSuggestionKeyDown(int position, int keyCode, KeyEvent event) { 503 // Treat enter or search as a click 504 if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_SEARCH) { 505 return launchSuggestion(position); 506 } 507 508 if (keyCode == KeyEvent.KEYCODE_DPAD_UP 509 && mSuggestionsView.getSelectedItemPosition() == 0) { 510 // Moved up from the top suggestion, restore the user query and focus query box 511 if (DBG) Log.d(TAG, "Up and out"); 512 restoreUserQuery(); 513 return false; // let the framework handle the move 514 } 515 516 if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT 517 || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { 518 // Moved left / right from a suggestion, keep current query, move 519 // focus to query box, and move cursor to far left / right 520 if (DBG) Log.d(TAG, "Left/right on a suggestion"); 521 int cursorPos = (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) ? 0 : mQueryTextView.length(); 522 mQueryTextView.setSelection(cursorPos); 523 mQueryTextView.requestFocus(); 524 // TODO: should we modify the list selection? 525 return true; 526 } 527 528 return false; 529 } 530 531 protected void onSourceSelected() { 532 if (DBG) Log.d(TAG, "No suggestion selected"); 533 restoreUserQuery(); 534 } 535 536 protected int getSelectedPosition() { 537 return mSuggestionsView.getSelectedPosition(); 538 } 539 540 /** 541 * Hides the input method. 542 */ 543 protected void hideInputMethod() { 544 InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); 545 if (imm != null) { 546 imm.hideSoftInputFromWindow(mQueryTextView.getWindowToken(), 0); 547 } 548 } 549 550 protected void showInputMethodForQuery() { 551 InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); 552 if (imm != null) { 553 imm.showSoftInput(mQueryTextView, 0); 554 } 555 } 556 557 /** 558 * Hides the input method when the suggestions get focus. 559 */ 560 private class SuggestListFocusListener implements OnFocusChangeListener { 561 public void onFocusChange(View v, boolean focused) { 562 if (DBG) Log.d(TAG, "Suggestions focus change, now: " + focused); 563 if (focused) { 564 // The suggestions list got focus, hide the input method 565 hideInputMethod(); 566 } 567 } 568 } 569 570 private class QueryTextViewFocusListener implements OnFocusChangeListener { 571 public void onFocusChange(View v, boolean focused) { 572 if (DBG) Log.d(TAG, "Query focus change, now: " + focused); 573 if (focused) { 574 // The query box got focus, show the input method if the 575 // query box got focus? 576 showInputMethodForQuery(); 577 } 578 } 579 } 580 581 private void startSearchProgress() { 582 // TODO: Do we need a progress indicator? 583 } 584 585 private void stopSearchProgress() { 586 Drawable animation = mSearchGoButton.getDrawable(); 587 if (animation instanceof Animatable) { 588 // TODO: Is this needed, or is it done automatically when the 589 // animation is removed? 590 ((Animatable) animation).stop(); 591 } 592 mSearchGoButton.setImageResource(R.drawable.ic_btn_search); 593 } 594 595 private void updateSuggestions(String query) { 596 query = ltrim(query); 597 LatencyTracker latency = new LatencyTracker(TAG); 598 Suggestions suggestions = getSuggestionsProvider().getSuggestions(query); 599 latency.addEvent("getSuggestions_done"); 600 if (!suggestions.isDone()) { 601 suggestions.registerDataSetObserver(new ProgressUpdater(suggestions)); 602 startSearchProgress(); 603 } else { 604 stopSearchProgress(); 605 } 606 mSuggestionsAdapter.setSuggestions(suggestions); 607 latency.addEvent("shortcuts_shown"); 608 long userVisibleLatency = latency.getUserVisibleLatency(); 609 if (DBG) { 610 Log.d(TAG, "User visible latency (shortcuts): " + userVisibleLatency + " ms."); 611 } 612 } 613 614 private boolean forwardKeyToQueryTextView(int keyCode, KeyEvent event) { 615 if (!event.isSystem() && !isDpadKey(keyCode)) { 616 if (DBG) Log.d(TAG, "Forwarding key to query box: " + event); 617 if (mQueryTextView.requestFocus()) { 618 return mQueryTextView.dispatchKeyEvent(event); 619 } 620 } 621 return false; 622 } 623 624 private boolean isDpadKey(int keyCode) { 625 switch (keyCode) { 626 case KeyEvent.KEYCODE_DPAD_UP: 627 case KeyEvent.KEYCODE_DPAD_DOWN: 628 case KeyEvent.KEYCODE_DPAD_LEFT: 629 case KeyEvent.KEYCODE_DPAD_RIGHT: 630 case KeyEvent.KEYCODE_DPAD_CENTER: 631 return true; 632 default: 633 return false; 634 } 635 } 636 637 /** 638 * Filters the suggestions list when the search text changes. 639 */ 640 private class SearchTextWatcher implements TextWatcher { 641 public void afterTextChanged(Editable s) { 642 boolean empty = s.length() == 0; 643 if (empty != mQueryWasEmpty) { 644 mQueryWasEmpty = empty; 645 updateQueryTextView(empty); 646 updateSearchGoButton(empty); 647 updateVoiceSearchButton(empty); 648 } 649 if (mUpdateSuggestions) { 650 String query = s == null ? "" : s.toString(); 651 setUserQuery(query); 652 updateSuggestions(query); 653 } 654 } 655 656 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 657 } 658 659 public void onTextChanged(CharSequence s, int start, int before, int count) { 660 } 661 } 662 663 /** 664 * Handles non-text keys in the query text view. 665 */ 666 private class QueryTextViewKeyListener implements View.OnKeyListener { 667 public boolean onKey(View view, int keyCode, KeyEvent event) { 668 // Handle IME search action key 669 if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_UP) { 670 onSearchClicked(Logger.SEARCH_METHOD_KEYBOARD); 671 } 672 return false; 673 } 674 } 675 676 /** 677 * Handles key events on the search and voice search buttons, 678 * by refocusing to EditText. 679 */ 680 private class ButtonsKeyListener implements View.OnKeyListener { 681 public boolean onKey(View v, int keyCode, KeyEvent event) { 682 return forwardKeyToQueryTextView(keyCode, event); 683 } 684 } 685 686 /** 687 * Handles key events on the suggestions list view. 688 */ 689 private class SuggestionsViewKeyListener implements View.OnKeyListener { 690 public boolean onKey(View v, int keyCode, KeyEvent event) { 691 if (event.getAction() == KeyEvent.ACTION_DOWN) { 692 int position = getSelectedPosition(); 693 if (onSuggestionKeyDown(position, keyCode, event)) { 694 return true; 695 } 696 } 697 return forwardKeyToQueryTextView(keyCode, event); 698 } 699 } 700 701 private class InputMethodCloser implements SuggestionsView.InteractionListener { 702 public void onInteraction() { 703 hideInputMethod(); 704 } 705 } 706 707 private class ClickHandler implements SuggestionClickListener { 708 public void onSuggestionClicked(int position) { 709 launchSuggestion(position); 710 } 711 712 public boolean onSuggestionLongClicked(int position) { 713 return SearchActivity.this.onSuggestionLongClicked(position); 714 } 715 } 716 717 private class SelectionHandler implements SuggestionSelectionListener { 718 public void onSuggestionSelected(int position) { 719 SuggestionCursor suggestions = getSuggestions(); 720 suggestions.moveTo(position); 721 String displayQuery = suggestions.getSuggestionDisplayQuery(); 722 if (TextUtils.isEmpty(displayQuery)) { 723 restoreUserQuery(); 724 } else { 725 setQuery(displayQuery, false); 726 } 727 } 728 729 public void onNothingSelected() { 730 // This happens when a suggestion has been selected with the 731 // dpad / trackball and then a different UI element is touched. 732 // Do nothing, since we want to keep the query of the selection 733 // in the search box. 734 } 735 } 736 737 /** 738 * Listens for clicks on the source selector. 739 */ 740 private class SearchGoButtonClickListener implements View.OnClickListener { 741 public void onClick(View view) { 742 onSearchClicked(Logger.SEARCH_METHOD_BUTTON); 743 } 744 } 745 746 /** 747 * Listens for clicks on the search button. 748 */ 749 private class CorpusIndicatorClickListener implements View.OnClickListener { 750 public void onClick(View view) { 751 showCorpusSelectionDialog(); 752 } 753 } 754 755 /** 756 * Listens for clicks on the voice search button. 757 */ 758 private class VoiceSearchButtonClickListener implements View.OnClickListener { 759 public void onClick(View view) { 760 onVoiceSearchClicked(); 761 } 762 } 763 764 /** 765 * Updates the progress bar when the suggestions adapter changes its progress. 766 */ 767 private class ProgressUpdater extends DataSetObserver { 768 private final Suggestions mSuggestions; 769 770 public ProgressUpdater(Suggestions suggestions) { 771 mSuggestions = suggestions; 772 } 773 774 @Override 775 public void onChanged() { 776 if (mSuggestions.isDone()) { 777 stopSearchProgress(); 778 } 779 } 780 } 781 782 private static String ltrim(String text) { 783 int start = 0; 784 int length = text.length(); 785 while (start < length && Character.isWhitespace(text.charAt(start))) { 786 start++; 787 } 788 return start > 0 ? text.substring(start, length) : text; 789 } 790 791} 792