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