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