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