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