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