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