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