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