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