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