SearchDialog.java revision e6af73e18eaa1303a1499b6ec240afd953a92607
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.server.search.SearchableInfo; 41import android.speech.RecognizerIntent; 42import android.text.Editable; 43import android.text.InputType; 44import android.text.TextUtils; 45import android.text.TextWatcher; 46import android.text.util.Regex; 47import android.util.AndroidRuntimeException; 48import android.util.AttributeSet; 49import android.util.Log; 50import android.view.ContextThemeWrapper; 51import android.view.Gravity; 52import android.view.KeyEvent; 53import android.view.MotionEvent; 54import android.view.View; 55import android.view.ViewConfiguration; 56import android.view.ViewGroup; 57import android.view.Window; 58import android.view.WindowManager; 59import android.view.inputmethod.EditorInfo; 60import android.view.inputmethod.InputMethodManager; 61import android.widget.AdapterView; 62import android.widget.AutoCompleteTextView; 63import android.widget.Button; 64import android.widget.ImageButton; 65import android.widget.ImageView; 66import android.widget.LinearLayout; 67import android.widget.ListView; 68import android.widget.TextView; 69import android.widget.AdapterView.OnItemClickListener; 70import android.widget.AdapterView.OnItemSelectedListener; 71 72import java.util.ArrayList; 73import java.util.WeakHashMap; 74import 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 */ 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 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 */ 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 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 */ 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 */ 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 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 */ 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 */ 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 */ 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 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 */ 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 */ 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 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 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 */ 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 */ 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 */ 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 */ 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 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 */ 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 */ 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 */ 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 */ 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 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 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 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 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 */ 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 */ 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 */ 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 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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(0)); 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 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 */ 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 */ 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(0)); 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() 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 1716 public SearchBar(Context context, AttributeSet attrs) { 1717 super(context, attrs); 1718 } 1719 1720 public SearchBar(Context context) { 1721 super(context); 1722 } 1723 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 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 1760 public SearchAutoComplete(Context context) { 1761 super(context); 1762 mThreshold = getThreshold(); 1763 } 1764 1765 public SearchAutoComplete(Context context, AttributeSet attrs) { 1766 super(context, attrs); 1767 mThreshold = getThreshold(); 1768 } 1769 1770 public SearchAutoComplete(Context context, AttributeSet attrs, int defStyle) { 1771 super(context, attrs, defStyle); 1772 mThreshold = getThreshold(); 1773 } 1774 1775 @Override 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 */ 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 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 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 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 1824 public boolean enoughToFilter() { 1825 return mThreshold <= 0 || super.enoughToFilter(); 1826 } 1827 1828 } 1829 1830 @Override 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 */ 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 */ 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 */ 1871 public void onNothingSelected(AdapterView<?> parent) { 1872 if (DBG) Log.d(LOG_TAG, "onNothingSelected()"); 1873 } 1874 1875 /** 1876 * Query rewriting. 1877 */ 1878 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 */ 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 */ 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 */ 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()); 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