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