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