SearchActivity.java revision 3d382f0ca49a8150d504cf2629124a7f13175e1e
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.SearchSourceSelector; 21import com.android.quicksearchbox.ui.SuggestionClickListener; 22import com.android.quicksearchbox.ui.SuggestionSelectionListener; 23import com.android.quicksearchbox.ui.SuggestionViewFactory; 24import com.android.quicksearchbox.ui.SuggestionsAdapter; 25import com.android.quicksearchbox.ui.SuggestionsView; 26 27import android.app.Activity; 28import android.app.Dialog; 29import android.app.SearchManager; 30import android.content.ComponentName; 31import android.content.Intent; 32import android.database.DataSetObserver; 33import android.graphics.drawable.Animatable; 34import android.graphics.drawable.Drawable; 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.View.OnFocusChangeListener; 45import android.view.inputmethod.InputMethodManager; 46import android.widget.EditText; 47import android.widget.ImageButton; 48 49import java.util.ArrayList; 50 51/** 52 * The main activity for Quick Search Box. Shows the search UI. 53 * 54 */ 55public class SearchActivity extends Activity { 56 57 private static final boolean DBG = true; 58 private static final String TAG = "QSB.SearchActivity"; 59 60 public static final String INTENT_ACTION_QSB_AND_SELECT_SEARCH_SOURCE 61 = "com.android.quicksearchbox.action.QSB_AND_SELECT_SEARCH_SOURCE"; 62 63 // Keys for the saved instance state. 64 private static final String INSTANCE_KEY_SOURCE = "source"; 65 private static final String INSTANCE_KEY_USER_QUERY = "query"; 66 67 // Dialog IDs 68 private static final int DIALOG_SOURCE_SELECTOR = 0; 69 70 // Timestamp for last onCreate()/onNewIntent() call, as returned by SystemClock.uptimeMillis(). 71 private long mStartTime; 72 // Whether QSB is starting. True between the calls to onCreate()/onNewIntent() and onResume(). 73 private boolean mStarting; 74 // True if the user has taken some action, e.g. launching a search, voice search, 75 // or suggestions, since QSB was last started. 76 private boolean mTookAction; 77 78 protected SuggestionsAdapter mSuggestionsAdapter; 79 80 protected EditText mQueryTextView; 81 82 protected SuggestionsView mSuggestionsView; 83 84 protected ImageButton mSearchGoButton; 85 protected ImageButton mVoiceSearchButton; 86 protected SearchSourceSelector mSourceSelector; 87 88 private Launcher mLauncher; 89 90 private Source mSource; 91 private Bundle mAppSearchData; 92 private boolean mUpdateSuggestions; 93 private String mUserQuery; 94 private boolean mSelectAll; 95 96 /** Called when the activity is first created. */ 97 @Override 98 public void onCreate(Bundle savedInstanceState) { 99 recordStartTime(); 100 if (DBG) Log.d(TAG, "onCreate()"); 101 super.onCreate(savedInstanceState); 102 103 setContentView(R.layout.search_bar); 104 105 mSuggestionsAdapter = getQsbApplication().createSuggestionsAdapter(); 106 107 mQueryTextView = (EditText) findViewById(R.id.search_src_text); 108 mSuggestionsView = (SuggestionsView) findViewById(R.id.suggestions); 109 mSuggestionsView.setSuggestionClickListener(new ClickHandler()); 110 mSuggestionsView.setSuggestionSelectionListener(new SelectionHandler()); 111 mSuggestionsView.setInteractionListener(new InputMethodCloser()); 112 mSuggestionsView.setOnKeyListener(new SuggestionsViewKeyListener()); 113 mSuggestionsView.setOnFocusChangeListener(new SuggestListFocusListener()); 114 115 mSearchGoButton = (ImageButton) findViewById(R.id.search_go_btn); 116 mVoiceSearchButton = (ImageButton) findViewById(R.id.search_voice_btn); 117 mSourceSelector = new SearchSourceSelector(findViewById(R.id.search_source_selector)); 118 119 mLauncher = new Launcher(this); 120 // TODO: should this check for voice search in the current source? 121 mVoiceSearchButton.setVisibility( 122 mLauncher.isVoiceSearchAvailable() ? View.VISIBLE : View.GONE); 123 124 mQueryTextView.addTextChangedListener(new SearchTextWatcher()); 125 mQueryTextView.setOnKeyListener(new QueryTextViewKeyListener()); 126 mQueryTextView.setOnFocusChangeListener(new QueryTextViewFocusListener()); 127 128 mSourceSelector.setOnClickListener(new SourceSelectorClickListener()); 129 130 mSearchGoButton.setOnClickListener(new SearchGoButtonClickListener()); 131 132 mVoiceSearchButton.setOnClickListener(new VoiceSearchButtonClickListener()); 133 134 ButtonsKeyListener buttonsKeyListener = new ButtonsKeyListener(); 135 mSearchGoButton.setOnKeyListener(buttonsKeyListener); 136 mVoiceSearchButton.setOnKeyListener(buttonsKeyListener); 137 mSourceSelector.setOnKeyListener(buttonsKeyListener); 138 139 mUpdateSuggestions = true; 140 141 // First get setup from intent 142 Intent intent = getIntent(); 143 setupFromIntent(intent); 144 // Then restore any saved instance state 145 restoreInstanceState(savedInstanceState); 146 147 // Do this at the end, to avoid updating the list view when setSource() 148 // is called. 149 mSuggestionsView.setAdapter(mSuggestionsAdapter); 150 } 151 152 @Override 153 protected void onNewIntent(Intent intent) { 154 recordStartTime(); 155 setIntent(intent); 156 setupFromIntent(intent); 157 } 158 159 private void recordStartTime() { 160 mStartTime = SystemClock.uptimeMillis(); 161 mStarting = true; 162 mTookAction = false; 163 } 164 165 protected void restoreInstanceState(Bundle savedInstanceState) { 166 if (savedInstanceState == null) return; 167 ComponentName sourceName = savedInstanceState.getParcelable(INSTANCE_KEY_SOURCE); 168 String query = savedInstanceState.getString(INSTANCE_KEY_USER_QUERY); 169 setSource(getSourceByComponentName(sourceName)); 170 setUserQuery(query); 171 } 172 173 @Override 174 protected void onSaveInstanceState(Bundle outState) { 175 super.onSaveInstanceState(outState); 176 // We don't save appSearchData, since we always get the value 177 // from the intent and the user can't change it. 178 outState.putParcelable(INSTANCE_KEY_SOURCE, getSourceName()); 179 outState.putString(INSTANCE_KEY_USER_QUERY, mUserQuery); 180 } 181 182 private void setupFromIntent(Intent intent) { 183 if (DBG) Log.d(TAG, "setupFromIntent(" + intent.toUri(0) + ")"); 184 ComponentName sourceName = SearchSourceSelector.getSource(intent); 185 String query = intent.getStringExtra(SearchManager.QUERY); 186 Bundle appSearchData = intent.getBundleExtra(SearchManager.APP_DATA); 187 188 Source source = getSourceByComponentName(sourceName); 189 setSource(source); 190 setUserQuery(query); 191 mSelectAll = intent.getBooleanExtra(SearchManager.EXTRA_SELECT_QUERY, false); 192 setAppSearchData(appSearchData); 193 194 if (INTENT_ACTION_QSB_AND_SELECT_SEARCH_SOURCE.equals(intent.getAction())) { 195 showSourceSelectorDialog(); 196 } 197 } 198 199 private Source getSourceByComponentName(ComponentName sourceName) { 200 if (sourceName == null) return null; 201 Source source = getSources().getSourceByComponentName(sourceName); 202 if (source == null) { 203 Log.w(TAG, "Unknown source " + sourceName); 204 return null; 205 } 206 return source; 207 } 208 209 private void setSource(Source source) { 210 if (DBG) Log.d(TAG, "setSource(" + source + ")"); 211 mSource = source; 212 Drawable sourceIcon; 213 if (source == null) { 214 sourceIcon = getSuggestionViewFactory().getGlobalSearchIcon(); 215 } else { 216 sourceIcon = source.getSourceIcon(); 217 } 218 ComponentName sourceName = getSourceName(); 219 mSuggestionsAdapter.setSource(sourceName); 220 mSourceSelector.setSourceIcon(sourceIcon); 221 } 222 223 private ComponentName getSourceName() { 224 return mSource == null ? null : mSource.getComponentName(); 225 } 226 227 private QsbApplication getQsbApplication() { 228 return (QsbApplication) getApplication(); 229 } 230 231 private SourceLookup getSources() { 232 return getQsbApplication().getSources(); 233 } 234 235 private ShortcutRepository getShortcutRepository() { 236 return getQsbApplication().getShortcutRepository(); 237 } 238 239 private SuggestionsProvider getSuggestionsProvider() { 240 return getQsbApplication().getSuggestionsProvider(mSource); 241 } 242 243 private SuggestionViewFactory getSuggestionViewFactory() { 244 return getQsbApplication().getSuggestionViewFactory(); 245 } 246 247 private Logger getLogger() { 248 return getQsbApplication().getLogger(); 249 } 250 251 @Override 252 protected void onDestroy() { 253 if (DBG) Log.d(TAG, "onDestroy()"); 254 super.onDestroy(); 255 mSuggestionsView.setAdapter(null); // closes mSuggestionsAdapter 256 } 257 258 @Override 259 protected void onStop() { 260 if (DBG) Log.d(TAG, "onStop()"); 261 // Close all open suggestion cursors. The query will be redone in onResume() 262 // if we come back to this activity. 263 mSuggestionsAdapter.setSuggestions(null); 264 getQsbApplication().getShortcutRefresher().reset(); 265 super.onStop(); 266 } 267 268 @Override 269 protected void onResume() { 270 if (DBG) Log.d(TAG, "onResume()"); 271 super.onResume(); 272 setQuery(mUserQuery, mSelectAll); 273 // Only select everything the first time after creating the activity. 274 mSelectAll = false; 275 updateSuggestions(mUserQuery); 276 mQueryTextView.requestFocus(); 277 if (mStarting) { 278 mStarting = false; 279 // Start up latency should not exceed 2^31 ms (~ 25 days). Note that 280 // SystemClock.uptimeMillis() does not advance during deep sleep. 281 int latency = (int) (SystemClock.uptimeMillis() - mStartTime); 282 String source = getIntent().getStringExtra(Search.SOURCE); 283 getLogger().logStart(latency, source, mSource, 284 getSuggestionsProvider().getOrderedSources()); 285 } 286 } 287 288 @Override 289 public boolean onCreateOptionsMenu(Menu menu) { 290 super.onCreateOptionsMenu(menu); 291 292 Intent settings = new Intent(SearchManager.INTENT_ACTION_SEARCH_SETTINGS); 293 settings.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); 294 // Don't show activity chooser if there are multiple search settings activities, 295 // e.g. from different QSB implementations. 296 settings.setPackage(this.getPackageName()); 297 menu.add(Menu.NONE, Menu.NONE, 0, R.string.menu_settings) 298 .setIcon(android.R.drawable.ic_menu_preferences).setAlphabeticShortcut('P') 299 .setIntent(settings); 300 301 return true; 302 } 303 304 /** 305 * Sets the query as typed by the user. Does not update the suggestions 306 * or the text in the query box. 307 */ 308 protected void setUserQuery(String userQuery) { 309 if (userQuery == null) userQuery = ""; 310 mUserQuery = userQuery; 311 } 312 313 protected void setAppSearchData(Bundle appSearchData) { 314 mAppSearchData = appSearchData; 315 mLauncher.setAppSearchData(appSearchData); 316 } 317 318 protected String getQuery() { 319 CharSequence q = mQueryTextView.getText(); 320 return q == null ? "" : q.toString(); 321 } 322 323 /** 324 * Restores the query entered by the user. 325 */ 326 private void restoreUserQuery() { 327 if (DBG) Log.d(TAG, "Restoring query to '" + mUserQuery + "'"); 328 setQuery(mUserQuery, false); 329 } 330 331 /** 332 * Sets the text in the query box. Does not update the suggestions, 333 * and does not change the saved user-entered query. 334 * {@link #restoreUserQuery()} will restore the query to the last 335 * user-entered query. 336 */ 337 private void setQuery(String query, boolean selectAll) { 338 mUpdateSuggestions = false; 339 mQueryTextView.setText(query); 340 setTextSelection(selectAll); 341 mUpdateSuggestions = true; 342 } 343 344 /** 345 * Sets the text selection in the query text view. 346 * 347 * @param selectAll If {@code true}, selects the entire query. 348 * If {@false}, no characters are selected, and the cursor is placed 349 * at the end of the query. 350 */ 351 private void setTextSelection(boolean selectAll) { 352 if (selectAll) { 353 mQueryTextView.setSelection(0, mQueryTextView.length()); 354 } else { 355 mQueryTextView.setSelection(mQueryTextView.length()); 356 } 357 } 358 359 protected void showSourceSelectorDialog() { 360 showDialog(DIALOG_SOURCE_SELECTOR); 361 } 362 363 364 @Override 365 protected Dialog onCreateDialog(int id, Bundle args) { 366 switch (id) { 367 case DIALOG_SOURCE_SELECTOR: 368 return createSourceSelectorDialog(); 369 default: 370 throw new IllegalArgumentException("Unknown dialog: " + id); 371 } 372 } 373 374 @Override 375 protected void onPrepareDialog(int id, Dialog dialog, Bundle args) { 376 switch (id) { 377 case DIALOG_SOURCE_SELECTOR: 378 prepareSourceSelectorDialog((SelectSearchSourceDialog) dialog); 379 break; 380 default: 381 throw new IllegalArgumentException("Unknown dialog: " + id); 382 } 383 } 384 385 protected SelectSearchSourceDialog createSourceSelectorDialog() { 386 SelectSearchSourceDialog dialog = new SelectSearchSourceDialog(this); 387 dialog.setOwnerActivity(this); 388 return dialog; 389 } 390 391 protected void prepareSourceSelectorDialog(SelectSearchSourceDialog dialog) { 392 dialog.setSource(getSourceName()); 393 dialog.setQuery(getQuery()); 394 dialog.setAppData(mAppSearchData); 395 } 396 397 protected void onSearchClicked(int method) { 398 String query = getQuery(); 399 if (DBG) Log.d(TAG, "Search clicked, query=" + query); 400 mTookAction = true; 401 getLogger().logSearch(mSource, method, query.length()); 402 mLauncher.startSearch(mSource, query); 403 } 404 405 protected void onVoiceSearchClicked() { 406 if (DBG) Log.d(TAG, "Voice Search clicked"); 407 mTookAction = true; 408 getLogger().logVoiceSearch(mSource); 409 410 // TODO: should this start voice search in the current source? 411 mLauncher.startVoiceSearch(); 412 } 413 414 protected boolean launchSuggestion(SuggestionPosition suggestion) { 415 return launchSuggestion(suggestion, KeyEvent.KEYCODE_UNKNOWN, null); 416 } 417 418 protected boolean launchSuggestion(SuggestionPosition suggestion, 419 int actionKey, String actionMsg) { 420 if (DBG) Log.d(TAG, "Launching suggestion " + suggestion); 421 mTookAction = true; 422 SuggestionCursor suggestions = mSuggestionsAdapter.getCurrentSuggestions(); 423 // TODO: This should be just the queried sources, but currently 424 // all sources are queried 425 ArrayList<Source> sources = getSuggestionsProvider().getOrderedSources(); 426 getLogger().logSuggestionClick(suggestion.getPosition(), suggestions, sources); 427 428 mLauncher.launchSuggestion(suggestion, actionKey, actionMsg); 429 getShortcutRepository().reportClick(suggestion); 430 return true; 431 } 432 433 protected boolean onSuggestionLongClicked(SuggestionPosition suggestion) { 434 SuggestionCursor sourceResult = suggestion.getSuggestion(); 435 if (DBG) Log.d(TAG, "Long clicked on suggestion " + sourceResult.getSuggestionText1()); 436 return false; 437 } 438 439 protected void onSuggestionSelected(SuggestionPosition suggestion) { 440 if (suggestion == null) { 441 // This happens when a suggestion has been selected with the 442 // dpad / trackball and then a different UI element is touched. 443 // Do nothing, since we want to keep the query of the selection 444 // in the search box. 445 return; 446 } 447 SuggestionCursor sourceResult = suggestion.getSuggestion(); 448 String displayQuery = sourceResult.getSuggestionDisplayQuery(); 449 if (DBG) { 450 Log.d(TAG, "Selected suggestion " + sourceResult.getSuggestionText1() 451 + ",displayQuery="+ displayQuery); 452 } 453 if (TextUtils.isEmpty(displayQuery)) { 454 restoreUserQuery(); 455 } else { 456 setQuery(displayQuery, false); 457 } 458 } 459 460 protected boolean onSuggestionKeyDown(SuggestionPosition suggestion, 461 int keyCode, KeyEvent event) { 462 // Treat enter or search as a click 463 if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_SEARCH) { 464 return launchSuggestion(suggestion); 465 } 466 467 if (keyCode == KeyEvent.KEYCODE_DPAD_UP 468 && mSuggestionsView.getSelectedItemPosition() == 0) { 469 // Moved up from the top suggestion, restore the user query and focus query box 470 if (DBG) Log.d(TAG, "Up and out"); 471 restoreUserQuery(); 472 return false; // let the framework handle the move 473 } 474 475 if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT 476 || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { 477 // Moved left / right from a suggestion, keep current query, move 478 // focus to query box, and move cursor to far left / right 479 if (DBG) Log.d(TAG, "Left/right on a suggestion"); 480 int cursorPos = (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) ? 0 : mQueryTextView.length(); 481 mQueryTextView.setSelection(cursorPos); 482 mQueryTextView.requestFocus(); 483 // TODO: should we modify the list selection? 484 return true; 485 } 486 487 // Handle source-specified action keys 488 String actionMsg = suggestion.getSuggestion().getActionKeyMsg(keyCode); 489 if (actionMsg != null) { 490 return launchSuggestion(suggestion, keyCode, actionMsg); 491 } 492 493 return false; 494 } 495 496 protected void onSourceSelected() { 497 if (DBG) Log.d(TAG, "No suggestion selected"); 498 restoreUserQuery(); 499 } 500 501 protected int getSelectedPosition() { 502 return mSuggestionsView.getSelectedPosition(); 503 } 504 505 protected SuggestionPosition getSelectedSuggestion() { 506 return mSuggestionsView.getSelectedSuggestion(); 507 } 508 509 /** 510 * Hides the input method. 511 */ 512 protected void hideInputMethod() { 513 InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); 514 if (imm != null) { 515 imm.hideSoftInputFromWindow(mQueryTextView.getWindowToken(), 0); 516 } 517 } 518 519 protected void showInputMethodForQuery() { 520 InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); 521 if (imm != null) { 522 imm.showSoftInput(mQueryTextView, 0); 523 } 524 } 525 526 /** 527 * Hides the input method when the suggestions get focus. 528 */ 529 private class SuggestListFocusListener implements OnFocusChangeListener { 530 public void onFocusChange(View v, boolean focused) { 531 if (DBG) Log.d(TAG, "Suggestions focus change, now: " + focused); 532 if (focused) { 533 // The suggestions list got focus, hide the input method 534 hideInputMethod(); 535 } 536 } 537 } 538 539 private class QueryTextViewFocusListener implements OnFocusChangeListener { 540 public void onFocusChange(View v, boolean focused) { 541 if (DBG) Log.d(TAG, "Query focus change, now: " + focused); 542 if (focused) { 543 // The query box got focus, show the input method if the 544 // query box got focus? 545 showInputMethodForQuery(); 546 } 547 } 548 } 549 550 private void startSearchProgress() { 551 // TODO: Cache animation between calls? 552 mSearchGoButton.setImageResource(R.drawable.searching); 553 Animatable animation = (Animatable) mSearchGoButton.getDrawable(); 554 animation.start(); 555 } 556 557 private void stopSearchProgress() { 558 Drawable animation = mSearchGoButton.getDrawable(); 559 if (animation instanceof Animatable) { 560 // TODO: Is this needed, or is it done automatically when the 561 // animation is removed? 562 ((Animatable) animation).stop(); 563 } 564 mSearchGoButton.setImageResource(R.drawable.ic_btn_search); 565 } 566 567 private void updateSuggestions(String query) { 568 query = ltrim(query); 569 LatencyTracker latency = new LatencyTracker(TAG); 570 Suggestions suggestions = getSuggestionsProvider().getSuggestions(query); 571 latency.addEvent("getSuggestions_done"); 572 if (!suggestions.isDone()) { 573 suggestions.registerDataSetObserver(new ProgressUpdater(suggestions)); 574 startSearchProgress(); 575 } else { 576 stopSearchProgress(); 577 } 578 mSuggestionsAdapter.setSuggestions(suggestions); 579 latency.addEvent("shortcuts_shown"); 580 long userVisibleLatency = latency.getUserVisibleLatency(); 581 if (DBG) { 582 Log.d(TAG, "User visible latency (shortcuts): " + userVisibleLatency + " ms."); 583 } 584 } 585 586 private boolean forwardKeyToQueryTextView(int keyCode, KeyEvent event) { 587 if (!event.isSystem() && !isDpadKey(keyCode)) { 588 if (DBG) Log.d(TAG, "Forwarding key to query box: " + event); 589 if (mQueryTextView.requestFocus()) { 590 return mQueryTextView.dispatchKeyEvent(event); 591 } 592 } 593 return false; 594 } 595 596 private boolean isDpadKey(int keyCode) { 597 switch (keyCode) { 598 case KeyEvent.KEYCODE_DPAD_UP: 599 case KeyEvent.KEYCODE_DPAD_DOWN: 600 case KeyEvent.KEYCODE_DPAD_LEFT: 601 case KeyEvent.KEYCODE_DPAD_RIGHT: 602 case KeyEvent.KEYCODE_DPAD_CENTER: 603 return true; 604 default: 605 return false; 606 } 607 } 608 609 /** 610 * Filters the suggestions list when the search text changes. 611 */ 612 private class SearchTextWatcher implements TextWatcher { 613 public void afterTextChanged(Editable s) { 614 if (mUpdateSuggestions) { 615 String query = s == null ? "" : s.toString(); 616 setUserQuery(query); 617 updateSuggestions(query); 618 } 619 } 620 621 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 622 } 623 624 public void onTextChanged(CharSequence s, int start, int before, int count) { 625 } 626 } 627 628 /** 629 * Handles non-text keys in the query text view. 630 */ 631 private class QueryTextViewKeyListener implements View.OnKeyListener { 632 public boolean onKey(View view, int keyCode, KeyEvent event) { 633 // Handle IME search action key 634 if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_UP) { 635 onSearchClicked(Logger.SEARCH_METHOD_KEYBOARD); 636 } 637 return false; 638 } 639 } 640 641 /** 642 * Handles key events on the search and voice search buttons, 643 * by refocusing to EditText. 644 */ 645 private class ButtonsKeyListener implements View.OnKeyListener { 646 public boolean onKey(View v, int keyCode, KeyEvent event) { 647 return forwardKeyToQueryTextView(keyCode, event); 648 } 649 } 650 651 /** 652 * Handles key events on the suggestions list view. 653 */ 654 private class SuggestionsViewKeyListener implements View.OnKeyListener { 655 public boolean onKey(View v, int keyCode, KeyEvent event) { 656 if (event.getAction() == KeyEvent.ACTION_DOWN) { 657 SuggestionPosition suggestion = getSelectedSuggestion(); 658 if (suggestion != null) { 659 if (onSuggestionKeyDown(suggestion, keyCode, event)) { 660 return true; 661 } 662 } 663 } 664 return forwardKeyToQueryTextView(keyCode, event); 665 } 666 } 667 668 private class InputMethodCloser implements SuggestionsView.InteractionListener { 669 public void onInteraction() { 670 hideInputMethod(); 671 } 672 } 673 674 private class ClickHandler implements SuggestionClickListener { 675 public void onSuggestionClicked(SuggestionPosition suggestion) { 676 launchSuggestion(suggestion); 677 } 678 679 public boolean onSuggestionLongClicked(SuggestionPosition suggestion) { 680 return SearchActivity.this.onSuggestionLongClicked(suggestion); 681 } 682 } 683 684 private class SelectionHandler implements SuggestionSelectionListener { 685 public void onSelectionChanged(SuggestionPosition suggestion) { 686 onSuggestionSelected(suggestion); 687 } 688 } 689 690 /** 691 * Listens for clicks on the source selector. 692 */ 693 private class SearchGoButtonClickListener implements View.OnClickListener { 694 public void onClick(View view) { 695 onSearchClicked(Logger.SEARCH_METHOD_BUTTON); 696 } 697 } 698 699 /** 700 * Listens for clicks on the search button. 701 */ 702 private class SourceSelectorClickListener implements View.OnClickListener { 703 public void onClick(View view) { 704 showSourceSelectorDialog(); 705 } 706 } 707 708 /** 709 * Listens for clicks on the voice search button. 710 */ 711 private class VoiceSearchButtonClickListener implements View.OnClickListener { 712 public void onClick(View view) { 713 onVoiceSearchClicked(); 714 } 715 } 716 717 /** 718 * Updates the progress bar when the suggestions adapter changes its progress. 719 */ 720 private class ProgressUpdater extends DataSetObserver { 721 private final Suggestions mSuggestions; 722 723 public ProgressUpdater(Suggestions suggestions) { 724 mSuggestions = suggestions; 725 } 726 727 @Override 728 public void onChanged() { 729 if (mSuggestions.isDone()) { 730 stopSearchProgress(); 731 } 732 } 733 } 734 735 private static String ltrim(String text) { 736 int start = 0; 737 int length = text.length(); 738 while (start < length && Character.isWhitespace(text.charAt(start))) { 739 start++; 740 } 741 return start > 0 ? text.substring(start, length) : text; 742 } 743 744} 745