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