SearchActivity.java revision 6ae65b29ce9313a4f0624ef825f75151db5ec2fe
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.SuggestionsView; 25 26import android.app.Activity; 27import android.app.SearchManager; 28import android.content.DialogInterface; 29import android.content.Intent; 30import android.database.DataSetObserver; 31import android.graphics.drawable.Drawable; 32import android.net.Uri; 33import android.os.Bundle; 34import android.os.Debug; 35import android.os.Handler; 36import android.text.Editable; 37import android.text.TextUtils; 38import android.text.TextWatcher; 39import android.util.Log; 40import android.view.KeyEvent; 41import android.view.Menu; 42import android.view.View; 43import android.view.View.OnFocusChangeListener; 44import android.view.inputmethod.InputMethodManager; 45import android.widget.AbsListView; 46import android.widget.EditText; 47import android.widget.ImageButton; 48 49import java.io.File; 50import java.util.Collection; 51 52/** 53 * The main activity for Quick Search Box. Shows the search UI. 54 * 55 */ 56public class SearchActivity extends Activity { 57 58 private static final boolean DBG = false; 59 private static final String TAG = "QSB.SearchActivity"; 60 private static final boolean TRACE = false; 61 62 private static final String SCHEME_CORPUS = "qsb.corpus"; 63 64 public static final String INTENT_ACTION_QSB_AND_SELECT_CORPUS 65 = "com.android.quicksearchbox.action.QSB_AND_SELECT_CORPUS"; 66 67 // The string used for privateImeOptions to identify to the IME that it should not show 68 // a microphone button since one already exists in the search dialog. 69 // TODO: This should move to android-common or something. 70 private static final String IME_OPTION_NO_MICROPHONE = "nm"; 71 72 // Keys for the saved instance state. 73 private static final String INSTANCE_KEY_CORPUS = "corpus"; 74 private static final String INSTANCE_KEY_USER_QUERY = "query"; 75 76 // Measures time from for last onCreate()/onNewIntent() call. 77 private LatencyTracker mStartLatencyTracker; 78 // Whether QSB is starting. True between the calls to onCreate()/onNewIntent() and onResume(). 79 private boolean mStarting; 80 // True if the user has taken some action, e.g. launching a search, voice search, 81 // or suggestions, since QSB was last started. 82 private boolean mTookAction; 83 84 private CorpusSelectionDialog mCorpusSelectionDialog; 85 86 protected SuggestionsAdapter mSuggestionsAdapter; 87 88 private CorporaObserver mCorporaObserver; 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 96 protected ImageButton mSearchGoButton; 97 protected ImageButton mVoiceSearchButton; 98 protected ImageButton mCorpusIndicator; 99 100 private VoiceSearch mVoiceSearch; 101 102 private Corpus mCorpus; 103 private Bundle mAppSearchData; 104 private boolean mUpdateSuggestions; 105 private String mUserQuery; 106 private boolean mSelectAll; 107 108 private final Handler mHandler = new Handler(); 109 private final Runnable mUpdateSuggestionsTask = new Runnable() { 110 public void run() { 111 updateSuggestions(getQuery()); 112 } 113 }; 114 115 private final Runnable mShowInputMethodTask = new Runnable() { 116 public void run() { 117 showInputMethodForQuery(); 118 } 119 }; 120 121 /** Called when the activity is first created. */ 122 @Override 123 public void onCreate(Bundle savedInstanceState) { 124 if (TRACE) startMethodTracing(); 125 recordStartTime(); 126 if (DBG) Log.d(TAG, "onCreate()"); 127 super.onCreate(savedInstanceState); 128 129 setContentView(); 130 mSuggestionsAdapter = getQsbApplication().createSuggestionsAdapter(); 131 132 mQueryTextView = (EditText) findViewById(R.id.search_src_text); 133 mSuggestionsView = (SuggestionsView) findViewById(R.id.suggestions); 134 mSuggestionsView.setSuggestionClickListener(new ClickHandler()); 135 mSuggestionsView.addSuggestionSelectionListener(new SelectionHandler()); 136 mSuggestionsView.setOnScrollListener(new InputMethodCloser()); 137 mSuggestionsView.setOnKeyListener(new SuggestionsViewKeyListener()); 138 mSuggestionsView.setOnFocusChangeListener(new SuggestListFocusListener()); 139 140 mSearchGoButton = (ImageButton) findViewById(R.id.search_go_btn); 141 mVoiceSearchButton = (ImageButton) findViewById(R.id.search_voice_btn); 142 mCorpusIndicator = (ImageButton) findViewById(R.id.corpus_indicator); 143 144 mVoiceSearch = new VoiceSearch(this); 145 146 mQueryTextView.addTextChangedListener(new SearchTextWatcher()); 147 mQueryTextView.setOnKeyListener(new QueryTextViewKeyListener()); 148 mQueryTextView.setOnFocusChangeListener(new QueryTextViewFocusListener()); 149 150 mCorpusIndicator.setOnClickListener(new CorpusIndicatorClickListener()); 151 152 mSearchGoButton.setOnClickListener(new SearchGoButtonClickListener()); 153 154 mVoiceSearchButton.setOnClickListener(new VoiceSearchButtonClickListener()); 155 156 ButtonsKeyListener buttonsKeyListener = new ButtonsKeyListener(); 157 mSearchGoButton.setOnKeyListener(buttonsKeyListener); 158 mVoiceSearchButton.setOnKeyListener(buttonsKeyListener); 159 mCorpusIndicator.setOnKeyListener(buttonsKeyListener); 160 161 mUpdateSuggestions = true; 162 163 // First get setup from intent 164 Intent intent = getIntent(); 165 setupFromIntent(intent); 166 // Then restore any saved instance state 167 restoreInstanceState(savedInstanceState); 168 169 // Do this at the end, to avoid updating the list view when setSource() 170 // is called. 171 mSuggestionsView.setAdapter(mSuggestionsAdapter); 172 173 mCorporaObserver = new CorporaObserver(); 174 getCorpora().registerDataSetObserver(mCorporaObserver); 175 } 176 177 protected void setContentView() { 178 setContentView(R.layout.search_activity); 179 } 180 181 private void startMethodTracing() { 182 File traceDir = getDir("traces", 0); 183 String traceFile = new File(traceDir, "qsb.trace").getAbsolutePath(); 184 Debug.startMethodTracing(traceFile); 185 } 186 187 @Override 188 protected void onNewIntent(Intent intent) { 189 recordStartTime(); 190 setIntent(intent); 191 setupFromIntent(intent); 192 } 193 194 private void recordStartTime() { 195 mStartLatencyTracker = new LatencyTracker(); 196 mStarting = true; 197 mTookAction = false; 198 } 199 200 protected void restoreInstanceState(Bundle savedInstanceState) { 201 if (savedInstanceState == null) return; 202 String corpusName = savedInstanceState.getString(INSTANCE_KEY_CORPUS); 203 String query = savedInstanceState.getString(INSTANCE_KEY_USER_QUERY); 204 setCorpus(corpusName); 205 setUserQuery(query); 206 } 207 208 @Override 209 protected void onSaveInstanceState(Bundle outState) { 210 super.onSaveInstanceState(outState); 211 // We don't save appSearchData, since we always get the value 212 // from the intent and the user can't change it. 213 214 outState.putString(INSTANCE_KEY_CORPUS, getCorpusName()); 215 outState.putString(INSTANCE_KEY_USER_QUERY, mUserQuery); 216 } 217 218 private void setupFromIntent(Intent intent) { 219 if (DBG) Log.d(TAG, "setupFromIntent(" + intent.toUri(0) + ")"); 220 String corpusName = getCorpusNameFromUri(intent.getData()); 221 String query = intent.getStringExtra(SearchManager.QUERY); 222 Bundle appSearchData = intent.getBundleExtra(SearchManager.APP_DATA); 223 224 setCorpus(corpusName); 225 setUserQuery(query); 226 mSelectAll = intent.getBooleanExtra(SearchManager.EXTRA_SELECT_QUERY, false); 227 mAppSearchData = appSearchData; 228 229 if (startedIntoCorpusSelectionDialog()) { 230 showCorpusSelectionDialog(); 231 } 232 } 233 234 public boolean startedIntoCorpusSelectionDialog() { 235 return INTENT_ACTION_QSB_AND_SELECT_CORPUS.equals(getIntent().getAction()); 236 } 237 238 /** 239 * Removes corpus selector intent action, so that BACK works normally after 240 * dismissing and reopening the corpus selector. 241 */ 242 private void clearStartedIntoCorpusSelectionDialog() { 243 Intent oldIntent = getIntent(); 244 if (SearchActivity.INTENT_ACTION_QSB_AND_SELECT_CORPUS.equals(oldIntent.getAction())) { 245 Intent newIntent = new Intent(oldIntent); 246 newIntent.setAction(SearchManager.INTENT_ACTION_GLOBAL_SEARCH); 247 setIntent(newIntent); 248 } 249 } 250 251 public static Uri getCorpusUri(Corpus corpus) { 252 if (corpus == null) return null; 253 return new Uri.Builder() 254 .scheme(SCHEME_CORPUS) 255 .authority(corpus.getName()) 256 .build(); 257 } 258 259 private String getCorpusNameFromUri(Uri uri) { 260 if (uri == null) return null; 261 if (!SCHEME_CORPUS.equals(uri.getScheme())) return null; 262 return uri.getAuthority(); 263 } 264 265 private Corpus getCorpus(String sourceName) { 266 if (sourceName == null) return null; 267 Corpus corpus = getCorpora().getCorpus(sourceName); 268 if (corpus == null) { 269 Log.w(TAG, "Unknown corpus " + sourceName); 270 return null; 271 } 272 return corpus; 273 } 274 275 private void setCorpus(String corpusName) { 276 if (DBG) Log.d(TAG, "setCorpus(" + corpusName + ")"); 277 mCorpus = getCorpus(corpusName); 278 Drawable sourceIcon; 279 if (mCorpus == null) { 280 sourceIcon = getCorpusViewFactory().getGlobalSearchIcon(); 281 } else { 282 sourceIcon = mCorpus.getCorpusIcon(); 283 } 284 mSuggestionsAdapter.setCorpus(mCorpus); 285 mCorpusIndicator.setImageDrawable(sourceIcon); 286 287 updateUi(getQuery().length() == 0); 288 } 289 290 private String getCorpusName() { 291 return mCorpus == null ? null : mCorpus.getName(); 292 } 293 294 private QsbApplication getQsbApplication() { 295 return (QsbApplication) getApplication(); 296 } 297 298 private Config getConfig() { 299 return getQsbApplication().getConfig(); 300 } 301 302 private Corpora getCorpora() { 303 return getQsbApplication().getCorpora(); 304 } 305 306 private ShortcutRepository getShortcutRepository() { 307 return getQsbApplication().getShortcutRepository(); 308 } 309 310 private SuggestionsProvider getSuggestionsProvider() { 311 return getQsbApplication().getSuggestionsProvider(); 312 } 313 314 private CorpusRanker getCorpusRanker() { 315 return getQsbApplication().getCorpusRanker(); 316 } 317 318 private CorpusViewFactory getCorpusViewFactory() { 319 return getQsbApplication().getCorpusViewFactory(); 320 } 321 322 private Logger getLogger() { 323 return getQsbApplication().getLogger(); 324 } 325 326 @Override 327 protected void onDestroy() { 328 if (DBG) Log.d(TAG, "onDestroy()"); 329 super.onDestroy(); 330 getCorpora().unregisterDataSetObserver(mCorporaObserver); 331 mSuggestionsView.setAdapter(null); // closes mSuggestionsAdapter 332 } 333 334 @Override 335 protected void onStop() { 336 if (DBG) Log.d(TAG, "onStop()"); 337 if (!mTookAction) { 338 // TODO: This gets logged when starting other activities, e.g. by opening he search 339 // settings, or clicking a notification in the status bar. 340 getLogger().logExit(getCurrentSuggestions(), getQuery().length()); 341 } 342 // Close all open suggestion cursors. The query will be redone in onResume() 343 // if we come back to this activity. 344 mSuggestionsAdapter.setSuggestions(null); 345 getQsbApplication().getShortcutRefresher().reset(); 346 dismissCorpusSelectionDialog(); 347 super.onStop(); 348 } 349 350 @Override 351 protected void onResume() { 352 if (DBG) Log.d(TAG, "onResume()"); 353 super.onResume(); 354 setQuery(mUserQuery, mSelectAll); 355 // Only select everything the first time after creating the activity. 356 mSelectAll = false; 357 updateSuggestionsBuffered(); 358 if (!isCorpusSelectionDialogShowing()) { 359 mQueryTextView.requestFocus(); 360 } 361 if (TRACE) Debug.stopMethodTracing(); 362 } 363 364 @Override 365 public boolean onCreateOptionsMenu(Menu menu) { 366 super.onCreateOptionsMenu(menu); 367 SearchSettings.addSearchSettingsMenuItem(this, menu); 368 return true; 369 } 370 371 @Override 372 public void onWindowFocusChanged(boolean hasFocus) { 373 super.onWindowFocusChanged(hasFocus); 374 if (hasFocus) { 375 // Launch the IME after a bit 376 mHandler.postDelayed(mShowInputMethodTask, 0); 377 } 378 } 379 380 /** 381 * Sets the query as typed by the user. Does not update the suggestions 382 * or the text in the query box. 383 */ 384 protected void setUserQuery(String userQuery) { 385 if (userQuery == null) userQuery = ""; 386 mUserQuery = userQuery; 387 } 388 389 protected String getQuery() { 390 CharSequence q = mQueryTextView.getText(); 391 return q == null ? "" : q.toString(); 392 } 393 394 /** 395 * Restores the query entered by the user. 396 */ 397 private void restoreUserQuery() { 398 if (DBG) Log.d(TAG, "Restoring query to '" + mUserQuery + "'"); 399 setQuery(mUserQuery, false); 400 } 401 402 /** 403 * Sets the text in the query box. Does not update the suggestions, 404 * and does not change the saved user-entered query. 405 * {@link #restoreUserQuery()} will restore the query to the last 406 * user-entered query. 407 */ 408 private void setQuery(String query, boolean selectAll) { 409 mUpdateSuggestions = false; 410 mQueryTextView.setText(query); 411 setTextSelection(selectAll); 412 mUpdateSuggestions = true; 413 } 414 415 /** 416 * Sets the text selection in the query text view. 417 * 418 * @param selectAll If {@code true}, selects the entire query. 419 * If {@false}, no characters are selected, and the cursor is placed 420 * at the end of the query. 421 */ 422 private void setTextSelection(boolean selectAll) { 423 if (selectAll) { 424 mQueryTextView.selectAll(); 425 } else { 426 mQueryTextView.setSelection(mQueryTextView.length()); 427 } 428 } 429 430 protected void updateUi(boolean queryEmpty) { 431 updateQueryTextView(queryEmpty); 432 updateSearchGoButton(queryEmpty); 433 updateVoiceSearchButton(queryEmpty); 434 } 435 436 private void updateQueryTextView(boolean queryEmpty) { 437 if (queryEmpty) { 438 if (mCorpus == null || mCorpus.isWebCorpus()) { 439 mQueryTextView.setBackgroundResource(R.drawable.textfield_search_empty_google); 440 mQueryTextView.setHint(null); 441 } else { 442 mQueryTextView.setBackgroundResource(R.drawable.textfield_search_empty); 443 mQueryTextView.setHint(mCorpus.getHint()); 444 } 445 } else { 446 mQueryTextView.setBackgroundResource(R.drawable.textfield_search); 447 } 448 } 449 450 private void updateSearchGoButton(boolean queryEmpty) { 451 if (queryEmpty) { 452 mSearchGoButton.setVisibility(View.GONE); 453 } else { 454 mSearchGoButton.setVisibility(View.VISIBLE); 455 } 456 } 457 458 protected void updateVoiceSearchButton(boolean queryEmpty) { 459 if (queryEmpty && mVoiceSearch.shouldShowVoiceSearch(mCorpus)) { 460 mVoiceSearchButton.setVisibility(View.VISIBLE); 461 mQueryTextView.setPrivateImeOptions(IME_OPTION_NO_MICROPHONE); 462 } else { 463 mVoiceSearchButton.setVisibility(View.GONE); 464 mQueryTextView.setPrivateImeOptions(null); 465 } 466 } 467 468 protected void showCorpusSelectionDialog() { 469 if (mCorpusSelectionDialog == null) { 470 mCorpusSelectionDialog = new CorpusSelectionDialog(this); 471 mCorpusSelectionDialog.setOwnerActivity(this); 472 mCorpusSelectionDialog.setOnDismissListener(new CorpusSelectorDismissListener()); 473 mCorpusSelectionDialog.setOnCorpusSelectedListener(new CorpusSelectionListener()); 474 } 475 mCorpusSelectionDialog.show(mCorpus); 476 } 477 478 protected boolean isCorpusSelectionDialogShowing() { 479 return mCorpusSelectionDialog != null && mCorpusSelectionDialog.isShowing(); 480 } 481 482 protected void dismissCorpusSelectionDialog() { 483 if (mCorpusSelectionDialog != null) { 484 mCorpusSelectionDialog.dismiss(); 485 } 486 } 487 488 /** 489 * @return true if a search was performed as a result of this click, false otherwise. 490 */ 491 protected boolean onSearchClicked(int method) { 492 String query = ltrim(getQuery()); 493 if (DBG) Log.d(TAG, "Search clicked, query=" + query); 494 495 // Don't do empty queries 496 if (TextUtils.getTrimmedLength(query) == 0) return false; 497 498 Corpus searchCorpus = getSearchCorpus(); 499 if (searchCorpus == null) return false; 500 501 mTookAction = true; 502 503 // Log search start 504 getLogger().logSearch(mCorpus, method, query.length()); 505 506 // Create shortcut 507 SuggestionData searchShortcut = searchCorpus.createSearchShortcut(query); 508 if (searchShortcut != null) { 509 DataSuggestionCursor cursor = new DataSuggestionCursor(query); 510 cursor.add(searchShortcut); 511 getShortcutRepository().reportClick(cursor, 0); 512 } 513 514 // Start search 515 Intent intent = searchCorpus.createSearchIntent(query, mAppSearchData); 516 launchIntent(intent); 517 return true; 518 } 519 520 protected void onVoiceSearchClicked() { 521 if (DBG) Log.d(TAG, "Voice Search clicked"); 522 Corpus searchCorpus = getSearchCorpus(); 523 if (searchCorpus == null) return; 524 525 mTookAction = true; 526 527 // Log voice search start 528 getLogger().logVoiceSearch(searchCorpus); 529 530 // Start voice search 531 Intent intent = searchCorpus.createVoiceSearchIntent(mAppSearchData); 532 launchIntent(intent); 533 } 534 535 /** 536 * Gets the corpus to use for any searches. This is the web corpus in "All" mode, 537 * and the selected corpus otherwise. 538 */ 539 protected Corpus getSearchCorpus() { 540 if (mCorpus != null) { 541 return mCorpus; 542 } else { 543 Corpus webCorpus = getCorpora().getWebCorpus(); 544 if (webCorpus == null) { 545 Log.e(TAG, "No web corpus"); 546 } 547 return webCorpus; 548 } 549 } 550 551 protected SuggestionCursor getCurrentSuggestions() { 552 return mSuggestionsAdapter.getCurrentSuggestions(); 553 } 554 555 protected void launchIntent(Intent intent) { 556 if (intent == null) { 557 return; 558 } 559 try { 560 startActivity(intent); 561 } catch (RuntimeException ex) { 562 // Since the intents for suggestions specified by suggestion providers, 563 // guard against them not being handled, not allowed, etc. 564 Log.e(TAG, "Failed to start " + intent.toUri(0), ex); 565 } 566 } 567 568 protected boolean launchSuggestion(int position) { 569 SuggestionCursor suggestions = getCurrentSuggestions(); 570 if (position < 0 || position >= suggestions.getCount()) { 571 Log.w(TAG, "Tried to launch invalid suggestion " + position); 572 return false; 573 } 574 575 if (DBG) Log.d(TAG, "Launching suggestion " + position); 576 mTookAction = true; 577 578 // Log suggestion click 579 Collection<Corpus> corpora = mSuggestionsAdapter.getSuggestions().getIncludedCorpora(); 580 getLogger().logSuggestionClick(position, suggestions, corpora); 581 582 // Create shortcut 583 getShortcutRepository().reportClick(suggestions, position); 584 585 // Launch intent 586 suggestions.moveTo(position); 587 Intent intent = suggestions.getSuggestionIntent(mAppSearchData); 588 launchIntent(intent); 589 590 return true; 591 } 592 593 protected boolean onSuggestionLongClicked(int position) { 594 if (DBG) Log.d(TAG, "Long clicked on suggestion " + position); 595 return false; 596 } 597 598 protected boolean onSuggestionKeyDown(int position, int keyCode, KeyEvent event) { 599 // Treat enter or search as a click 600 if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_SEARCH) { 601 return launchSuggestion(position); 602 } 603 604 if (keyCode == KeyEvent.KEYCODE_DPAD_UP && position == 0) { 605 // Moved up from the top suggestion, restore the user query and focus query box 606 if (DBG) Log.d(TAG, "Up and out"); 607 restoreUserQuery(); 608 return false; // let the framework handle the move 609 } 610 611 if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT 612 || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { 613 // Moved left / right from a suggestion, keep current query, move 614 // focus to query box, and move cursor to far left / right 615 if (DBG) Log.d(TAG, "Left/right on a suggestion"); 616 String query = getQuery(); 617 int cursorPos = (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) ? 0 : query.length(); 618 mQueryTextView.setSelection(cursorPos); 619 mQueryTextView.requestFocus(); 620 updateSuggestions(query); 621 return true; 622 } 623 624 return false; 625 } 626 627 protected void onSourceSelected() { 628 if (DBG) Log.d(TAG, "No suggestion selected"); 629 restoreUserQuery(); 630 } 631 632 protected int getSelectedPosition() { 633 return mSuggestionsView.getSelectedPosition(); 634 } 635 636 /** 637 * Hides the input method. 638 */ 639 protected void hideInputMethod() { 640 InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); 641 if (imm != null) { 642 imm.hideSoftInputFromWindow(mQueryTextView.getWindowToken(), 0); 643 } 644 } 645 646 protected void showInputMethodForQuery() { 647 InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); 648 if (imm != null) { 649 imm.showSoftInput(mQueryTextView, 0); 650 } 651 } 652 653 /** 654 * Hides the input method when the suggestions get focus. 655 */ 656 private class SuggestListFocusListener implements OnFocusChangeListener { 657 public void onFocusChange(View v, boolean focused) { 658 if (DBG) Log.d(TAG, "Suggestions focus change, now: " + focused); 659 if (focused) { 660 // The suggestions list got focus, hide the input method 661 hideInputMethod(); 662 } 663 } 664 } 665 666 private class QueryTextViewFocusListener implements OnFocusChangeListener { 667 public void onFocusChange(View v, boolean focused) { 668 if (DBG) Log.d(TAG, "Query focus change, now: " + focused); 669 if (focused) { 670 // The query box got focus, show the input method 671 showInputMethodForQuery(); 672 } 673 } 674 } 675 676 private int getMaxSuggestions() { 677 Config config = getConfig(); 678 return mCorpus == null 679 ? config.getMaxPromotedSuggestions() 680 : config.getMaxResultsPerSource(); 681 } 682 683 private void updateSuggestionsBuffered() { 684 mHandler.removeCallbacks(mUpdateSuggestionsTask); 685 long delay = getConfig().getTypingUpdateSuggestionsDelayMillis(); 686 mHandler.postDelayed(mUpdateSuggestionsTask, delay); 687 } 688 689 protected void updateSuggestions(String query) { 690 691 query = ltrim(query); 692 Suggestions suggestions = getSuggestionsProvider().getSuggestions( 693 query, mCorpus, getMaxSuggestions()); 694 695 // Log start latency if this is the first suggestions update 696 if (mStarting) { 697 mStarting = false; 698 String source = getIntent().getStringExtra(Search.SOURCE); 699 int latency = mStartLatencyTracker.getLatency(); 700 getLogger().logStart(latency, source, mCorpus, suggestions.getExpectedCorpora()); 701 } 702 703 mSuggestionsAdapter.setSuggestions(suggestions); 704 } 705 706 private boolean forwardKeyToQueryTextView(int keyCode, KeyEvent event) { 707 if (!event.isSystem() && !isDpadKey(keyCode)) { 708 if (DBG) Log.d(TAG, "Forwarding key to query box: " + event); 709 if (mQueryTextView.requestFocus()) { 710 return mQueryTextView.dispatchKeyEvent(event); 711 } 712 } 713 return false; 714 } 715 716 private boolean isDpadKey(int keyCode) { 717 switch (keyCode) { 718 case KeyEvent.KEYCODE_DPAD_UP: 719 case KeyEvent.KEYCODE_DPAD_DOWN: 720 case KeyEvent.KEYCODE_DPAD_LEFT: 721 case KeyEvent.KEYCODE_DPAD_RIGHT: 722 case KeyEvent.KEYCODE_DPAD_CENTER: 723 return true; 724 default: 725 return false; 726 } 727 } 728 729 /** 730 * Filters the suggestions list when the search text changes. 731 */ 732 private class SearchTextWatcher implements TextWatcher { 733 public void afterTextChanged(Editable s) { 734 boolean empty = s.length() == 0; 735 if (empty != mQueryWasEmpty) { 736 mQueryWasEmpty = empty; 737 updateUi(empty); 738 } 739 if (mUpdateSuggestions) { 740 String query = s == null ? "" : s.toString(); 741 setUserQuery(query); 742 updateSuggestionsBuffered(); 743 } 744 } 745 746 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 747 } 748 749 public void onTextChanged(CharSequence s, int start, int before, int count) { 750 } 751 } 752 753 /** 754 * Handles non-text keys in the query text view. 755 */ 756 private class QueryTextViewKeyListener implements View.OnKeyListener { 757 public boolean onKey(View view, int keyCode, KeyEvent event) { 758 // Handle IME search action key 759 if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_UP) { 760 // if no action was taken, consume the key event so that the keyboard 761 // remains on screen. 762 return !onSearchClicked(Logger.SEARCH_METHOD_KEYBOARD); 763 } 764 return false; 765 } 766 } 767 768 /** 769 * Handles key events on the search and voice search buttons, 770 * by refocusing to EditText. 771 */ 772 private class ButtonsKeyListener implements View.OnKeyListener { 773 public boolean onKey(View v, int keyCode, KeyEvent event) { 774 return forwardKeyToQueryTextView(keyCode, event); 775 } 776 } 777 778 /** 779 * Handles key events on the suggestions list view. 780 */ 781 private class SuggestionsViewKeyListener implements View.OnKeyListener { 782 public boolean onKey(View v, int keyCode, KeyEvent event) { 783 if (event.getAction() == KeyEvent.ACTION_DOWN) { 784 int position = getSelectedPosition(); 785 if (onSuggestionKeyDown(position, keyCode, event)) { 786 return true; 787 } 788 } 789 return forwardKeyToQueryTextView(keyCode, event); 790 } 791 } 792 793 private class InputMethodCloser implements SuggestionsView.OnScrollListener { 794 795 public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, 796 int totalItemCount) { 797 } 798 799 public void onScrollStateChanged(AbsListView view, int scrollState) { 800 hideInputMethod(); 801 } 802 } 803 804 private class ClickHandler implements SuggestionClickListener { 805 public void onSuggestionClicked(int position) { 806 launchSuggestion(position); 807 } 808 809 public boolean onSuggestionLongClicked(int position) { 810 return SearchActivity.this.onSuggestionLongClicked(position); 811 } 812 813 public void onSuggestionQueryRefineClicked(int position) { 814 if (DBG) Log.d(TAG, "query refine clicked, pos " + position); 815 SuggestionCursor suggestions = getCurrentSuggestions(); 816 if (suggestions != null) { 817 suggestions.moveTo(position); 818 String query = suggestions.getSuggestionQuery(); 819 if (!TextUtils.isEmpty(query)) { 820 query += " "; 821 setUserQuery(query); 822 setQuery(query, false); 823 updateSuggestions(query); 824 } 825 } 826 } 827 } 828 829 private class SelectionHandler implements SuggestionSelectionListener { 830 public void onSuggestionSelected(int position) { 831 SuggestionCursor suggestions = getCurrentSuggestions(); 832 suggestions.moveTo(position); 833 String displayQuery = suggestions.getSuggestionDisplayQuery(); 834 if (TextUtils.isEmpty(displayQuery)) { 835 restoreUserQuery(); 836 } else { 837 setQuery(displayQuery, false); 838 } 839 } 840 841 public void onNothingSelected() { 842 // This happens when a suggestion has been selected with the 843 // dpad / trackball and then a different UI element is touched. 844 // Do nothing, since we want to keep the query of the selection 845 // in the search box. 846 } 847 } 848 849 /** 850 * Listens for clicks on the source selector. 851 */ 852 private class SearchGoButtonClickListener implements View.OnClickListener { 853 public void onClick(View view) { 854 onSearchClicked(Logger.SEARCH_METHOD_BUTTON); 855 } 856 } 857 858 /** 859 * Listens for clicks on the search button. 860 */ 861 private class CorpusIndicatorClickListener implements View.OnClickListener { 862 public void onClick(View view) { 863 showCorpusSelectionDialog(); 864 } 865 } 866 867 private class CorpusSelectorDismissListener implements DialogInterface.OnDismissListener { 868 public void onDismiss(DialogInterface dialog) { 869 if (DBG) Log.d(TAG, "Corpus selector dismissed"); 870 clearStartedIntoCorpusSelectionDialog(); 871 } 872 } 873 874 private class CorpusSelectionListener 875 implements CorpusSelectionDialog.OnCorpusSelectedListener { 876 public void onCorpusSelected(String corpusName) { 877 setCorpus(corpusName); 878 updateSuggestions(getQuery()); 879 mQueryTextView.requestFocus(); 880 showInputMethodForQuery(); 881 } 882 } 883 884 /** 885 * Listens for clicks on the voice search button. 886 */ 887 private class VoiceSearchButtonClickListener implements View.OnClickListener { 888 public void onClick(View view) { 889 onVoiceSearchClicked(); 890 } 891 } 892 893 private class CorporaObserver extends DataSetObserver { 894 @Override 895 public void onChanged() { 896 setCorpus(getCorpusName()); 897 updateSuggestions(getQuery()); 898 } 899 } 900 901 private static String ltrim(String text) { 902 int start = 0; 903 int length = text.length(); 904 while (start < length && Character.isWhitespace(text.charAt(start))) { 905 start++; 906 } 907 return start > 0 ? text.substring(start, length) : text; 908 } 909 910} 911