SearchDialog.java revision d2d6014f715f12f6263f61ba3eeb6f8cba6d0fa6
1/* 2 * Copyright (C) 2008 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 android.app; 18 19import static android.app.SuggestionsAdapter.getColumnString; 20 21import android.content.ActivityNotFoundException; 22import android.content.ComponentName; 23import android.content.ContentResolver; 24import android.content.ContentValues; 25import android.content.Context; 26import android.content.Intent; 27import android.content.pm.ActivityInfo; 28import android.content.pm.PackageManager; 29import android.content.pm.ResolveInfo; 30import android.content.pm.PackageManager.NameNotFoundException; 31import android.content.res.Resources; 32import android.database.Cursor; 33import android.graphics.drawable.Animatable; 34import android.graphics.drawable.Drawable; 35import android.net.Uri; 36import android.os.Bundle; 37import android.os.IBinder; 38import android.os.RemoteException; 39import android.os.SystemClock; 40import android.provider.Browser; 41import android.server.search.SearchableInfo; 42import android.speech.RecognizerIntent; 43import android.text.Editable; 44import android.text.InputType; 45import android.text.TextUtils; 46import android.text.TextWatcher; 47import android.text.util.Regex; 48import android.util.AndroidRuntimeException; 49import android.util.AttributeSet; 50import android.util.Log; 51import android.view.ContextThemeWrapper; 52import android.view.Gravity; 53import android.view.KeyEvent; 54import android.view.Menu; 55import android.view.MotionEvent; 56import android.view.View; 57import android.view.ViewConfiguration; 58import android.view.ViewGroup; 59import android.view.Window; 60import android.view.WindowManager; 61import android.view.inputmethod.EditorInfo; 62import android.view.inputmethod.InputMethodManager; 63import android.widget.AdapterView; 64import android.widget.AutoCompleteTextView; 65import android.widget.Button; 66import android.widget.ImageButton; 67import android.widget.ImageView; 68import android.widget.ListView; 69import android.widget.TextView; 70import android.widget.ListAdapter; 71import android.widget.AdapterView.OnItemClickListener; 72import android.widget.AdapterView.OnItemSelectedListener; 73 74import java.util.ArrayList; 75import java.util.WeakHashMap; 76import java.util.concurrent.atomic.AtomicLong; 77 78/** 79 * System search dialog. This is controlled by the 80 * SearchManagerService and runs in the system process. 81 * 82 * @hide 83 */ 84public class SearchDialog extends Dialog implements OnItemClickListener, OnItemSelectedListener { 85 86 // Debugging support 87 private static final boolean DBG = false; 88 private static final String LOG_TAG = "SearchDialog"; 89 private static final boolean DBG_LOG_TIMING = false; 90 91 private static final String INSTANCE_KEY_COMPONENT = "comp"; 92 private static final String INSTANCE_KEY_APPDATA = "data"; 93 private static final String INSTANCE_KEY_GLOBALSEARCH = "glob"; 94 private static final String INSTANCE_KEY_STORED_COMPONENT = "sComp"; 95 private static final String INSTANCE_KEY_STORED_APPDATA = "sData"; 96 private static final String INSTANCE_KEY_PREVIOUS_COMPONENTS = "sPrev"; 97 private static final String INSTANCE_KEY_USER_QUERY = "uQry"; 98 99 // The extra key used in an intent to the speech recognizer for in-app voice search. 100 private static final String EXTRA_CALLING_PACKAGE = "calling_package"; 101 102 private static final int SEARCH_PLATE_LEFT_PADDING_GLOBAL = 12; 103 private static final int SEARCH_PLATE_LEFT_PADDING_NON_GLOBAL = 7; 104 105 // views & widgets 106 private TextView mBadgeLabel; 107 private ImageView mAppIcon; 108 private SearchAutoComplete mSearchAutoComplete; 109 private Button mGoButton; 110 private ImageButton mVoiceButton; 111 private View mSearchPlate; 112 private Drawable mWorkingSpinner; 113 114 // interaction with searchable application 115 private SearchableInfo mSearchable; 116 private ComponentName mLaunchComponent; 117 private Bundle mAppSearchData; 118 private boolean mGlobalSearchMode; 119 private Context mActivityContext; 120 121 // Values we store to allow user to toggle between in-app search and global search. 122 private ComponentName mStoredComponentName; 123 private Bundle mStoredAppSearchData; 124 125 // stack of previous searchables, to support the BACK key after 126 // SearchManager.INTENT_ACTION_CHANGE_SEARCH_SOURCE. 127 // The top of the stack (= previous searchable) is the last element of the list, 128 // since adding and removing is efficient at the end of an ArrayList. 129 private ArrayList<ComponentName> mPreviousComponents; 130 131 // For voice searching 132 private Intent mVoiceWebSearchIntent; 133 private Intent mVoiceAppSearchIntent; 134 135 // support for AutoCompleteTextView suggestions display 136 private SuggestionsAdapter mSuggestionsAdapter; 137 138 // Whether to rewrite queries when selecting suggestions 139 private static final boolean REWRITE_QUERIES = true; 140 141 // The query entered by the user. This is not changed when selecting a suggestion 142 // that modifies the contents of the text field. But if the user then edits 143 // the suggestion, the resulting string is saved. 144 private String mUserQuery; 145 146 // A weak map of drawables we've gotten from other packages, so we don't load them 147 // more than once. 148 private final WeakHashMap<String, Drawable.ConstantState> mOutsideDrawablesCache = 149 new WeakHashMap<String, Drawable.ConstantState>(); 150 151 // Last known IME options value for the search edit text. 152 private int mSearchAutoCompleteImeOptions; 153 154 /** 155 * Constructor - fires it up and makes it look like the search UI. 156 * 157 * @param context Application Context we can use for system acess 158 */ 159 public SearchDialog(Context context) { 160 super(context, com.android.internal.R.style.Theme_GlobalSearchBar); 161 } 162 163 /** 164 * We create the search dialog just once, and it stays around (hidden) 165 * until activated by the user. 166 */ 167 @Override 168 protected void onCreate(Bundle savedInstanceState) { 169 super.onCreate(savedInstanceState); 170 171 setContentView(com.android.internal.R.layout.search_bar); 172 173 Window theWindow = getWindow(); 174 WindowManager.LayoutParams lp = theWindow.getAttributes(); 175 lp.type = WindowManager.LayoutParams.TYPE_SEARCH_BAR; 176 lp.width = ViewGroup.LayoutParams.FILL_PARENT; 177 // taking up the whole window (even when transparent) is less than ideal, 178 // but necessary to show the popup window until the window manager supports 179 // having windows anchored by their parent but not clipped by them. 180 lp.height = ViewGroup.LayoutParams.FILL_PARENT; 181 lp.gravity = Gravity.TOP | Gravity.FILL_HORIZONTAL; 182 lp.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; 183 theWindow.setAttributes(lp); 184 185 // get the view elements for local access 186 mBadgeLabel = (TextView) findViewById(com.android.internal.R.id.search_badge); 187 mSearchAutoComplete = (SearchAutoComplete) 188 findViewById(com.android.internal.R.id.search_src_text); 189 mAppIcon = (ImageView) findViewById(com.android.internal.R.id.search_app_icon); 190 mGoButton = (Button) findViewById(com.android.internal.R.id.search_go_btn); 191 mVoiceButton = (ImageButton) findViewById(com.android.internal.R.id.search_voice_btn); 192 mSearchPlate = findViewById(com.android.internal.R.id.search_plate); 193 mWorkingSpinner = getContext().getResources(). 194 getDrawable(com.android.internal.R.drawable.search_spinner); 195 196 // attach listeners 197 mSearchAutoComplete.addTextChangedListener(mTextWatcher); 198 mSearchAutoComplete.setOnKeyListener(mTextKeyListener); 199 mSearchAutoComplete.setOnItemClickListener(this); 200 mSearchAutoComplete.setOnItemSelectedListener(this); 201 mGoButton.setOnClickListener(mGoButtonClickListener); 202 mGoButton.setOnKeyListener(mButtonsKeyListener); 203 mVoiceButton.setOnClickListener(mVoiceButtonClickListener); 204 mVoiceButton.setOnKeyListener(mButtonsKeyListener); 205 206 mSearchAutoComplete.setSearchDialog(this); 207 208 // pre-hide all the extraneous elements 209 mBadgeLabel.setVisibility(View.GONE); 210 211 // Additional adjustments to make Dialog work for Search 212 213 // Touching outside of the search dialog will dismiss it 214 setCanceledOnTouchOutside(true); 215 216 // Save voice intent for later queries/launching 217 mVoiceWebSearchIntent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH); 218 mVoiceWebSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 219 mVoiceWebSearchIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, 220 RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH); 221 222 mVoiceAppSearchIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); 223 mVoiceAppSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 224 225 mSearchAutoCompleteImeOptions = mSearchAutoComplete.getImeOptions(); 226 } 227 228 /** 229 * Set up the search dialog 230 * 231 * @return true if search dialog launched, false if not 232 */ 233 public boolean show(String initialQuery, boolean selectInitialQuery, 234 ComponentName componentName, Bundle appSearchData, boolean globalSearch) { 235 236 // Reset any stored values from last time dialog was shown. 237 mStoredComponentName = null; 238 mStoredAppSearchData = null; 239 240 boolean success = doShow(initialQuery, selectInitialQuery, componentName, appSearchData, 241 globalSearch); 242 if (success) { 243 // Display the drop down as soon as possible instead of waiting for the rest of the 244 // pending UI stuff to get done, so that things appear faster to the user. 245 mSearchAutoComplete.showDropDownAfterLayout(); 246 } 247 return success; 248 } 249 250 private boolean isInRealAppSearch() { 251 return !mGlobalSearchMode 252 && (mPreviousComponents == null || mPreviousComponents.isEmpty()); 253 } 254 255 /** 256 * Called in response to a press of the hard search button in 257 * {@link #onKeyDown(int, KeyEvent)}, this method toggles between in-app 258 * search and global search when relevant. 259 * 260 * If pressed within an in-app search context, this switches the search dialog out to 261 * global search. If pressed within a global search context that was originally an in-app 262 * search context, this switches back to the in-app search context. If pressed within a 263 * global search context that has no original in-app search context (e.g., global search 264 * from Home), this does nothing. 265 * 266 * @return false if we wanted to toggle context but could not do so successfully, true 267 * in all other cases 268 */ 269 private boolean toggleGlobalSearch() { 270 String currentSearchText = mSearchAutoComplete.getText().toString(); 271 if (!mGlobalSearchMode) { 272 mStoredComponentName = mLaunchComponent; 273 mStoredAppSearchData = mAppSearchData; 274 275 // If this is the browser, we have a special case to not show the icon to the left 276 // of the text field, for extra space for url entry (this should be reconciled in 277 // Eclair). So special case a second tap of the search button to remove any 278 // already-entered text so that we can be sure to show the "Quick Search Box" hint 279 // text to still make it clear to the user that we've jumped out to global search. 280 // 281 // TODO: When the browser icon issue is reconciled in Eclair, remove this special case. 282 if (isBrowserSearch()) currentSearchText = ""; 283 284 return doShow(currentSearchText, false, null, mAppSearchData, true); 285 } else { 286 if (mStoredComponentName != null) { 287 // This means we should toggle *back* to an in-app search context from 288 // global search. 289 return doShow(currentSearchText, false, mStoredComponentName, 290 mStoredAppSearchData, false); 291 } else { 292 return true; 293 } 294 } 295 } 296 297 /** 298 * Does the rest of the work required to show the search dialog. Called by both 299 * {@link #show(String, boolean, ComponentName, Bundle, boolean)} and 300 * {@link #toggleGlobalSearch()}. 301 * 302 * @return true if search dialog showed, false if not 303 */ 304 private boolean doShow(String initialQuery, boolean selectInitialQuery, 305 ComponentName componentName, Bundle appSearchData, 306 boolean globalSearch) { 307 // set up the searchable and show the dialog 308 if (!show(componentName, appSearchData, globalSearch)) { 309 return false; 310 } 311 312 // finally, load the user's initial text (which may trigger suggestions) 313 setUserQuery(initialQuery); 314 if (selectInitialQuery) { 315 mSearchAutoComplete.selectAll(); 316 } 317 318 return true; 319 } 320 321 /** 322 * Sets up the search dialog and shows it. 323 * 324 * @return <code>true</code> if search dialog launched 325 */ 326 private boolean show(ComponentName componentName, Bundle appSearchData, 327 boolean globalSearch) { 328 329 if (DBG) { 330 Log.d(LOG_TAG, "show(" + componentName + ", " 331 + appSearchData + ", " + globalSearch + ")"); 332 } 333 334 SearchManager searchManager = (SearchManager) 335 mContext.getSystemService(Context.SEARCH_SERVICE); 336 // Try to get the searchable info for the provided component (or for global search, 337 // if globalSearch == true). 338 mSearchable = searchManager.getSearchableInfo(componentName, globalSearch); 339 340 // If we got back nothing, and it wasn't a request for global search, then try again 341 // for global search, as we'll try to launch that in lieu of any component-specific search. 342 if (!globalSearch && mSearchable == null) { 343 globalSearch = true; 344 mSearchable = searchManager.getSearchableInfo(componentName, globalSearch); 345 } 346 347 // If there's not even a searchable info available for global search, then really give up. 348 if (mSearchable == null) { 349 Log.w(LOG_TAG, "No global search provider."); 350 return false; 351 } 352 353 mLaunchComponent = componentName; 354 mAppSearchData = appSearchData; 355 // Using globalSearch here is just an optimization, just calling 356 // isDefaultSearchable() should always give the same result. 357 mGlobalSearchMode = globalSearch || searchManager.isDefaultSearchable(mSearchable); 358 mActivityContext = mSearchable.getActivityContext(getContext()); 359 360 // show the dialog. this will call onStart(). 361 if (!isShowing()) { 362 // The Dialog uses a ContextThemeWrapper for the context; use this to change the 363 // theme out from underneath us, between the global search theme and the in-app 364 // search theme. They are identical except that the global search theme does not 365 // dim the background of the window (because global search is full screen so it's 366 // not needed and this should save a little bit of time on global search invocation). 367 Object context = getContext(); 368 if (context instanceof ContextThemeWrapper) { 369 ContextThemeWrapper wrapper = (ContextThemeWrapper) context; 370 if (globalSearch) { 371 wrapper.setTheme(com.android.internal.R.style.Theme_GlobalSearchBar); 372 } else { 373 wrapper.setTheme(com.android.internal.R.style.Theme_SearchBar); 374 } 375 } 376 show(); 377 } 378 updateUI(); 379 380 return true; 381 } 382 383 /** 384 * The search dialog is being dismissed, so handle all of the local shutdown operations. 385 * 386 * This function is designed to be idempotent so that dismiss() can be safely called at any time 387 * (even if already closed) and more likely to really dump any memory. No leaks! 388 */ 389 @Override 390 public void onStop() { 391 super.onStop(); 392 393 closeSuggestionsAdapter(); 394 395 // dump extra memory we're hanging on to 396 mLaunchComponent = null; 397 mAppSearchData = null; 398 mSearchable = null; 399 mActivityContext = null; 400 mUserQuery = null; 401 mPreviousComponents = null; 402 } 403 404 /** 405 * Sets the search dialog to the 'working' state, which shows a working spinner in the 406 * right hand size of the text field. 407 * 408 * @param working true to show spinner, false to hide spinner 409 */ 410 public void setWorking(boolean working) { 411 if (working) { 412 mSearchAutoComplete.setCompoundDrawablesWithIntrinsicBounds( 413 null, null, mWorkingSpinner, null); 414 ((Animatable) mWorkingSpinner).start(); 415 } else { 416 mSearchAutoComplete.setCompoundDrawablesWithIntrinsicBounds( 417 null, null, null, null); 418 ((Animatable) mWorkingSpinner).stop(); 419 } 420 } 421 422 /** 423 * Closes and gets rid of the suggestions adapter. 424 */ 425 private void closeSuggestionsAdapter() { 426 // remove the adapter from the autocomplete first, to avoid any updates 427 // when we drop the cursor 428 mSearchAutoComplete.setAdapter((SuggestionsAdapter)null); 429 // close any leftover cursor 430 if (mSuggestionsAdapter != null) { 431 mSuggestionsAdapter.changeCursor(null); 432 } 433 mSuggestionsAdapter = null; 434 } 435 436 /** 437 * Save the minimal set of data necessary to recreate the search 438 * 439 * @return A bundle with the state of the dialog, or {@code null} if the search 440 * dialog is not showing. 441 */ 442 @Override 443 public Bundle onSaveInstanceState() { 444 if (!isShowing()) return null; 445 446 Bundle bundle = new Bundle(); 447 448 // setup info so I can recreate this particular search 449 bundle.putParcelable(INSTANCE_KEY_COMPONENT, mLaunchComponent); 450 bundle.putBundle(INSTANCE_KEY_APPDATA, mAppSearchData); 451 bundle.putBoolean(INSTANCE_KEY_GLOBALSEARCH, mGlobalSearchMode); 452 bundle.putParcelable(INSTANCE_KEY_STORED_COMPONENT, mStoredComponentName); 453 bundle.putBundle(INSTANCE_KEY_STORED_APPDATA, mStoredAppSearchData); 454 bundle.putParcelableArrayList(INSTANCE_KEY_PREVIOUS_COMPONENTS, mPreviousComponents); 455 bundle.putString(INSTANCE_KEY_USER_QUERY, mUserQuery); 456 457 return bundle; 458 } 459 460 /** 461 * Restore the state of the dialog from a previously saved bundle. 462 * 463 * TODO: go through this and make sure that it saves everything that is saved 464 * 465 * @param savedInstanceState The state of the dialog previously saved by 466 * {@link #onSaveInstanceState()}. 467 */ 468 @Override 469 public void onRestoreInstanceState(Bundle savedInstanceState) { 470 if (savedInstanceState == null) return; 471 472 ComponentName launchComponent = savedInstanceState.getParcelable(INSTANCE_KEY_COMPONENT); 473 Bundle appSearchData = savedInstanceState.getBundle(INSTANCE_KEY_APPDATA); 474 boolean globalSearch = savedInstanceState.getBoolean(INSTANCE_KEY_GLOBALSEARCH); 475 ComponentName storedComponentName = 476 savedInstanceState.getParcelable(INSTANCE_KEY_STORED_COMPONENT); 477 Bundle storedAppSearchData = 478 savedInstanceState.getBundle(INSTANCE_KEY_STORED_APPDATA); 479 ArrayList<ComponentName> previousComponents = 480 savedInstanceState.getParcelableArrayList(INSTANCE_KEY_PREVIOUS_COMPONENTS); 481 String userQuery = savedInstanceState.getString(INSTANCE_KEY_USER_QUERY); 482 483 // Set stored state 484 mStoredComponentName = storedComponentName; 485 mStoredAppSearchData = storedAppSearchData; 486 mPreviousComponents = previousComponents; 487 488 // show the dialog. 489 if (!doShow(userQuery, false, launchComponent, appSearchData, globalSearch)) { 490 // for some reason, we couldn't re-instantiate 491 return; 492 } 493 } 494 495 /** 496 * Called after resources have changed, e.g. after screen rotation or locale change. 497 */ 498 public void onConfigurationChanged() { 499 if (isShowing()) { 500 // Redraw (resources may have changed) 501 updateSearchButton(); 502 updateSearchAppIcon(); 503 updateSearchBadge(); 504 updateQueryHint(); 505 } 506 } 507 508 /** 509 * Update the UI according to the info in the current value of {@link #mSearchable}. 510 */ 511 private void updateUI() { 512 if (mSearchable != null) { 513 mDecor.setVisibility(View.VISIBLE); 514 updateSearchAutoComplete(); 515 updateSearchButton(); 516 updateSearchAppIcon(); 517 updateSearchBadge(); 518 updateQueryHint(); 519 updateVoiceButton(); 520 521 // In order to properly configure the input method (if one is being used), we 522 // need to let it know if we'll be providing suggestions. Although it would be 523 // difficult/expensive to know if every last detail has been configured properly, we 524 // can at least see if a suggestions provider has been configured, and use that 525 // as our trigger. 526 int inputType = mSearchable.getInputType(); 527 // We only touch this if the input type is set up for text (which it almost certainly 528 // should be, in the case of search!) 529 if ((inputType & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_TEXT) { 530 // The existence of a suggestions authority is the proxy for "suggestions 531 // are available here" 532 inputType &= ~InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE; 533 if (mSearchable.getSuggestAuthority() != null) { 534 inputType |= InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE; 535 } 536 } 537 mSearchAutoComplete.setInputType(inputType); 538 mSearchAutoCompleteImeOptions = mSearchable.getImeOptions(); 539 mSearchAutoComplete.setImeOptions(mSearchAutoCompleteImeOptions); 540 } 541 } 542 543 /** 544 * Updates the auto-complete text view. 545 */ 546 private void updateSearchAutoComplete() { 547 // close any existing suggestions adapter 548 closeSuggestionsAdapter(); 549 550 mSearchAutoComplete.setDropDownAnimationStyle(0); // no animation 551 mSearchAutoComplete.setThreshold(mSearchable.getSuggestThreshold()); 552 // we dismiss the entire dialog instead 553 mSearchAutoComplete.setDropDownDismissedOnCompletion(false); 554 555 if (!isInRealAppSearch()) { 556 mSearchAutoComplete.setDropDownAlwaysVisible(true); // fill space until results come in 557 } else { 558 mSearchAutoComplete.setDropDownAlwaysVisible(false); 559 } 560 561 mSearchAutoComplete.setForceIgnoreOutsideTouch(true); 562 563 // attach the suggestions adapter, if suggestions are available 564 // The existence of a suggestions authority is the proxy for "suggestions available here" 565 if (mSearchable.getSuggestAuthority() != null) { 566 mSuggestionsAdapter = new SuggestionsAdapter(getContext(), this, mSearchable, 567 mOutsideDrawablesCache, mGlobalSearchMode); 568 mSearchAutoComplete.setAdapter(mSuggestionsAdapter); 569 } 570 } 571 572 /** 573 * Update the text in the search button. Note: This is deprecated functionality, for 574 * 1.0 compatibility only. 575 */ 576 private void updateSearchButton() { 577 String textLabel = null; 578 Drawable iconLabel = null; 579 int textId = mSearchable.getSearchButtonText(); 580 if (textId != 0) { 581 textLabel = mActivityContext.getResources().getString(textId); 582 } else { 583 iconLabel = getContext().getResources(). 584 getDrawable(com.android.internal.R.drawable.ic_btn_search); 585 } 586 mGoButton.setText(textLabel); 587 mGoButton.setCompoundDrawablesWithIntrinsicBounds(iconLabel, null, null, null); 588 } 589 590 private void updateSearchAppIcon() { 591 // In Donut, we special-case the case of the browser to hide the app icon as if it were 592 // global search, for extra space for url entry. 593 // 594 // TODO: Remove this special case once the issue has been reconciled in Eclair. 595 if (mGlobalSearchMode || isBrowserSearch()) { 596 mAppIcon.setImageResource(0); 597 mAppIcon.setVisibility(View.GONE); 598 mSearchPlate.setPadding(SEARCH_PLATE_LEFT_PADDING_GLOBAL, 599 mSearchPlate.getPaddingTop(), 600 mSearchPlate.getPaddingRight(), 601 mSearchPlate.getPaddingBottom()); 602 } else { 603 PackageManager pm = getContext().getPackageManager(); 604 Drawable icon; 605 try { 606 ActivityInfo info = pm.getActivityInfo(mLaunchComponent, 0); 607 icon = pm.getApplicationIcon(info.applicationInfo); 608 if (DBG) Log.d(LOG_TAG, "Using app-specific icon"); 609 } catch (NameNotFoundException e) { 610 icon = pm.getDefaultActivityIcon(); 611 Log.w(LOG_TAG, mLaunchComponent + " not found, using generic app icon"); 612 } 613 mAppIcon.setImageDrawable(icon); 614 mAppIcon.setVisibility(View.VISIBLE); 615 mSearchPlate.setPadding(SEARCH_PLATE_LEFT_PADDING_NON_GLOBAL, 616 mSearchPlate.getPaddingTop(), 617 mSearchPlate.getPaddingRight(), 618 mSearchPlate.getPaddingBottom()); 619 } 620 } 621 622 /** 623 * Setup the search "Badge" if requested by mode flags. 624 */ 625 private void updateSearchBadge() { 626 // assume both hidden 627 int visibility = View.GONE; 628 Drawable icon = null; 629 CharSequence text = null; 630 631 // optionally show one or the other. 632 if (mSearchable.useBadgeIcon()) { 633 icon = mActivityContext.getResources().getDrawable(mSearchable.getIconId()); 634 visibility = View.VISIBLE; 635 if (DBG) Log.d(LOG_TAG, "Using badge icon: " + mSearchable.getIconId()); 636 } else if (mSearchable.useBadgeLabel()) { 637 text = mActivityContext.getResources().getText(mSearchable.getLabelId()).toString(); 638 visibility = View.VISIBLE; 639 if (DBG) Log.d(LOG_TAG, "Using badge label: " + mSearchable.getLabelId()); 640 } 641 642 mBadgeLabel.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null); 643 mBadgeLabel.setText(text); 644 mBadgeLabel.setVisibility(visibility); 645 } 646 647 /** 648 * Update the hint in the query text field. 649 */ 650 private void updateQueryHint() { 651 if (isShowing()) { 652 String hint = null; 653 if (mSearchable != null) { 654 int hintId = mSearchable.getHintId(); 655 if (hintId != 0) { 656 hint = mActivityContext.getString(hintId); 657 } 658 } 659 mSearchAutoComplete.setHint(hint); 660 } 661 } 662 663 /** 664 * Update the visibility of the voice button. There are actually two voice search modes, 665 * either of which will activate the button. 666 */ 667 private void updateVoiceButton() { 668 int visibility = View.GONE; 669 if (mSearchable.getVoiceSearchEnabled()) { 670 Intent testIntent = null; 671 if (mSearchable.getVoiceSearchLaunchWebSearch()) { 672 testIntent = mVoiceWebSearchIntent; 673 } else if (mSearchable.getVoiceSearchLaunchRecognizer()) { 674 testIntent = mVoiceAppSearchIntent; 675 } 676 if (testIntent != null) { 677 ResolveInfo ri = getContext().getPackageManager(). 678 resolveActivity(testIntent, PackageManager.MATCH_DEFAULT_ONLY); 679 if (ri != null) { 680 visibility = View.VISIBLE; 681 } 682 } 683 } 684 mVoiceButton.setVisibility(visibility); 685 } 686 687 /** 688 * Hack to determine whether this is the browser, so we can remove the browser icon 689 * to the left of the search field, as a special requirement for Donut. 690 * 691 * TODO: For Eclair, reconcile this with the rest of the global search UI. 692 */ 693 private boolean isBrowserSearch() { 694 return mLaunchComponent.flattenToShortString().startsWith("com.android.browser/"); 695 } 696 697 /** 698 * Listeners of various types 699 */ 700 701 /** 702 * {@link Dialog#onTouchEvent(MotionEvent)} will cancel the dialog only when the 703 * touch is outside the window. But the window includes space for the drop-down, 704 * so we also cancel on taps outside the search bar when the drop-down is not showing. 705 */ 706 @Override 707 public boolean onTouchEvent(MotionEvent event) { 708 // cancel if the drop-down is not showing and the touch event was outside the search plate 709 if (!mSearchAutoComplete.isPopupShowing() && isOutOfBounds(mSearchPlate, event)) { 710 if (DBG) Log.d(LOG_TAG, "Pop-up not showing and outside of search plate."); 711 cancel(); 712 return true; 713 } 714 // Let Dialog handle events outside the window while the pop-up is showing. 715 return super.onTouchEvent(event); 716 } 717 718 private boolean isOutOfBounds(View v, MotionEvent event) { 719 final int x = (int) event.getX(); 720 final int y = (int) event.getY(); 721 final int slop = ViewConfiguration.get(mContext).getScaledWindowTouchSlop(); 722 return (x < -slop) || (y < -slop) 723 || (x > (v.getWidth()+slop)) 724 || (y > (v.getHeight()+slop)); 725 } 726 727 /** 728 * Dialog's OnKeyListener implements various search-specific functionality 729 * 730 * @param keyCode This is the keycode of the typed key, and is the same value as 731 * found in the KeyEvent parameter. 732 * @param event The complete event record for the typed key 733 * 734 * @return Return true if the event was handled here, or false if not. 735 */ 736 @Override 737 public boolean onKeyDown(int keyCode, KeyEvent event) { 738 if (DBG) Log.d(LOG_TAG, "onKeyDown(" + keyCode + "," + event + ")"); 739 if (mSearchable == null) { 740 return false; 741 } 742 743 // handle back key to go back to previous searchable, etc. 744 if (handleBackKey(keyCode, event)) { 745 return true; 746 } 747 748 if (keyCode == KeyEvent.KEYCODE_SEARCH) { 749 // If the search key is pressed, toggle between global and in-app search. If we are 750 // currently doing global search and there is no in-app search context to toggle to, 751 // just don't do anything. 752 return toggleGlobalSearch(); 753 } 754 755 // if it's an action specified by the searchable activity, launch the 756 // entered query with the action key 757 SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode); 758 if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) { 759 launchQuerySearch(keyCode, actionKey.getQueryActionMsg()); 760 return true; 761 } 762 763 return false; 764 } 765 766 /** 767 * Callback to watch the textedit field for empty/non-empty 768 */ 769 private TextWatcher mTextWatcher = new TextWatcher() { 770 771 public void beforeTextChanged(CharSequence s, int start, int before, int after) { } 772 773 public void onTextChanged(CharSequence s, int start, 774 int before, int after) { 775 if (DBG_LOG_TIMING) { 776 dbgLogTiming("onTextChanged()"); 777 } 778 if (mSearchable == null) { 779 return; 780 } 781 updateWidgetState(); 782 if (!mSearchAutoComplete.isPerformingCompletion()) { 783 // The user changed the query, remember it. 784 mUserQuery = s == null ? "" : s.toString(); 785 } 786 } 787 788 public void afterTextChanged(Editable s) { 789 if (mSearchable == null) { 790 return; 791 } 792 if (mSearchable.autoUrlDetect() && !mSearchAutoComplete.isPerformingCompletion()) { 793 // The user changed the query, check if it is a URL and if so change the search 794 // button in the soft keyboard to the 'Go' button. 795 int options = (mSearchAutoComplete.getImeOptions() & (~EditorInfo.IME_MASK_ACTION)); 796 if (Regex.WEB_URL_PATTERN.matcher(mUserQuery).matches()) { 797 options = options | EditorInfo.IME_ACTION_GO; 798 } else { 799 options = options | EditorInfo.IME_ACTION_SEARCH; 800 } 801 if (options != mSearchAutoCompleteImeOptions) { 802 mSearchAutoCompleteImeOptions = options; 803 mSearchAutoComplete.setImeOptions(options); 804 // This call is required to update the soft keyboard UI with latest IME flags. 805 mSearchAutoComplete.setInputType(mSearchAutoComplete.getInputType()); 806 } 807 } 808 } 809 }; 810 811 /** 812 * Enable/Disable the cancel button based on edit text state (any text?) 813 */ 814 private void updateWidgetState() { 815 // enable the button if we have one or more non-space characters 816 boolean enabled = !mSearchAutoComplete.isEmpty(); 817 mGoButton.setEnabled(enabled); 818 mGoButton.setFocusable(enabled); 819 } 820 821 /** 822 * React to typing in the GO search button by refocusing to EditText. 823 * Continue typing the query. 824 */ 825 View.OnKeyListener mButtonsKeyListener = new View.OnKeyListener() { 826 public boolean onKey(View v, int keyCode, KeyEvent event) { 827 // guard against possible race conditions 828 if (mSearchable == null) { 829 return false; 830 } 831 832 if (!event.isSystem() && 833 (keyCode != KeyEvent.KEYCODE_DPAD_UP) && 834 (keyCode != KeyEvent.KEYCODE_DPAD_LEFT) && 835 (keyCode != KeyEvent.KEYCODE_DPAD_RIGHT) && 836 (keyCode != KeyEvent.KEYCODE_DPAD_CENTER)) { 837 // restore focus and give key to EditText ... 838 if (mSearchAutoComplete.requestFocus()) { 839 return mSearchAutoComplete.dispatchKeyEvent(event); 840 } 841 } 842 843 return false; 844 } 845 }; 846 847 /** 848 * React to a click in the GO button by launching a search. 849 */ 850 View.OnClickListener mGoButtonClickListener = new View.OnClickListener() { 851 public void onClick(View v) { 852 // guard against possible race conditions 853 if (mSearchable == null) { 854 return; 855 } 856 launchQuerySearch(); 857 } 858 }; 859 860 /** 861 * React to a click in the voice search button. 862 */ 863 View.OnClickListener mVoiceButtonClickListener = new View.OnClickListener() { 864 public void onClick(View v) { 865 // guard against possible race conditions 866 if (mSearchable == null) { 867 return; 868 } 869 try { 870 // First stop the existing search before starting voice search, or else we'll end 871 // up showing the search dialog again once we return to the app. 872 ((SearchManager) getContext().getSystemService(Context.SEARCH_SERVICE)). 873 stopSearch(); 874 875 if (mSearchable.getVoiceSearchLaunchWebSearch()) { 876 getContext().startActivity(mVoiceWebSearchIntent); 877 } else if (mSearchable.getVoiceSearchLaunchRecognizer()) { 878 Intent appSearchIntent = createVoiceAppSearchIntent(mVoiceAppSearchIntent); 879 getContext().startActivity(appSearchIntent); 880 } 881 } catch (ActivityNotFoundException e) { 882 // Should not happen, since we check the availability of 883 // voice search before showing the button. But just in case... 884 Log.w(LOG_TAG, "Could not find voice search activity"); 885 } 886 } 887 }; 888 889 /** 890 * Create and return an Intent that can launch the voice search activity, perform a specific 891 * voice transcription, and forward the results to the searchable activity. 892 * 893 * @param baseIntent The voice app search intent to start from 894 * @return A completely-configured intent ready to send to the voice search activity 895 */ 896 private Intent createVoiceAppSearchIntent(Intent baseIntent) { 897 ComponentName searchActivity = mSearchable.getSearchActivity(); 898 899 // create the necessary intent to set up a search-and-forward operation 900 // in the voice search system. We have to keep the bundle separate, 901 // because it becomes immutable once it enters the PendingIntent 902 Intent queryIntent = new Intent(Intent.ACTION_SEARCH); 903 queryIntent.setComponent(searchActivity); 904 PendingIntent pending = PendingIntent.getActivity( 905 getContext(), 0, queryIntent, PendingIntent.FLAG_ONE_SHOT); 906 907 // Now set up the bundle that will be inserted into the pending intent 908 // when it's time to do the search. We always build it here (even if empty) 909 // because the voice search activity will always need to insert "QUERY" into 910 // it anyway. 911 Bundle queryExtras = new Bundle(); 912 if (mAppSearchData != null) { 913 queryExtras.putBundle(SearchManager.APP_DATA, mAppSearchData); 914 } 915 916 // Now build the intent to launch the voice search. Add all necessary 917 // extras to launch the voice recognizer, and then all the necessary extras 918 // to forward the results to the searchable activity 919 Intent voiceIntent = new Intent(baseIntent); 920 921 // Add all of the configuration options supplied by the searchable's metadata 922 String languageModel = RecognizerIntent.LANGUAGE_MODEL_FREE_FORM; 923 String prompt = null; 924 String language = null; 925 int maxResults = 1; 926 Resources resources = mActivityContext.getResources(); 927 if (mSearchable.getVoiceLanguageModeId() != 0) { 928 languageModel = resources.getString(mSearchable.getVoiceLanguageModeId()); 929 } 930 if (mSearchable.getVoicePromptTextId() != 0) { 931 prompt = resources.getString(mSearchable.getVoicePromptTextId()); 932 } 933 if (mSearchable.getVoiceLanguageId() != 0) { 934 language = resources.getString(mSearchable.getVoiceLanguageId()); 935 } 936 if (mSearchable.getVoiceMaxResults() != 0) { 937 maxResults = mSearchable.getVoiceMaxResults(); 938 } 939 voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, languageModel); 940 voiceIntent.putExtra(RecognizerIntent.EXTRA_PROMPT, prompt); 941 voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, language); 942 voiceIntent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, maxResults); 943 voiceIntent.putExtra(EXTRA_CALLING_PACKAGE, 944 searchActivity == null ? null : searchActivity.toShortString()); 945 946 // Add the values that configure forwarding the results 947 voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT, pending); 948 voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT_BUNDLE, queryExtras); 949 950 return voiceIntent; 951 } 952 953 /** 954 * Corrects http/https typo errors in the given url string, and if the protocol specifier was 955 * not present defaults to http. 956 * 957 * @param inUrl URL to check and fix 958 * @return fixed URL string. 959 */ 960 private String fixUrl(String inUrl) { 961 if (inUrl.startsWith("http://") || inUrl.startsWith("https://")) 962 return inUrl; 963 964 if (inUrl.startsWith("http:") || inUrl.startsWith("https:")) { 965 if (inUrl.startsWith("http:/") || inUrl.startsWith("https:/")) { 966 inUrl = inUrl.replaceFirst("/", "//"); 967 } else { 968 inUrl = inUrl.replaceFirst(":", "://"); 969 } 970 } 971 972 if (inUrl.indexOf("://") == -1) { 973 inUrl = "http://" + inUrl; 974 } 975 976 return inUrl; 977 } 978 979 /** 980 * React to the user typing "enter" or other hardwired keys while typing in the search box. 981 * This handles these special keys while the edit box has focus. 982 */ 983 View.OnKeyListener mTextKeyListener = new View.OnKeyListener() { 984 public boolean onKey(View v, int keyCode, KeyEvent event) { 985 // guard against possible race conditions 986 if (mSearchable == null) { 987 return false; 988 } 989 990 if (DBG_LOG_TIMING) dbgLogTiming("doTextKey()"); 991 if (DBG) { 992 Log.d(LOG_TAG, "mTextListener.onKey(" + keyCode + "," + event 993 + "), selection: " + mSearchAutoComplete.getListSelection()); 994 } 995 996 // If a suggestion is selected, handle enter, search key, and action keys 997 // as presses on the selected suggestion 998 if (mSearchAutoComplete.isPopupShowing() && 999 mSearchAutoComplete.getListSelection() != ListView.INVALID_POSITION) { 1000 return onSuggestionsKey(v, keyCode, event); 1001 } 1002 1003 // If there is text in the query box, handle enter, and action keys 1004 // The search key is handled by the dialog's onKeyDown(). 1005 if (!mSearchAutoComplete.isEmpty()) { 1006 if (keyCode == KeyEvent.KEYCODE_ENTER 1007 && event.getAction() == KeyEvent.ACTION_UP) { 1008 v.cancelLongPress(); 1009 1010 // If this is a url entered by the user & we displayed the 'Go' button which 1011 // the user clicked, launch the url instead of using it as a search query. 1012 if (mSearchable.autoUrlDetect() && 1013 (mSearchAutoCompleteImeOptions & EditorInfo.IME_MASK_ACTION) 1014 == EditorInfo.IME_ACTION_GO) { 1015 Uri uri = Uri.parse(fixUrl(mSearchAutoComplete.getText().toString())); 1016 Intent intent = new Intent(Intent.ACTION_VIEW, uri); 1017 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 1018 launchIntent(intent); 1019 } else { 1020 // Launch as a regular search. 1021 launchQuerySearch(); 1022 } 1023 return true; 1024 } 1025 if (event.getAction() == KeyEvent.ACTION_DOWN) { 1026 SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode); 1027 if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) { 1028 launchQuerySearch(keyCode, actionKey.getQueryActionMsg()); 1029 return true; 1030 } 1031 } 1032 } 1033 return false; 1034 } 1035 }; 1036 1037 @Override 1038 public void hide() { 1039 if (!isShowing()) return; 1040 1041 // We made sure the IME was displayed, so also make sure it is closed 1042 // when we go away. 1043 InputMethodManager imm = (InputMethodManager)getContext() 1044 .getSystemService(Context.INPUT_METHOD_SERVICE); 1045 if (imm != null) { 1046 imm.hideSoftInputFromWindow( 1047 getWindow().getDecorView().getWindowToken(), 0); 1048 } 1049 1050 super.hide(); 1051 } 1052 1053 /** 1054 * React to the user typing while in the suggestions list. First, check for action 1055 * keys. If not handled, try refocusing regular characters into the EditText. 1056 */ 1057 private boolean onSuggestionsKey(View v, int keyCode, KeyEvent event) { 1058 // guard against possible race conditions (late arrival after dismiss) 1059 if (mSearchable == null) { 1060 return false; 1061 } 1062 if (mSuggestionsAdapter == null) { 1063 return false; 1064 } 1065 if (event.getAction() == KeyEvent.ACTION_DOWN) { 1066 if (DBG_LOG_TIMING) { 1067 dbgLogTiming("onSuggestionsKey()"); 1068 } 1069 1070 // First, check for enter or search (both of which we'll treat as a "click") 1071 if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_SEARCH) { 1072 int position = mSearchAutoComplete.getListSelection(); 1073 return launchSuggestion(position); 1074 } 1075 1076 // Next, check for left/right moves, which we use to "return" the user to the edit view 1077 if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { 1078 // give "focus" to text editor, with cursor at the beginning if 1079 // left key, at end if right key 1080 // TODO: Reverse left/right for right-to-left languages, e.g. Arabic 1081 int selPoint = (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) ? 1082 0 : mSearchAutoComplete.length(); 1083 mSearchAutoComplete.setSelection(selPoint); 1084 mSearchAutoComplete.setListSelection(0); 1085 mSearchAutoComplete.clearListSelection(); 1086 mSearchAutoComplete.ensureImeVisible(); 1087 1088 return true; 1089 } 1090 1091 // Next, check for an "up and out" move 1092 if (keyCode == KeyEvent.KEYCODE_DPAD_UP 1093 && 0 == mSearchAutoComplete.getListSelection()) { 1094 restoreUserQuery(); 1095 // let ACTV complete the move 1096 return false; 1097 } 1098 1099 // Next, check for an "action key" 1100 SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode); 1101 if ((actionKey != null) && 1102 ((actionKey.getSuggestActionMsg() != null) || 1103 (actionKey.getSuggestActionMsgColumn() != null))) { 1104 // launch suggestion using action key column 1105 int position = mSearchAutoComplete.getListSelection(); 1106 if (position != ListView.INVALID_POSITION) { 1107 Cursor c = mSuggestionsAdapter.getCursor(); 1108 if (c.moveToPosition(position)) { 1109 final String actionMsg = getActionKeyMessage(c, actionKey); 1110 if (actionMsg != null && (actionMsg.length() > 0)) { 1111 return launchSuggestion(position, keyCode, actionMsg); 1112 } 1113 } 1114 } 1115 } 1116 } 1117 return false; 1118 } 1119 1120 /** 1121 * Launch a search for the text in the query text field. 1122 */ 1123 public void launchQuerySearch() { 1124 launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null); 1125 } 1126 1127 /** 1128 * Launch a search for the text in the query text field. 1129 * 1130 * @param actionKey The key code of the action key that was pressed, 1131 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none. 1132 * @param actionMsg The message for the action key that was pressed, 1133 * or <code>null</code> if none. 1134 */ 1135 protected void launchQuerySearch(int actionKey, String actionMsg) { 1136 String query = mSearchAutoComplete.getText().toString(); 1137 String action = mGlobalSearchMode ? Intent.ACTION_WEB_SEARCH : Intent.ACTION_SEARCH; 1138 Intent intent = createIntent(action, null, null, query, null, 1139 actionKey, actionMsg); 1140 launchIntent(intent); 1141 } 1142 1143 /** 1144 * Launches an intent based on a suggestion. 1145 * 1146 * @param position The index of the suggestion to create the intent from. 1147 * @return true if a successful launch, false if could not (e.g. bad position). 1148 */ 1149 protected boolean launchSuggestion(int position) { 1150 return launchSuggestion(position, KeyEvent.KEYCODE_UNKNOWN, null); 1151 } 1152 1153 /** 1154 * Launches an intent based on a suggestion. 1155 * 1156 * @param position The index of the suggestion to create the intent from. 1157 * @param actionKey The key code of the action key that was pressed, 1158 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none. 1159 * @param actionMsg The message for the action key that was pressed, 1160 * or <code>null</code> if none. 1161 * @return true if a successful launch, false if could not (e.g. bad position). 1162 */ 1163 protected boolean launchSuggestion(int position, int actionKey, String actionMsg) { 1164 Cursor c = mSuggestionsAdapter.getCursor(); 1165 if ((c != null) && c.moveToPosition(position)) { 1166 1167 Intent intent = createIntentFromSuggestion(c, actionKey, actionMsg); 1168 1169 // report back about the click 1170 if (mGlobalSearchMode) { 1171 // in global search mode, do it via cursor 1172 mSuggestionsAdapter.callCursorOnClick(c, position); 1173 } else if (intent != null 1174 && mPreviousComponents != null 1175 && !mPreviousComponents.isEmpty()) { 1176 // in-app search (and we have pivoted in as told by mPreviousComponents, 1177 // which is used for keeping track of what we pop back to when we are pivoting into 1178 // in app search.) 1179 reportInAppClickToGlobalSearch(c, intent); 1180 } 1181 1182 // launch the intent 1183 launchIntent(intent); 1184 1185 return true; 1186 } 1187 return false; 1188 } 1189 1190 /** 1191 * Report a click from an in app search result back to global search for shortcutting porpoises. 1192 * 1193 * @param c The cursor that is pointing to the clicked position. 1194 * @param intent The intent that will be launched for the click. 1195 */ 1196 private void reportInAppClickToGlobalSearch(Cursor c, Intent intent) { 1197 // for in app search, still tell global search via content provider 1198 Uri uri = getClickReportingUri(); 1199 final ContentValues cv = new ContentValues(); 1200 cv.put(SearchManager.SEARCH_CLICK_REPORT_COLUMN_QUERY, mUserQuery); 1201 final ComponentName source = mSearchable.getSearchActivity(); 1202 cv.put(SearchManager.SEARCH_CLICK_REPORT_COLUMN_COMPONENT, source.flattenToShortString()); 1203 1204 // grab the intent columns from the intent we created since it has additional 1205 // logic for falling back on the searchable default 1206 cv.put(SearchManager.SUGGEST_COLUMN_INTENT_ACTION, intent.getAction()); 1207 cv.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA, intent.getDataString()); 1208 cv.put(SearchManager.SUGGEST_COLUMN_INTENT_COMPONENT_NAME, 1209 intent.getStringExtra(SearchManager.COMPONENT_NAME_KEY)); 1210 1211 // ensure the icons will work for global search 1212 cv.put(SearchManager.SUGGEST_COLUMN_ICON_1, 1213 wrapIconForPackage( 1214 mSearchable.getSuggestPackage(), 1215 getColumnString(c, SearchManager.SUGGEST_COLUMN_ICON_1))); 1216 cv.put(SearchManager.SUGGEST_COLUMN_ICON_2, 1217 wrapIconForPackage( 1218 mSearchable.getSuggestPackage(), 1219 getColumnString(c, SearchManager.SUGGEST_COLUMN_ICON_2))); 1220 1221 // the rest can be passed through directly 1222 cv.put(SearchManager.SUGGEST_COLUMN_FORMAT, 1223 getColumnString(c, SearchManager.SUGGEST_COLUMN_FORMAT)); 1224 cv.put(SearchManager.SUGGEST_COLUMN_TEXT_1, 1225 getColumnString(c, SearchManager.SUGGEST_COLUMN_TEXT_1)); 1226 cv.put(SearchManager.SUGGEST_COLUMN_TEXT_2, 1227 getColumnString(c, SearchManager.SUGGEST_COLUMN_TEXT_2)); 1228 cv.put(SearchManager.SUGGEST_COLUMN_QUERY, 1229 getColumnString(c, SearchManager.SUGGEST_COLUMN_QUERY)); 1230 cv.put(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID, 1231 getColumnString(c, SearchManager.SUGGEST_COLUMN_SHORTCUT_ID)); 1232 // note: deliberately omitting background color since it is only for global search 1233 // "more results" entries 1234 mContext.getContentResolver().insert(uri, cv); 1235 } 1236 1237 /** 1238 * @return A URI appropriate for reporting a click. 1239 */ 1240 private Uri getClickReportingUri() { 1241 Uri.Builder uriBuilder = new Uri.Builder() 1242 .scheme(ContentResolver.SCHEME_CONTENT) 1243 .authority(SearchManager.SEARCH_CLICK_REPORT_AUTHORITY); 1244 1245 uriBuilder.appendPath(SearchManager.SEARCH_CLICK_REPORT_URI_PATH); 1246 1247 return uriBuilder 1248 .query("") // TODO: Remove, workaround for a bug in Uri.writeToParcel() 1249 .fragment("") // TODO: Remove, workaround for a bug in Uri.writeToParcel() 1250 .build(); 1251 } 1252 1253 /** 1254 * Wraps an icon for a particular package. If the icon is a resource id, it is converted into 1255 * an android.resource:// URI. 1256 * 1257 * @param packageName The source of the icon 1258 * @param icon The icon retrieved from a suggestion column 1259 * @return An icon string appropriate for the package. 1260 */ 1261 private String wrapIconForPackage(String packageName, String icon) { 1262 if (icon == null || icon.length() == 0 || "0".equals(icon)) { 1263 // SearchManager specifies that null or zero can be returned to indicate 1264 // no icon. We also allow empty string. 1265 return null; 1266 } else if (!Character.isDigit(icon.charAt(0))){ 1267 return icon; 1268 } else { 1269 return new Uri.Builder() 1270 .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) 1271 .authority(packageName) 1272 .encodedPath(icon) 1273 .toString(); 1274 } 1275 } 1276 1277 /** 1278 * Launches an intent, including any special intent handling. Doesn't dismiss the dialog 1279 * since that will be handled in {@link SearchDialogWrapper#performActivityResuming} 1280 */ 1281 private void launchIntent(Intent intent) { 1282 if (intent == null) { 1283 return; 1284 } 1285 if (handleSpecialIntent(intent)){ 1286 return; 1287 } 1288 Log.d(LOG_TAG, "launching " + intent); 1289 try { 1290 // in global search mode, we send the activity straight to the original suggestion 1291 // source. this is because GlobalSearch may not have permission to launch the 1292 // intent, and to avoid the extra step of going through GlobalSearch. 1293 if (mGlobalSearchMode) { 1294 launchGlobalSearchIntent(intent); 1295 } else { 1296 // If the intent was created from a suggestion, it will always have an explicit 1297 // component here. 1298 Log.i(LOG_TAG, "Starting (as ourselves) " + intent.toURI()); 1299 getContext().startActivity(intent); 1300 // If the search switches to a different activity, 1301 // SearchDialogWrapper#performActivityResuming 1302 // will handle hiding the dialog when the next activity starts, but for 1303 // real in-app search, we still need to dismiss the dialog. 1304 if (isInRealAppSearch()) { 1305 dismiss(); 1306 } 1307 } 1308 } catch (RuntimeException ex) { 1309 Log.e(LOG_TAG, "Failed launch activity: " + intent, ex); 1310 } 1311 } 1312 1313 private void launchGlobalSearchIntent(Intent intent) { 1314 final String packageName; 1315 // GlobalSearch puts the original source of the suggestion in the 1316 // 'component name' column. If set, we send the intent to that activity. 1317 // We trust GlobalSearch to always set this to the suggestion source. 1318 String intentComponent = intent.getStringExtra(SearchManager.COMPONENT_NAME_KEY); 1319 if (intentComponent != null) { 1320 ComponentName componentName = ComponentName.unflattenFromString(intentComponent); 1321 intent.setComponent(componentName); 1322 intent.removeExtra(SearchManager.COMPONENT_NAME_KEY); 1323 // Launch the intent as the suggestion source. 1324 // This prevents sources from using the search dialog to launch 1325 // intents that they don't have permission for themselves. 1326 packageName = componentName.getPackageName(); 1327 } else { 1328 // If there is no component in the suggestion, it must be a built-in suggestion 1329 // from GlobalSearch (e.g. "Search the web for") or the intent 1330 // launched when pressing the search/go button in the search dialog. 1331 // Launch the intent with the permissions of GlobalSearch. 1332 packageName = mSearchable.getSearchActivity().getPackageName(); 1333 } 1334 1335 // Launch all global search suggestions as new tasks, since they don't relate 1336 // to the current task. 1337 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 1338 setBrowserApplicationId(intent); 1339 1340 startActivityInPackage(intent, packageName); 1341 } 1342 1343 /** 1344 * If the intent is to open an HTTP or HTTPS URL, we set 1345 * {@link Browser#EXTRA_APPLICATION_ID} so that any existing browser window that 1346 * has been opened by us for the same URL will be reused. 1347 */ 1348 private void setBrowserApplicationId(Intent intent) { 1349 Uri data = intent.getData(); 1350 if (Intent.ACTION_VIEW.equals(intent.getAction()) && data != null) { 1351 String scheme = data.getScheme(); 1352 if (scheme != null && scheme.startsWith("http")) { 1353 intent.putExtra(Browser.EXTRA_APPLICATION_ID, data.toString()); 1354 } 1355 } 1356 } 1357 1358 /** 1359 * Starts an activity as if it had been started by the given package. 1360 * 1361 * @param intent The description of the activity to start. 1362 * @param packageName 1363 * @throws ActivityNotFoundException If the intent could not be resolved to 1364 * and existing activity. 1365 * @throws SecurityException If the package does not have permission to start 1366 * start the activity. 1367 * @throws AndroidRuntimeException If some other error occurs. 1368 */ 1369 private void startActivityInPackage(Intent intent, String packageName) { 1370 try { 1371 int uid = ActivityThread.getPackageManager().getPackageUid(packageName); 1372 if (uid < 0) { 1373 throw new AndroidRuntimeException("Package UID not found " + packageName); 1374 } 1375 String resolvedType = intent.resolveTypeIfNeeded(getContext().getContentResolver()); 1376 IBinder resultTo = null; 1377 String resultWho = null; 1378 int requestCode = -1; 1379 boolean onlyIfNeeded = false; 1380 Log.i(LOG_TAG, "Starting (uid " + uid + ", " + packageName + ") " + intent.toURI()); 1381 int result = ActivityManagerNative.getDefault().startActivityInPackage( 1382 uid, intent, resolvedType, resultTo, resultWho, requestCode, onlyIfNeeded); 1383 checkStartActivityResult(result, intent); 1384 } catch (RemoteException ex) { 1385 throw new AndroidRuntimeException(ex); 1386 } 1387 } 1388 1389 // Stolen from Instrumentation.checkStartActivityResult() 1390 private static void checkStartActivityResult(int res, Intent intent) { 1391 if (res >= IActivityManager.START_SUCCESS) { 1392 return; 1393 } 1394 switch (res) { 1395 case IActivityManager.START_INTENT_NOT_RESOLVED: 1396 case IActivityManager.START_CLASS_NOT_FOUND: 1397 if (intent.getComponent() != null) 1398 throw new ActivityNotFoundException( 1399 "Unable to find explicit activity class " 1400 + intent.getComponent().toShortString() 1401 + "; have you declared this activity in your AndroidManifest.xml?"); 1402 throw new ActivityNotFoundException( 1403 "No Activity found to handle " + intent); 1404 case IActivityManager.START_PERMISSION_DENIED: 1405 throw new SecurityException("Not allowed to start activity " 1406 + intent); 1407 case IActivityManager.START_FORWARD_AND_REQUEST_CONFLICT: 1408 throw new AndroidRuntimeException( 1409 "FORWARD_RESULT_FLAG used while also requesting a result"); 1410 default: 1411 throw new AndroidRuntimeException("Unknown error code " 1412 + res + " when starting " + intent); 1413 } 1414 } 1415 1416 /** 1417 * Handles the special intent actions declared in {@link SearchManager}. 1418 * 1419 * @return <code>true</code> if the intent was handled. 1420 */ 1421 private boolean handleSpecialIntent(Intent intent) { 1422 String action = intent.getAction(); 1423 if (SearchManager.INTENT_ACTION_CHANGE_SEARCH_SOURCE.equals(action)) { 1424 handleChangeSourceIntent(intent); 1425 return true; 1426 } 1427 return false; 1428 } 1429 1430 /** 1431 * Handles {@link SearchManager#INTENT_ACTION_CHANGE_SEARCH_SOURCE}. 1432 */ 1433 private void handleChangeSourceIntent(Intent intent) { 1434 Uri dataUri = intent.getData(); 1435 if (dataUri == null) { 1436 Log.w(LOG_TAG, "SearchManager.INTENT_ACTION_CHANGE_SOURCE without intent data."); 1437 return; 1438 } 1439 ComponentName componentName = ComponentName.unflattenFromString(dataUri.toString()); 1440 if (componentName == null) { 1441 Log.w(LOG_TAG, "Invalid ComponentName: " + dataUri); 1442 return; 1443 } 1444 if (DBG) Log.d(LOG_TAG, "Switching to " + componentName); 1445 1446 pushPreviousComponent(mLaunchComponent); 1447 if (!show(componentName, mAppSearchData, false)) { 1448 Log.w(LOG_TAG, "Failed to switch to source " + componentName); 1449 popPreviousComponent(); 1450 return; 1451 } 1452 1453 String query = intent.getStringExtra(SearchManager.QUERY); 1454 setUserQuery(query); 1455 mSearchAutoComplete.showDropDown(); 1456 } 1457 1458 /** 1459 * Sets the list item selection in the AutoCompleteTextView's ListView. 1460 */ 1461 public void setListSelection(int index) { 1462 mSearchAutoComplete.setListSelection(index); 1463 } 1464 1465 /** 1466 * Saves the previous component that was searched, so that we can go 1467 * back to it. 1468 */ 1469 private void pushPreviousComponent(ComponentName componentName) { 1470 if (mPreviousComponents == null) { 1471 mPreviousComponents = new ArrayList<ComponentName>(); 1472 } 1473 mPreviousComponents.add(componentName); 1474 } 1475 1476 /** 1477 * Pops the previous component off the stack and returns it. 1478 * 1479 * @return The component name, or <code>null</code> if there was 1480 * no previous component. 1481 */ 1482 private ComponentName popPreviousComponent() { 1483 if (mPreviousComponents == null) { 1484 return null; 1485 } 1486 int size = mPreviousComponents.size(); 1487 if (size == 0) { 1488 return null; 1489 } 1490 return mPreviousComponents.remove(size - 1); 1491 } 1492 1493 /** 1494 * Goes back to the previous component that was searched, if any. 1495 * 1496 * @return <code>true</code> if there was a previous component that we could go back to. 1497 */ 1498 private boolean backToPreviousComponent() { 1499 ComponentName previous = popPreviousComponent(); 1500 if (previous == null) { 1501 return false; 1502 } 1503 if (!show(previous, mAppSearchData, false)) { 1504 Log.w(LOG_TAG, "Failed to switch to source " + previous); 1505 return false; 1506 } 1507 1508 // must touch text to trigger suggestions 1509 // TODO: should this be the text as it was when the user left 1510 // the source that we are now going back to? 1511 String query = mSearchAutoComplete.getText().toString(); 1512 setUserQuery(query); 1513 1514 return true; 1515 } 1516 1517 /** 1518 * When a particular suggestion has been selected, perform the various lookups required 1519 * to use the suggestion. This includes checking the cursor for suggestion-specific data, 1520 * and/or falling back to the XML for defaults; It also creates REST style Uri data when 1521 * the suggestion includes a data id. 1522 * 1523 * @param c The suggestions cursor, moved to the row of the user's selection 1524 * @param actionKey The key code of the action key that was pressed, 1525 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none. 1526 * @param actionMsg The message for the action key that was pressed, 1527 * or <code>null</code> if none. 1528 * @return An intent for the suggestion at the cursor's position. 1529 */ 1530 private Intent createIntentFromSuggestion(Cursor c, int actionKey, String actionMsg) { 1531 try { 1532 // use specific action if supplied, or default action if supplied, or fixed default 1533 String action = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_ACTION); 1534 1535 // some items are display only, or have effect via the cursor respond click reporting. 1536 if (SearchManager.INTENT_ACTION_NONE.equals(action)) { 1537 return null; 1538 } 1539 1540 if (action == null) { 1541 action = mSearchable.getSuggestIntentAction(); 1542 } 1543 if (action == null) { 1544 action = Intent.ACTION_SEARCH; 1545 } 1546 1547 // use specific data if supplied, or default data if supplied 1548 String data = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA); 1549 if (data == null) { 1550 data = mSearchable.getSuggestIntentData(); 1551 } 1552 // then, if an ID was provided, append it. 1553 if (data != null) { 1554 String id = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID); 1555 if (id != null) { 1556 data = data + "/" + Uri.encode(id); 1557 } 1558 } 1559 Uri dataUri = (data == null) ? null : Uri.parse(data); 1560 1561 String componentName = getColumnString( 1562 c, SearchManager.SUGGEST_COLUMN_INTENT_COMPONENT_NAME); 1563 1564 String query = getColumnString(c, SearchManager.SUGGEST_COLUMN_QUERY); 1565 String extraData = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA); 1566 1567 return createIntent(action, dataUri, extraData, query, componentName, actionKey, 1568 actionMsg); 1569 } catch (RuntimeException e ) { 1570 int rowNum; 1571 try { // be really paranoid now 1572 rowNum = c.getPosition(); 1573 } catch (RuntimeException e2 ) { 1574 rowNum = -1; 1575 } 1576 Log.w(LOG_TAG, "Search Suggestions cursor at row " + rowNum + 1577 " returned exception" + e.toString()); 1578 return null; 1579 } 1580 } 1581 1582 /** 1583 * Constructs an intent from the given information and the search dialog state. 1584 * 1585 * @param action Intent action. 1586 * @param data Intent data, or <code>null</code>. 1587 * @param extraData Data for {@link SearchManager#EXTRA_DATA_KEY} or <code>null</code>. 1588 * @param query Intent query, or <code>null</code>. 1589 * @param componentName Data for {@link SearchManager#COMPONENT_NAME_KEY} or <code>null</code>. 1590 * @param actionKey The key code of the action key that was pressed, 1591 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none. 1592 * @param actionMsg The message for the action key that was pressed, 1593 * or <code>null</code> if none. 1594 * @return The intent. 1595 */ 1596 private Intent createIntent(String action, Uri data, String extraData, String query, 1597 String componentName, int actionKey, String actionMsg) { 1598 // Now build the Intent 1599 Intent intent = new Intent(action); 1600 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 1601 if (data != null) { 1602 intent.setData(data); 1603 } 1604 intent.putExtra(SearchManager.USER_QUERY, mUserQuery); 1605 if (query != null) { 1606 intent.putExtra(SearchManager.QUERY, query); 1607 } 1608 if (extraData != null) { 1609 intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData); 1610 } 1611 if (componentName != null) { 1612 intent.putExtra(SearchManager.COMPONENT_NAME_KEY, componentName); 1613 } 1614 if (mAppSearchData != null) { 1615 intent.putExtra(SearchManager.APP_DATA, mAppSearchData); 1616 } 1617 if (actionKey != KeyEvent.KEYCODE_UNKNOWN) { 1618 intent.putExtra(SearchManager.ACTION_KEY, actionKey); 1619 intent.putExtra(SearchManager.ACTION_MSG, actionMsg); 1620 } 1621 // Only allow 3rd-party intents from GlobalSearch 1622 if (!mGlobalSearchMode) { 1623 intent.setComponent(mSearchable.getSearchActivity()); 1624 } 1625 return intent; 1626 } 1627 1628 /** 1629 * For a given suggestion and a given cursor row, get the action message. If not provided 1630 * by the specific row/column, also check for a single definition (for the action key). 1631 * 1632 * @param c The cursor providing suggestions 1633 * @param actionKey The actionkey record being examined 1634 * 1635 * @return Returns a string, or null if no action key message for this suggestion 1636 */ 1637 private static String getActionKeyMessage(Cursor c, SearchableInfo.ActionKeyInfo actionKey) { 1638 String result = null; 1639 // check first in the cursor data, for a suggestion-specific message 1640 final String column = actionKey.getSuggestActionMsgColumn(); 1641 if (column != null) { 1642 result = SuggestionsAdapter.getColumnString(c, column); 1643 } 1644 // If the cursor didn't give us a message, see if there's a single message defined 1645 // for the actionkey (for all suggestions) 1646 if (result == null) { 1647 result = actionKey.getSuggestActionMsg(); 1648 } 1649 return result; 1650 } 1651 1652 /** 1653 * Local subclass for AutoCompleteTextView. 1654 */ 1655 public static class SearchAutoComplete extends AutoCompleteTextView { 1656 1657 private int mThreshold; 1658 private SearchDialog mSearchDialog; 1659 1660 public SearchAutoComplete(Context context) { 1661 super(context); 1662 mThreshold = getThreshold(); 1663 } 1664 1665 public SearchAutoComplete(Context context, AttributeSet attrs) { 1666 super(context, attrs); 1667 mThreshold = getThreshold(); 1668 } 1669 1670 public SearchAutoComplete(Context context, AttributeSet attrs, int defStyle) { 1671 super(context, attrs, defStyle); 1672 mThreshold = getThreshold(); 1673 } 1674 1675 private void setSearchDialog(SearchDialog searchDialog) { 1676 mSearchDialog = searchDialog; 1677 } 1678 1679 @Override 1680 public void setThreshold(int threshold) { 1681 super.setThreshold(threshold); 1682 mThreshold = threshold; 1683 } 1684 1685 /** 1686 * Returns true if the text field is empty, or contains only whitespace. 1687 */ 1688 private boolean isEmpty() { 1689 return TextUtils.getTrimmedLength(getText()) == 0; 1690 } 1691 1692 /** 1693 * We override this method to avoid replacing the query box text 1694 * when a suggestion is clicked. 1695 */ 1696 @Override 1697 protected void replaceText(CharSequence text) { 1698 } 1699 1700 /** 1701 * We override this method to avoid an extra onItemClick being called on the 1702 * drop-down's OnItemClickListener by {@link AutoCompleteTextView#onKeyUp(int, KeyEvent)} 1703 * when an item is clicked with the trackball. 1704 */ 1705 @Override 1706 public void performCompletion() { 1707 } 1708 1709 /** 1710 * We override this method to be sure and show the soft keyboard if appropriate when 1711 * the TextView has focus. 1712 */ 1713 @Override 1714 public void onWindowFocusChanged(boolean hasWindowFocus) { 1715 super.onWindowFocusChanged(hasWindowFocus); 1716 1717 if (hasWindowFocus) { 1718 InputMethodManager inputManager = (InputMethodManager) 1719 getContext().getSystemService(Context.INPUT_METHOD_SERVICE); 1720 inputManager.showSoftInput(this, 0); 1721 } 1722 } 1723 1724 /** 1725 * We override this method so that we can allow a threshold of zero, which ACTV does not. 1726 */ 1727 @Override 1728 public boolean enoughToFilter() { 1729 return mThreshold <= 0 || super.enoughToFilter(); 1730 } 1731 1732 /** 1733 * {@link AutoCompleteTextView#onKeyPreIme(int, KeyEvent)}) dismisses the drop-down on BACK, 1734 * so we must override this method to modify the BACK behavior. 1735 */ 1736 @Override 1737 public boolean onKeyPreIme(int keyCode, KeyEvent event) { 1738 if (mSearchDialog.mSearchable == null) { 1739 return false; 1740 } 1741 if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_DOWN) { 1742 if (mSearchDialog.backToPreviousComponent()) { 1743 return true; 1744 } 1745 // If the drop-down obscures the keyboard, the user wouldn't see anything 1746 // happening when pressing back, so we dismiss the entire dialog instead. 1747 // 1748 // also: if there is no text entered, we also want to dismiss the whole dialog, 1749 // not just the soft keyboard. the exception to this is if there are shortcuts 1750 // that aren't displayed (e.g are being obscured by the soft keyboard); in that 1751 // case we want to dismiss the soft keyboard so the user can see the rest of the 1752 // shortcuts. 1753 if (isInputMethodNotNeeded() || 1754 (isEmpty() && getDropDownChildCount() >= getAdapterCount())) { 1755 mSearchDialog.cancel(); 1756 return true; 1757 } 1758 return false; // will dismiss soft keyboard if necessary 1759 } 1760 return false; 1761 } 1762 1763 private int getAdapterCount() { 1764 final ListAdapter adapter = getAdapter(); 1765 return adapter == null ? 0 : adapter.getCount(); 1766 } 1767 } 1768 1769 protected boolean handleBackKey(int keyCode, KeyEvent event) { 1770 if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_DOWN) { 1771 if (backToPreviousComponent()) { 1772 return true; 1773 } 1774 cancel(); 1775 return true; 1776 } 1777 return false; 1778 } 1779 1780 /** 1781 * Implements OnItemClickListener 1782 */ 1783 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 1784 if (DBG) Log.d(LOG_TAG, "onItemClick() position " + position); 1785 launchSuggestion(position); 1786 } 1787 1788 /** 1789 * Implements OnItemSelectedListener 1790 */ 1791 public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { 1792 if (DBG) Log.d(LOG_TAG, "onItemSelected() position " + position); 1793 // A suggestion has been selected, rewrite the query if possible, 1794 // otherwise the restore the original query. 1795 if (REWRITE_QUERIES) { 1796 rewriteQueryFromSuggestion(position); 1797 } 1798 } 1799 1800 /** 1801 * Implements OnItemSelectedListener 1802 */ 1803 public void onNothingSelected(AdapterView<?> parent) { 1804 if (DBG) Log.d(LOG_TAG, "onNothingSelected()"); 1805 } 1806 1807 /** 1808 * Query rewriting. 1809 */ 1810 1811 private void rewriteQueryFromSuggestion(int position) { 1812 Cursor c = mSuggestionsAdapter.getCursor(); 1813 if (c == null) { 1814 return; 1815 } 1816 if (c.moveToPosition(position)) { 1817 // Get the new query from the suggestion. 1818 CharSequence newQuery = mSuggestionsAdapter.convertToString(c); 1819 if (newQuery != null) { 1820 // The suggestion rewrites the query. 1821 if (DBG) Log.d(LOG_TAG, "Rewriting query to '" + newQuery + "'"); 1822 // Update the text field, without getting new suggestions. 1823 setQuery(newQuery); 1824 } else { 1825 // The suggestion does not rewrite the query, restore the user's query. 1826 if (DBG) Log.d(LOG_TAG, "Suggestion gives no rewrite, restoring user query."); 1827 restoreUserQuery(); 1828 } 1829 } else { 1830 // We got a bad position, restore the user's query. 1831 Log.w(LOG_TAG, "Bad suggestion position: " + position); 1832 restoreUserQuery(); 1833 } 1834 } 1835 1836 /** 1837 * Restores the query entered by the user if needed. 1838 */ 1839 private void restoreUserQuery() { 1840 if (DBG) Log.d(LOG_TAG, "Restoring query to '" + mUserQuery + "'"); 1841 setQuery(mUserQuery); 1842 } 1843 1844 /** 1845 * Sets the text in the query box, without updating the suggestions. 1846 */ 1847 private void setQuery(CharSequence query) { 1848 mSearchAutoComplete.setText(query, false); 1849 if (query != null) { 1850 mSearchAutoComplete.setSelection(query.length()); 1851 } 1852 } 1853 1854 /** 1855 * Sets the text in the query box, updating the suggestions. 1856 */ 1857 private void setUserQuery(String query) { 1858 if (query == null) { 1859 query = ""; 1860 } 1861 mUserQuery = query; 1862 mSearchAutoComplete.setText(query); 1863 mSearchAutoComplete.setSelection(query.length()); 1864 } 1865 1866 /** 1867 * Debugging Support 1868 */ 1869 1870 /** 1871 * For debugging only, sample the millisecond clock and log it. 1872 * Uses AtomicLong so we can use in multiple threads 1873 */ 1874 private AtomicLong mLastLogTime = new AtomicLong(SystemClock.uptimeMillis()); 1875 private void dbgLogTiming(final String caller) { 1876 long millis = SystemClock.uptimeMillis(); 1877 long oldTime = mLastLogTime.getAndSet(millis); 1878 long delta = millis - oldTime; 1879 final String report = millis + " (+" + delta + ") ticks for Search keystroke in " + caller; 1880 Log.d(LOG_TAG,report); 1881 } 1882} 1883