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