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