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 17 package com.android.quicksearchbox; 18 19 import com.android.common.Search; 20 import com.android.quicksearchbox.ui.CorpusViewFactory; 21 import com.android.quicksearchbox.ui.QueryTextView; 22 import com.android.quicksearchbox.ui.SuggestionClickListener; 23 import com.android.quicksearchbox.ui.SuggestionsAdapter; 24 import com.android.quicksearchbox.ui.SuggestionsView; 25 import com.google.common.base.CharMatcher; 26 27 import android.app.Activity; 28 import android.app.SearchManager; 29 import android.content.DialogInterface; 30 import android.content.Intent; 31 import android.database.DataSetObserver; 32 import android.graphics.drawable.Drawable; 33 import android.net.Uri; 34 import android.os.Bundle; 35 import android.os.Debug; 36 import android.os.Handler; 37 import android.text.Editable; 38 import android.text.TextUtils; 39 import android.text.TextWatcher; 40 import android.util.Log; 41 import android.view.KeyEvent; 42 import android.view.Menu; 43 import android.view.View; 44 import android.view.View.OnFocusChangeListener; 45 import android.view.inputmethod.CompletionInfo; 46 import android.view.inputmethod.InputMethodManager; 47 import android.widget.AbsListView; 48 import android.widget.ImageButton; 49 50 import java.io.File; 51 import java.util.ArrayList; 52 import java.util.Arrays; 53 import java.util.Set; 54 55 /** 56 * The main activity for Quick Search Box. Shows the search UI. 57 * 58 */ 59 public 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 = createCorpusSelectionDialog(); 448 mCorpusSelectionDialog.setOwnerActivity(this); 449 mCorpusSelectionDialog.setOnDismissListener(new CorpusSelectorDismissListener()); 450 mCorpusSelectionDialog.setOnCorpusSelectedListener(new CorpusSelectionListener()); 451 } 452 mCorpusSelectionDialog.show(mCorpus); 453 } 454 455 protected CorpusSelectionDialog createCorpusSelectionDialog() { 456 return new CorpusSelectionDialog(this); 457 } 458 459 protected boolean isCorpusSelectionDialogShowing() { 460 return mCorpusSelectionDialog != null && mCorpusSelectionDialog.isShowing(); 461 } 462 463 protected void dismissCorpusSelectionDialog() { 464 if (mCorpusSelectionDialog != null) { 465 mCorpusSelectionDialog.dismiss(); 466 } 467 } 468 469 /** 470 * @return true if a search was performed as a result of this click, false otherwise. 471 */ 472 protected boolean onSearchClicked(int method) { 473 String query = CharMatcher.WHITESPACE.trimAndCollapseFrom(getQuery(), ' '); 474 if (DBG) Log.d(TAG, "Search clicked, query=" + query); 475 476 // Don't do empty queries 477 if (TextUtils.getTrimmedLength(query) == 0) return false; 478 479 Corpus searchCorpus = getSearchCorpus(); 480 if (searchCorpus == null) return false; 481 482 mTookAction = true; 483 484 // Log search start 485 getLogger().logSearch(mCorpus, method, query.length()); 486 487 // Create shortcut 488 SuggestionData searchShortcut = searchCorpus.createSearchShortcut(query); 489 if (searchShortcut != null) { 490 ListSuggestionCursor cursor = new ListSuggestionCursor(query); 491 cursor.add(searchShortcut); 492 getShortcutRepository().reportClick(cursor, 0); 493 } 494 495 // Start search 496 Intent intent = searchCorpus.createSearchIntent(query, mAppSearchData); 497 launchIntent(intent); 498 return true; 499 } 500 501 protected void onVoiceSearchClicked() { 502 if (DBG) Log.d(TAG, "Voice Search clicked"); 503 Corpus searchCorpus = getSearchCorpus(); 504 if (searchCorpus == null) return; 505 506 mTookAction = true; 507 508 // Log voice search start 509 getLogger().logVoiceSearch(searchCorpus); 510 511 // Start voice search 512 Intent intent = searchCorpus.createVoiceSearchIntent(mAppSearchData); 513 launchIntent(intent); 514 } 515 516 /** 517 * Gets the corpus to use for any searches. This is the web corpus in "All" mode, 518 * and the selected corpus otherwise. 519 */ 520 protected Corpus getSearchCorpus() { 521 if (mCorpus != null) { 522 return mCorpus; 523 } else { 524 Corpus webCorpus = getCorpora().getWebCorpus(); 525 if (webCorpus == null) { 526 Log.e(TAG, "No web corpus"); 527 } 528 return webCorpus; 529 } 530 } 531 532 /** 533 * Checks if the corpus used for typed searchs is the web corpus. 534 */ 535 protected boolean isSearchCorpusWeb() { 536 Corpus corpus = getSearchCorpus(); 537 return corpus != null && corpus.isWebCorpus(); 538 } 539 540 protected SuggestionCursor getCurrentSuggestions() { 541 return mSuggestionsAdapter.getCurrentSuggestions(); 542 } 543 544 protected SuggestionCursor getCurrentSuggestions(int position) { 545 SuggestionCursor suggestions = getCurrentSuggestions(); 546 if (suggestions == null) { 547 return null; 548 } 549 int count = suggestions.getCount(); 550 if (position < 0 || position >= count) { 551 Log.w(TAG, "Invalid suggestion position " + position + ", count = " + count); 552 return null; 553 } 554 suggestions.moveTo(position); 555 return suggestions; 556 } 557 558 protected Set<Corpus> getCurrentIncludedCorpora() { 559 Suggestions suggestions = mSuggestionsAdapter.getSuggestions(); 560 return suggestions == null ? null : suggestions.getIncludedCorpora(); 561 } 562 563 protected void launchIntent(Intent intent) { 564 if (intent == null) { 565 return; 566 } 567 try { 568 startActivity(intent); 569 } catch (RuntimeException ex) { 570 // Since the intents for suggestions specified by suggestion providers, 571 // guard against them not being handled, not allowed, etc. 572 Log.e(TAG, "Failed to start " + intent.toUri(0), ex); 573 } 574 } 575 576 protected boolean launchSuggestion(int position) { 577 SuggestionCursor suggestions = getCurrentSuggestions(position); 578 if (suggestions == null) return false; 579 580 if (DBG) Log.d(TAG, "Launching suggestion " + position); 581 mTookAction = true; 582 583 // Log suggestion click 584 getLogger().logSuggestionClick(position, suggestions, getCurrentIncludedCorpora(), 585 Logger.SUGGESTION_CLICK_TYPE_LAUNCH); 586 587 // Create shortcut 588 getShortcutRepository().reportClick(suggestions, position); 589 590 // Launch intent 591 suggestions.moveTo(position); 592 Intent intent = SuggestionUtils.getSuggestionIntent(suggestions, mAppSearchData); 593 launchIntent(intent); 594 595 return true; 596 } 597 598 protected void clickedQuickContact(int position) { 599 SuggestionCursor suggestions = getCurrentSuggestions(position); 600 if (suggestions == null) return; 601 602 if (DBG) Log.d(TAG, "Used suggestion " + position); 603 mTookAction = true; 604 605 // Log suggestion click 606 getLogger().logSuggestionClick(position, suggestions, getCurrentIncludedCorpora(), 607 Logger.SUGGESTION_CLICK_TYPE_QUICK_CONTACT); 608 609 // Create shortcut 610 getShortcutRepository().reportClick(suggestions, position); 611 } 612 613 protected boolean onSuggestionLongClicked(int position) { 614 if (DBG) Log.d(TAG, "Long clicked on suggestion " + position); 615 return false; 616 } 617 618 protected boolean onSuggestionKeyDown(int position, int keyCode, KeyEvent event) { 619 // Treat enter or search as a click 620 if ( keyCode == KeyEvent.KEYCODE_ENTER 621 || keyCode == KeyEvent.KEYCODE_SEARCH 622 || keyCode == KeyEvent.KEYCODE_DPAD_CENTER) { 623 return launchSuggestion(position); 624 } 625 626 return false; 627 } 628 629 protected void refineSuggestion(int position) { 630 if (DBG) Log.d(TAG, "query refine clicked, pos " + position); 631 SuggestionCursor suggestions = getCurrentSuggestions(position); 632 if (suggestions == null) { 633 return; 634 } 635 String query = suggestions.getSuggestionQuery(); 636 if (TextUtils.isEmpty(query)) { 637 return; 638 } 639 640 // Log refine click 641 getLogger().logSuggestionClick(position, suggestions, getCurrentIncludedCorpora(), 642 Logger.SUGGESTION_CLICK_TYPE_REFINE); 643 644 // Put query + space in query text view 645 String queryWithSpace = query + ' '; 646 setQuery(queryWithSpace, false); 647 updateSuggestions(queryWithSpace); 648 mQueryTextView.requestFocus(); 649 } 650 651 protected int getSelectedPosition() { 652 return mSuggestionsView.getSelectedPosition(); 653 } 654 655 /** 656 * Hides the input method. 657 */ 658 protected void hideInputMethod() { 659 mQueryTextView.hideInputMethod(); 660 } 661 662 protected void showInputMethodForQuery() { 663 mQueryTextView.showInputMethod(); 664 } 665 666 protected void onSuggestionListFocusChange(boolean focused) { 667 } 668 669 protected void onQueryTextViewFocusChange(boolean focused) { 670 } 671 672 /** 673 * Hides the input method when the suggestions get focus. 674 */ 675 private class SuggestListFocusListener implements OnFocusChangeListener { 676 public void onFocusChange(View v, boolean focused) { 677 if (DBG) Log.d(TAG, "Suggestions focus change, now: " + focused); 678 if (focused) { 679 // The suggestions list got focus, hide the input method 680 hideInputMethod(); 681 } 682 onSuggestionListFocusChange(focused); 683 } 684 } 685 686 private class QueryTextViewFocusListener implements OnFocusChangeListener { 687 public void onFocusChange(View v, boolean focused) { 688 if (DBG) Log.d(TAG, "Query focus change, now: " + focused); 689 if (focused) { 690 // The query box got focus, show the input method 691 showInputMethodForQuery(); 692 } 693 onQueryTextViewFocusChange(focused); 694 } 695 } 696 697 private int getMaxSuggestions() { 698 Config config = getConfig(); 699 return mCorpus == null 700 ? config.getMaxPromotedSuggestions() 701 : config.getMaxResultsPerSource(); 702 } 703 704 private void updateSuggestionsBuffered() { 705 mHandler.removeCallbacks(mUpdateSuggestionsTask); 706 long delay = getConfig().getTypingUpdateSuggestionsDelayMillis(); 707 mHandler.postDelayed(mUpdateSuggestionsTask, delay); 708 } 709 710 protected void updateSuggestions(String query) { 711 712 query = CharMatcher.WHITESPACE.trimLeadingFrom(query); 713 if (DBG) Log.d(TAG, "getSuggestions(\""+query+"\","+mCorpus + ","+getMaxSuggestions()+")"); 714 Suggestions suggestions = getSuggestionsProvider().getSuggestions( 715 query, mCorpus, getMaxSuggestions()); 716 717 // Log start latency if this is the first suggestions update 718 if (mStarting) { 719 mStarting = false; 720 String source = getIntent().getStringExtra(Search.SOURCE); 721 int latency = mStartLatencyTracker.getLatency(); 722 getLogger().logStart(latency, source, mCorpus, suggestions.getExpectedCorpora()); 723 getQsbApplication().onStartupComplete(); 724 } 725 726 mSuggestionsAdapter.setSuggestions(suggestions); 727 } 728 729 /** 730 * If the input method is in fullscreen mode, and the selector corpus 731 * is All or Web, use the web search suggestions as completions. 732 */ 733 protected void updateInputMethodSuggestions() { 734 InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); 735 if (imm == null || !imm.isFullscreenMode()) return; 736 Suggestions suggestions = mSuggestionsAdapter.getSuggestions(); 737 if (suggestions == null) return; 738 SuggestionCursor cursor = suggestions.getPromoted(); 739 if (cursor == null) return; 740 CompletionInfo[] completions = webSuggestionsToCompletions(cursor); 741 if (DBG) Log.d(TAG, "displayCompletions(" + Arrays.toString(completions) + ")"); 742 imm.displayCompletions(mQueryTextView, completions); 743 } 744 745 private CompletionInfo[] webSuggestionsToCompletions(SuggestionCursor cursor) { 746 int count = cursor.getCount(); 747 ArrayList<CompletionInfo> completions = new ArrayList<CompletionInfo>(count); 748 boolean usingWebCorpus = isSearchCorpusWeb(); 749 for (int i = 0; i < count; i++) { 750 cursor.moveTo(i); 751 if (!usingWebCorpus || cursor.isWebSearchSuggestion()) { 752 String text1 = cursor.getSuggestionText1(); 753 completions.add(new CompletionInfo(i, i, text1)); 754 } 755 } 756 return completions.toArray(new CompletionInfo[completions.size()]); 757 } 758 759 private boolean forwardKeyToQueryTextView(int keyCode, KeyEvent event) { 760 if (!event.isSystem() && !isDpadKey(keyCode)) { 761 if (DBG) Log.d(TAG, "Forwarding key to query box: " + event); 762 if (mQueryTextView.requestFocus()) { 763 return mQueryTextView.dispatchKeyEvent(event); 764 } 765 } 766 return false; 767 } 768 769 private boolean isDpadKey(int keyCode) { 770 switch (keyCode) { 771 case KeyEvent.KEYCODE_DPAD_UP: 772 case KeyEvent.KEYCODE_DPAD_DOWN: 773 case KeyEvent.KEYCODE_DPAD_LEFT: 774 case KeyEvent.KEYCODE_DPAD_RIGHT: 775 case KeyEvent.KEYCODE_DPAD_CENTER: 776 return true; 777 default: 778 return false; 779 } 780 } 781 782 /** 783 * Filters the suggestions list when the search text changes. 784 */ 785 private class SearchTextWatcher implements TextWatcher { 786 public void afterTextChanged(Editable s) { 787 boolean empty = s.length() == 0; 788 if (empty != mQueryWasEmpty) { 789 mQueryWasEmpty = empty; 790 updateUi(empty); 791 } 792 if (mUpdateSuggestions) { 793 updateSuggestionsBuffered(); 794 } 795 } 796 797 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 798 } 799 800 public void onTextChanged(CharSequence s, int start, int before, int count) { 801 } 802 } 803 804 /** 805 * Handles non-text keys in the query text view. 806 */ 807 private class QueryTextViewKeyListener implements View.OnKeyListener { 808 public boolean onKey(View view, int keyCode, KeyEvent event) { 809 // Handle IME search action key 810 if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_UP) { 811 // if no action was taken, consume the key event so that the keyboard 812 // remains on screen. 813 return !onSearchClicked(Logger.SEARCH_METHOD_KEYBOARD); 814 } 815 return false; 816 } 817 } 818 819 /** 820 * Handles key events on the search and voice search buttons, 821 * by refocusing to EditText. 822 */ 823 private class ButtonsKeyListener implements View.OnKeyListener { 824 public boolean onKey(View v, int keyCode, KeyEvent event) { 825 return forwardKeyToQueryTextView(keyCode, event); 826 } 827 } 828 829 /** 830 * Handles key events on the suggestions list view. 831 */ 832 private class SuggestionsViewKeyListener implements View.OnKeyListener { 833 public boolean onKey(View v, int keyCode, KeyEvent event) { 834 if (event.getAction() == KeyEvent.ACTION_DOWN) { 835 int position = getSelectedPosition(); 836 if (onSuggestionKeyDown(position, keyCode, event)) { 837 return true; 838 } 839 } 840 return forwardKeyToQueryTextView(keyCode, event); 841 } 842 } 843 844 private class InputMethodCloser implements SuggestionsView.OnScrollListener { 845 846 public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, 847 int totalItemCount) { 848 } 849 850 public void onScrollStateChanged(AbsListView view, int scrollState) { 851 hideInputMethod(); 852 } 853 } 854 855 private class ClickHandler implements SuggestionClickListener { 856 public void onSuggestionClicked(int position) { 857 launchSuggestion(position); 858 } 859 860 public void onSuggestionQuickContactClicked(int position) { 861 clickedQuickContact(position); 862 } 863 864 public boolean onSuggestionLongClicked(int position) { 865 return SearchActivity.this.onSuggestionLongClicked(position); 866 } 867 868 public void onSuggestionQueryRefineClicked(int position) { 869 refineSuggestion(position); 870 } 871 } 872 873 /** 874 * Listens for clicks on the source selector. 875 */ 876 private class SearchGoButtonClickListener implements View.OnClickListener { 877 public void onClick(View view) { 878 onSearchClicked(Logger.SEARCH_METHOD_BUTTON); 879 } 880 } 881 882 /** 883 * Listens for clicks on the search button. 884 */ 885 private class CorpusIndicatorClickListener implements View.OnClickListener { 886 public void onClick(View view) { 887 showCorpusSelectionDialog(); 888 } 889 } 890 891 private class CorpusSelectorDismissListener implements DialogInterface.OnDismissListener { 892 public void onDismiss(DialogInterface dialog) { 893 if (DBG) Log.d(TAG, "Corpus selector dismissed"); 894 clearStartedIntoCorpusSelectionDialog(); 895 } 896 } 897 898 private class CorpusSelectionListener 899 implements CorpusSelectionDialog.OnCorpusSelectedListener { 900 public void onCorpusSelected(String corpusName) { 901 setCorpus(corpusName); 902 updateSuggestions(getQuery()); 903 mQueryTextView.requestFocus(); 904 showInputMethodForQuery(); 905 } 906 } 907 908 /** 909 * Listens for clicks on the voice search button. 910 */ 911 private class VoiceSearchButtonClickListener implements View.OnClickListener { 912 public void onClick(View view) { 913 onVoiceSearchClicked(); 914 } 915 } 916 917 private class CorporaObserver extends DataSetObserver { 918 @Override 919 public void onChanged() { 920 setCorpus(getCorpusName()); 921 updateSuggestions(getQuery()); 922 } 923 } 924 925 private class SuggestionsObserver extends DataSetObserver { 926 @Override 927 public void onChanged() { 928 updateInputMethodSuggestions(); 929 } 930 } 931 932 } 933