SearchActivity.java revision 2dfefcf67c7c28c8337725ef30d623a47638b374
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.Set; 54 55/** 56 * The main activity for Quick Search Box. Shows the search UI. 57 * 58 */ 59public class SearchActivity extends Activity { 60 61 private static final boolean DBG = false; 62 private static final String TAG = "QSB.SearchActivity"; 63 private static final boolean TRACE = false; 64 65 private static final String SCHEME_CORPUS = "qsb.corpus"; 66 67 public static final String INTENT_ACTION_QSB_AND_SELECT_CORPUS 68 = "com.android.quicksearchbox.action.QSB_AND_SELECT_CORPUS"; 69 70 // The string used for privateImeOptions to identify to the IME that it should not show 71 // a microphone button since one already exists in the search dialog. 72 // TODO: This should move to android-common or something. 73 private static final String IME_OPTION_NO_MICROPHONE = "nm"; 74 75 // Keys for the saved instance state. 76 private static final String INSTANCE_KEY_CORPUS = "corpus"; 77 private static final String INSTANCE_KEY_QUERY = "query"; 78 79 // Measures time from for last onCreate()/onNewIntent() call. 80 private LatencyTracker mStartLatencyTracker; 81 // Whether QSB is starting. True between the calls to onCreate()/onNewIntent() and onResume(). 82 private boolean mStarting; 83 // True if the user has taken some action, e.g. launching a search, voice search, 84 // or suggestions, since QSB was last started. 85 private boolean mTookAction; 86 87 private CorpusSelectionDialog mCorpusSelectionDialog; 88 89 protected SuggestionsAdapter mSuggestionsAdapter; 90 91 private CorporaObserver mCorporaObserver; 92 93 protected QueryTextView mQueryTextView; 94 // True if the query was empty on the previous call to updateQuery() 95 protected boolean mQueryWasEmpty = true; 96 protected Drawable mQueryTextEmptyBg; 97 protected Drawable mQueryTextNotEmptyBg; 98 99 protected SuggestionsView mSuggestionsView; 100 101 protected ImageButton mSearchGoButton; 102 protected ImageButton mVoiceSearchButton; 103 protected ImageButton mCorpusIndicator; 104 105 private Corpus mCorpus; 106 private Bundle mAppSearchData; 107 private boolean mUpdateSuggestions; 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(); 131 SuggestListFocusListener suggestionFocusListener = new SuggestListFocusListener(); 132 mSuggestionsAdapter = getQsbApplication().createSuggestionsAdapter(); 133 mSuggestionsAdapter.setSuggestionClickListener(new ClickHandler()); 134 mSuggestionsAdapter.setOnFocusChangeListener(suggestionFocusListener); 135 136 mQueryTextView = (QueryTextView) findViewById(R.id.search_src_text); 137 mSuggestionsView = (SuggestionsView) findViewById(R.id.suggestions); 138 mSuggestionsView.setOnScrollListener(new InputMethodCloser()); 139 mSuggestionsView.setOnKeyListener(new SuggestionsViewKeyListener()); 140 mSuggestionsView.setOnFocusChangeListener(suggestionFocusListener); 141 142 mSearchGoButton = (ImageButton) findViewById(R.id.search_go_btn); 143 mVoiceSearchButton = (ImageButton) findViewById(R.id.search_voice_btn); 144 mCorpusIndicator = (ImageButton) findViewById(R.id.corpus_indicator); 145 146 mQueryTextView.addTextChangedListener(new SearchTextWatcher()); 147 mQueryTextView.setOnKeyListener(new QueryTextViewKeyListener()); 148 mQueryTextView.setOnFocusChangeListener(new QueryTextViewFocusListener()); 149 mQueryTextView.setSuggestionClickListener(new ClickHandler()); 150 mQueryTextEmptyBg = mQueryTextView.getBackground(); 151 152 mCorpusIndicator.setOnClickListener(new CorpusIndicatorClickListener()); 153 154 mSearchGoButton.setOnClickListener(new SearchGoButtonClickListener()); 155 156 mVoiceSearchButton.setOnClickListener(new VoiceSearchButtonClickListener()); 157 158 ButtonsKeyListener buttonsKeyListener = new ButtonsKeyListener(); 159 mSearchGoButton.setOnKeyListener(buttonsKeyListener); 160 mVoiceSearchButton.setOnKeyListener(buttonsKeyListener); 161 mCorpusIndicator.setOnKeyListener(buttonsKeyListener); 162 163 mUpdateSuggestions = true; 164 165 // First get setup from intent 166 Intent intent = getIntent(); 167 setupFromIntent(intent); 168 // Then restore any saved instance state 169 restoreInstanceState(savedInstanceState); 170 171 mSuggestionsAdapter.registerDataSetObserver(new SuggestionsObserver()); 172 173 // Do this at the end, to avoid updating the list view when setSource() 174 // is called. 175 mSuggestionsView.setAdapter(mSuggestionsAdapter); 176 177 mCorporaObserver = new CorporaObserver(); 178 getCorpora().registerDataSetObserver(mCorporaObserver); 179 } 180 181 protected void setContentView() { 182 setContentView(R.layout.search_activity); 183 } 184 185 private void startMethodTracing() { 186 File traceDir = getDir("traces", 0); 187 String traceFile = new File(traceDir, "qsb.trace").getAbsolutePath(); 188 Debug.startMethodTracing(traceFile); 189 } 190 191 @Override 192 protected void onNewIntent(Intent intent) { 193 if (DBG) Log.d(TAG, "onNewIntent()"); 194 recordStartTime(); 195 setIntent(intent); 196 setupFromIntent(intent); 197 } 198 199 private void recordStartTime() { 200 mStartLatencyTracker = new LatencyTracker(); 201 mStarting = true; 202 mTookAction = false; 203 } 204 205 protected void restoreInstanceState(Bundle savedInstanceState) { 206 if (savedInstanceState == null) return; 207 String corpusName = savedInstanceState.getString(INSTANCE_KEY_CORPUS); 208 String query = savedInstanceState.getString(INSTANCE_KEY_QUERY); 209 setCorpus(corpusName); 210 setQuery(query, false); 211 } 212 213 @Override 214 protected void onSaveInstanceState(Bundle outState) { 215 super.onSaveInstanceState(outState); 216 // We don't save appSearchData, since we always get the value 217 // from the intent and the user can't change it. 218 219 outState.putString(INSTANCE_KEY_CORPUS, getCorpusName()); 220 outState.putString(INSTANCE_KEY_QUERY, getQuery()); 221 } 222 223 private void setupFromIntent(Intent intent) { 224 if (DBG) Log.d(TAG, "setupFromIntent(" + intent.toUri(0) + ")"); 225 String corpusName = getCorpusNameFromUri(intent.getData()); 226 String query = intent.getStringExtra(SearchManager.QUERY); 227 Bundle appSearchData = intent.getBundleExtra(SearchManager.APP_DATA); 228 boolean selectAll = intent.getBooleanExtra(SearchManager.EXTRA_SELECT_QUERY, false); 229 230 setCorpus(corpusName); 231 setQuery(query, selectAll); 232 mAppSearchData = appSearchData; 233 234 if (startedIntoCorpusSelectionDialog()) { 235 showCorpusSelectionDialog(); 236 } 237 } 238 239 public boolean startedIntoCorpusSelectionDialog() { 240 return INTENT_ACTION_QSB_AND_SELECT_CORPUS.equals(getIntent().getAction()); 241 } 242 243 /** 244 * Removes corpus selector intent action, so that BACK works normally after 245 * dismissing and reopening the corpus selector. 246 */ 247 private void clearStartedIntoCorpusSelectionDialog() { 248 Intent oldIntent = getIntent(); 249 if (SearchActivity.INTENT_ACTION_QSB_AND_SELECT_CORPUS.equals(oldIntent.getAction())) { 250 Intent newIntent = new Intent(oldIntent); 251 newIntent.setAction(SearchManager.INTENT_ACTION_GLOBAL_SEARCH); 252 setIntent(newIntent); 253 } 254 } 255 256 public static Uri getCorpusUri(Corpus corpus) { 257 if (corpus == null) return null; 258 return new Uri.Builder() 259 .scheme(SCHEME_CORPUS) 260 .authority(corpus.getName()) 261 .build(); 262 } 263 264 private String getCorpusNameFromUri(Uri uri) { 265 if (uri == null) return null; 266 if (!SCHEME_CORPUS.equals(uri.getScheme())) return null; 267 return uri.getAuthority(); 268 } 269 270 private Corpus getCorpus(String sourceName) { 271 if (sourceName == null) return null; 272 Corpus corpus = getCorpora().getCorpus(sourceName); 273 if (corpus == null) { 274 Log.w(TAG, "Unknown corpus " + sourceName); 275 return null; 276 } 277 return corpus; 278 } 279 280 private void setCorpus(String corpusName) { 281 if (DBG) Log.d(TAG, "setCorpus(" + corpusName + ")"); 282 mCorpus = getCorpus(corpusName); 283 Drawable sourceIcon; 284 if (mCorpus == null) { 285 sourceIcon = getCorpusViewFactory().getGlobalSearchIcon(); 286 } else { 287 sourceIcon = mCorpus.getCorpusIcon(); 288 } 289 mSuggestionsAdapter.setCorpus(mCorpus); 290 mCorpusIndicator.setImageDrawable(sourceIcon); 291 292 updateUi(getQuery().length() == 0); 293 } 294 295 private String getCorpusName() { 296 return mCorpus == null ? null : mCorpus.getName(); 297 } 298 299 private QsbApplication getQsbApplication() { 300 return QsbApplication.get(this); 301 } 302 303 private Config getConfig() { 304 return getQsbApplication().getConfig(); 305 } 306 307 private Corpora getCorpora() { 308 return getQsbApplication().getCorpora(); 309 } 310 311 private ShortcutRepository getShortcutRepository() { 312 return getQsbApplication().getShortcutRepository(); 313 } 314 315 private SuggestionsProvider getSuggestionsProvider() { 316 return getQsbApplication().getSuggestionsProvider(); 317 } 318 319 private CorpusViewFactory getCorpusViewFactory() { 320 return getQsbApplication().getCorpusViewFactory(); 321 } 322 323 private VoiceSearch getVoiceSearch() { 324 return QsbApplication.get(this).getVoiceSearch(); 325 } 326 327 private Logger getLogger() { 328 return getQsbApplication().getLogger(); 329 } 330 331 @Override 332 protected void onDestroy() { 333 if (DBG) Log.d(TAG, "onDestroy()"); 334 super.onDestroy(); 335 getCorpora().unregisterDataSetObserver(mCorporaObserver); 336 mSuggestionsView.setAdapter(null); // closes mSuggestionsAdapter 337 } 338 339 @Override 340 protected void onStop() { 341 if (DBG) Log.d(TAG, "onStop()"); 342 if (!mTookAction) { 343 // TODO: This gets logged when starting other activities, e.g. by opening he search 344 // settings, or clicking a notification in the status bar. 345 getLogger().logExit(getCurrentSuggestions(), getQuery().length()); 346 } 347 // Close all open suggestion cursors. The query will be redone in onResume() 348 // if we come back to this activity. 349 mSuggestionsAdapter.setSuggestions(null); 350 getQsbApplication().getShortcutRefresher().reset(); 351 dismissCorpusSelectionDialog(); 352 super.onStop(); 353 } 354 355 @Override 356 protected void onRestart() { 357 if (DBG) Log.d(TAG, "onRestart()"); 358 super.onRestart(); 359 } 360 361 @Override 362 protected void onResume() { 363 if (DBG) Log.d(TAG, "onResume()"); 364 super.onResume(); 365 updateSuggestionsBuffered(); 366 if (!isCorpusSelectionDialogShowing()) { 367 mQueryTextView.requestFocus(); 368 } 369 if (TRACE) Debug.stopMethodTracing(); 370 } 371 372 @Override 373 public boolean onCreateOptionsMenu(Menu menu) { 374 super.onCreateOptionsMenu(menu); 375 SearchSettings.addSearchSettingsMenuItem(this, menu); 376 return true; 377 } 378 379 @Override 380 public void onWindowFocusChanged(boolean hasFocus) { 381 super.onWindowFocusChanged(hasFocus); 382 if (hasFocus) { 383 // Launch the IME after a bit 384 mHandler.postDelayed(mShowInputMethodTask, 0); 385 } 386 } 387 388 protected String getQuery() { 389 CharSequence q = mQueryTextView.getText(); 390 return q == null ? "" : q.toString(); 391 } 392 393 /** 394 * Sets the text in the query box. Does not update the suggestions. 395 */ 396 private void setQuery(String query, boolean selectAll) { 397 mUpdateSuggestions = false; 398 mQueryTextView.setText(query); 399 mQueryTextView.setTextSelection(selectAll); 400 mUpdateSuggestions = true; 401 } 402 403 protected void updateUi(boolean queryEmpty) { 404 updateQueryTextView(queryEmpty); 405 updateSearchGoButton(queryEmpty); 406 updateVoiceSearchButton(queryEmpty); 407 } 408 409 private void updateQueryTextView(boolean queryEmpty) { 410 if (queryEmpty) { 411 if (isSearchCorpusWeb()) { 412 mQueryTextView.setBackgroundDrawable(mQueryTextEmptyBg); 413 mQueryTextView.setHint(null); 414 } else { 415 if (mQueryTextNotEmptyBg == null) { 416 mQueryTextNotEmptyBg = 417 getResources().getDrawable(R.drawable.textfield_search_empty); 418 } 419 mQueryTextView.setBackgroundDrawable(mQueryTextNotEmptyBg); 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 Logger.SUGGESTION_CLICK_TYPE_LAUNCH); 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 void clickedQuickContact(int position) { 595 SuggestionCursor suggestions = getCurrentSuggestions(position); 596 if (suggestions == null) return; 597 598 if (DBG) Log.d(TAG, "Used suggestion " + position); 599 mTookAction = true; 600 601 // Log suggestion click 602 getLogger().logSuggestionClick(position, suggestions, getCurrentIncludedCorpora(), 603 Logger.SUGGESTION_CLICK_TYPE_QUICK_CONTACT); 604 605 // Create shortcut 606 getShortcutRepository().reportClick(suggestions, position); 607 } 608 609 protected boolean onSuggestionLongClicked(int position) { 610 if (DBG) Log.d(TAG, "Long clicked on suggestion " + position); 611 return false; 612 } 613 614 protected boolean onSuggestionKeyDown(int position, int keyCode, KeyEvent event) { 615 // Treat enter or search as a click 616 if ( keyCode == KeyEvent.KEYCODE_ENTER 617 || keyCode == KeyEvent.KEYCODE_SEARCH 618 || keyCode == KeyEvent.KEYCODE_DPAD_CENTER) { 619 return launchSuggestion(position); 620 } 621 622 return false; 623 } 624 625 protected void refineSuggestion(int position) { 626 if (DBG) Log.d(TAG, "query refine clicked, pos " + position); 627 SuggestionCursor suggestions = getCurrentSuggestions(position); 628 if (suggestions == null) { 629 return; 630 } 631 String query = suggestions.getSuggestionQuery(); 632 if (TextUtils.isEmpty(query)) { 633 return; 634 } 635 636 // Log refine click 637 getLogger().logSuggestionClick(position, suggestions, getCurrentIncludedCorpora(), 638 Logger.SUGGESTION_CLICK_TYPE_REFINE); 639 640 // Put query + space in query text view 641 String queryWithSpace = query + ' '; 642 setQuery(queryWithSpace, false); 643 updateSuggestions(queryWithSpace); 644 mQueryTextView.requestFocus(); 645 } 646 647 protected int getSelectedPosition() { 648 return mSuggestionsView.getSelectedPosition(); 649 } 650 651 /** 652 * Hides the input method. 653 */ 654 protected void hideInputMethod() { 655 mQueryTextView.hideInputMethod(); 656 } 657 658 protected void showInputMethodForQuery() { 659 mQueryTextView.showInputMethod(); 660 } 661 662 protected void onSuggestionListFocusChange(boolean focused) { 663 } 664 665 protected void onQueryTextViewFocusChange(boolean focused) { 666 } 667 668 /** 669 * Hides the input method when the suggestions get focus. 670 */ 671 private class SuggestListFocusListener implements OnFocusChangeListener { 672 public void onFocusChange(View v, boolean focused) { 673 if (DBG) Log.d(TAG, "Suggestions focus change, now: " + focused); 674 if (focused) { 675 // The suggestions list got focus, hide the input method 676 hideInputMethod(); 677 } 678 onSuggestionListFocusChange(focused); 679 } 680 } 681 682 private class QueryTextViewFocusListener implements OnFocusChangeListener { 683 public void onFocusChange(View v, boolean focused) { 684 if (DBG) Log.d(TAG, "Query focus change, now: " + focused); 685 if (focused) { 686 // The query box got focus, show the input method 687 showInputMethodForQuery(); 688 } 689 onQueryTextViewFocusChange(focused); 690 } 691 } 692 693 private int getMaxSuggestions() { 694 Config config = getConfig(); 695 return mCorpus == null 696 ? config.getMaxPromotedSuggestions() 697 : config.getMaxResultsPerSource(); 698 } 699 700 private void updateSuggestionsBuffered() { 701 mHandler.removeCallbacks(mUpdateSuggestionsTask); 702 long delay = getConfig().getTypingUpdateSuggestionsDelayMillis(); 703 mHandler.postDelayed(mUpdateSuggestionsTask, delay); 704 } 705 706 protected void updateSuggestions(String query) { 707 708 query = CharMatcher.WHITESPACE.trimLeadingFrom(query); 709 if (DBG) Log.d(TAG, "getSuggestions(\""+query+"\","+mCorpus + ","+getMaxSuggestions()+")"); 710 Suggestions suggestions = getSuggestionsProvider().getSuggestions( 711 query, mCorpus, getMaxSuggestions()); 712 713 // Log start latency if this is the first suggestions update 714 if (mStarting) { 715 mStarting = false; 716 String source = getIntent().getStringExtra(Search.SOURCE); 717 int latency = mStartLatencyTracker.getLatency(); 718 getLogger().logStart(latency, source, mCorpus, suggestions.getExpectedCorpora()); 719 getQsbApplication().onStartupComplete(); 720 } 721 722 mSuggestionsAdapter.setSuggestions(suggestions); 723 } 724 725 /** 726 * If the input method is in fullscreen mode, and the selector corpus 727 * is All or Web, use the web search suggestions as completions. 728 */ 729 protected void updateInputMethodSuggestions() { 730 InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); 731 if (imm == null || !imm.isFullscreenMode()) return; 732 Suggestions suggestions = mSuggestionsAdapter.getSuggestions(); 733 if (suggestions == null) return; 734 SuggestionCursor cursor = suggestions.getPromoted(); 735 if (cursor == null) return; 736 CompletionInfo[] completions = webSuggestionsToCompletions(cursor); 737 if (DBG) Log.d(TAG, "displayCompletions(" + Arrays.toString(completions) + ")"); 738 imm.displayCompletions(mQueryTextView, completions); 739 } 740 741 private CompletionInfo[] webSuggestionsToCompletions(SuggestionCursor cursor) { 742 int count = cursor.getCount(); 743 ArrayList<CompletionInfo> completions = new ArrayList<CompletionInfo>(count); 744 boolean usingWebCorpus = isSearchCorpusWeb(); 745 for (int i = 0; i < count; i++) { 746 cursor.moveTo(i); 747 if (!usingWebCorpus || cursor.isWebSearchSuggestion()) { 748 String text1 = cursor.getSuggestionText1(); 749 completions.add(new CompletionInfo(i, i, text1)); 750 } 751 } 752 return completions.toArray(new CompletionInfo[completions.size()]); 753 } 754 755 private boolean forwardKeyToQueryTextView(int keyCode, KeyEvent event) { 756 if (!event.isSystem() && !isDpadKey(keyCode)) { 757 if (DBG) Log.d(TAG, "Forwarding key to query box: " + event); 758 if (mQueryTextView.requestFocus()) { 759 return mQueryTextView.dispatchKeyEvent(event); 760 } 761 } 762 return false; 763 } 764 765 private boolean isDpadKey(int keyCode) { 766 switch (keyCode) { 767 case KeyEvent.KEYCODE_DPAD_UP: 768 case KeyEvent.KEYCODE_DPAD_DOWN: 769 case KeyEvent.KEYCODE_DPAD_LEFT: 770 case KeyEvent.KEYCODE_DPAD_RIGHT: 771 case KeyEvent.KEYCODE_DPAD_CENTER: 772 return true; 773 default: 774 return false; 775 } 776 } 777 778 /** 779 * Filters the suggestions list when the search text changes. 780 */ 781 private class SearchTextWatcher implements TextWatcher { 782 public void afterTextChanged(Editable s) { 783 boolean empty = s.length() == 0; 784 if (empty != mQueryWasEmpty) { 785 mQueryWasEmpty = empty; 786 updateUi(empty); 787 } 788 if (mUpdateSuggestions) { 789 updateSuggestionsBuffered(); 790 } 791 } 792 793 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 794 } 795 796 public void onTextChanged(CharSequence s, int start, int before, int count) { 797 } 798 } 799 800 /** 801 * Handles non-text keys in the query text view. 802 */ 803 private class QueryTextViewKeyListener implements View.OnKeyListener { 804 public boolean onKey(View view, int keyCode, KeyEvent event) { 805 // Handle IME search action key 806 if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_UP) { 807 // if no action was taken, consume the key event so that the keyboard 808 // remains on screen. 809 return !onSearchClicked(Logger.SEARCH_METHOD_KEYBOARD); 810 } 811 return false; 812 } 813 } 814 815 /** 816 * Handles key events on the search and voice search buttons, 817 * by refocusing to EditText. 818 */ 819 private class ButtonsKeyListener implements View.OnKeyListener { 820 public boolean onKey(View v, int keyCode, KeyEvent event) { 821 return forwardKeyToQueryTextView(keyCode, event); 822 } 823 } 824 825 /** 826 * Handles key events on the suggestions list view. 827 */ 828 private class SuggestionsViewKeyListener implements View.OnKeyListener { 829 public boolean onKey(View v, int keyCode, KeyEvent event) { 830 if (event.getAction() == KeyEvent.ACTION_DOWN) { 831 int position = getSelectedPosition(); 832 if (onSuggestionKeyDown(position, keyCode, event)) { 833 return true; 834 } 835 } 836 return forwardKeyToQueryTextView(keyCode, event); 837 } 838 } 839 840 private class InputMethodCloser implements SuggestionsView.OnScrollListener { 841 842 public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, 843 int totalItemCount) { 844 } 845 846 public void onScrollStateChanged(AbsListView view, int scrollState) { 847 hideInputMethod(); 848 } 849 } 850 851 private class ClickHandler implements SuggestionClickListener { 852 public void onSuggestionClicked(int position) { 853 launchSuggestion(position); 854 } 855 856 public void onSuggestionQuickContactClicked(int position) { 857 clickedQuickContact(position); 858 } 859 860 public boolean onSuggestionLongClicked(int position) { 861 return SearchActivity.this.onSuggestionLongClicked(position); 862 } 863 864 public void onSuggestionQueryRefineClicked(int position) { 865 refineSuggestion(position); 866 } 867 } 868 869 /** 870 * Listens for clicks on the source selector. 871 */ 872 private class SearchGoButtonClickListener implements View.OnClickListener { 873 public void onClick(View view) { 874 onSearchClicked(Logger.SEARCH_METHOD_BUTTON); 875 } 876 } 877 878 /** 879 * Listens for clicks on the search button. 880 */ 881 private class CorpusIndicatorClickListener implements View.OnClickListener { 882 public void onClick(View view) { 883 showCorpusSelectionDialog(); 884 } 885 } 886 887 private class CorpusSelectorDismissListener implements DialogInterface.OnDismissListener { 888 public void onDismiss(DialogInterface dialog) { 889 if (DBG) Log.d(TAG, "Corpus selector dismissed"); 890 clearStartedIntoCorpusSelectionDialog(); 891 } 892 } 893 894 private class CorpusSelectionListener 895 implements CorpusSelectionDialog.OnCorpusSelectedListener { 896 public void onCorpusSelected(String corpusName) { 897 setCorpus(corpusName); 898 updateSuggestions(getQuery()); 899 mQueryTextView.requestFocus(); 900 showInputMethodForQuery(); 901 } 902 } 903 904 /** 905 * Listens for clicks on the voice search button. 906 */ 907 private class VoiceSearchButtonClickListener implements View.OnClickListener { 908 public void onClick(View view) { 909 onVoiceSearchClicked(); 910 } 911 } 912 913 private class CorporaObserver extends DataSetObserver { 914 @Override 915 public void onChanged() { 916 setCorpus(getCorpusName()); 917 updateSuggestions(getQuery()); 918 } 919 } 920 921 private class SuggestionsObserver extends DataSetObserver { 922 @Override 923 public void onChanged() { 924 updateInputMethodSuggestions(); 925 } 926 } 927 928} 929