SearchDialog.java revision e9ce3f01d42769f03f10e70c3244500e92d7eee1
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.LinearLayout; 64import android.widget.ListView; 65import android.widget.TextView; 66import android.widget.AdapterView.OnItemClickListener; 67import android.widget.AdapterView.OnItemSelectedListener; 68 69import com.android.common.Patterns; 70 71import java.util.ArrayList; 72import java.util.WeakHashMap; 73import java.util.concurrent.atomic.AtomicLong; 74 75/** 76 * Search dialog. This is controlled by the 77 * SearchManager and runs in the current foreground process. 78 * 79 * @hide 80 */ 81public class SearchDialog extends Dialog implements OnItemClickListener, OnItemSelectedListener { 82 83 // Debugging support 84 private static final boolean DBG = false; 85 private static final String LOG_TAG = "SearchDialog"; 86 private static final boolean DBG_LOG_TIMING = false; 87 88 private static final String INSTANCE_KEY_COMPONENT = "comp"; 89 private static final String INSTANCE_KEY_APPDATA = "data"; 90 private static final String INSTANCE_KEY_GLOBALSEARCH = "glob"; 91 private static final String INSTANCE_KEY_STORED_COMPONENT = "sComp"; 92 private static final String INSTANCE_KEY_STORED_APPDATA = "sData"; 93 private static final String INSTANCE_KEY_PREVIOUS_COMPONENTS = "sPrev"; 94 private static final String INSTANCE_KEY_USER_QUERY = "uQry"; 95 96 // The extra key used in an intent to the speech recognizer for in-app voice search. 97 private static final String EXTRA_CALLING_PACKAGE = "calling_package"; 98 99 // The string used for privateImeOptions to identify to the IME that it should not show 100 // a microphone button since one already exists in the search dialog. 101 private static final String IME_OPTION_NO_MICROPHONE = "nm"; 102 103 private static final int SEARCH_PLATE_LEFT_PADDING_GLOBAL = 12; 104 private static final int SEARCH_PLATE_LEFT_PADDING_NON_GLOBAL = 7; 105 106 // views & widgets 107 private TextView mBadgeLabel; 108 private SearchSourceSelector mSourceSelector; 109 private SearchAutoComplete mSearchAutoComplete; 110 private Button mGoButton; 111 private ImageButton mVoiceButton; 112 private View mSearchPlate; 113 private Drawable mWorkingSpinner; 114 115 // interaction with searchable application 116 private SearchableInfo mSearchable; 117 private ComponentName mLaunchComponent; 118 private Bundle mAppSearchData; 119 private boolean mGlobalSearchMode; 120 private Context mActivityContext; 121 private SearchManager mSearchManager; 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, SearchManager searchManager) { 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 mSearchManager = searchManager; 173 } 174 175 /** 176 * Create the search dialog and any resources that are used for the 177 * entire lifetime of the dialog. 178 */ 179 @Override 180 protected void onCreate(Bundle savedInstanceState) { 181 super.onCreate(savedInstanceState); 182 183 Window theWindow = getWindow(); 184 WindowManager.LayoutParams lp = theWindow.getAttributes(); 185 lp.width = ViewGroup.LayoutParams.MATCH_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.MATCH_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 mSourceSelector = new SearchSourceSelector( 213 findViewById(com.android.internal.R.id.search_source_selector)); 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 mSourceSelector.setSource(mSearchable.getSearchActivity()); 613 mSourceSelector.setAppSearchData(mAppSearchData); 614 615 // In Donut, we special-case the case of the browser to hide the app icon as if it were 616 // global search, for extra space for url entry. 617 // 618 // TODO: Remove this special case once the issue has been reconciled in Eclair. 619 if (mGlobalSearchMode || isBrowserSearch()) { 620 mSourceSelector.setSourceIcon(null); 621 mSourceSelector.setVisibility(View.GONE); 622 mSearchPlate.setPadding(SEARCH_PLATE_LEFT_PADDING_GLOBAL, 623 mSearchPlate.getPaddingTop(), 624 mSearchPlate.getPaddingRight(), 625 mSearchPlate.getPaddingBottom()); 626 } else { 627 PackageManager pm = getContext().getPackageManager(); 628 Drawable icon; 629 try { 630 ActivityInfo info = pm.getActivityInfo(mLaunchComponent, 0); 631 icon = pm.getApplicationIcon(info.applicationInfo); 632 if (DBG) Log.d(LOG_TAG, "Using app-specific icon"); 633 } catch (NameNotFoundException e) { 634 icon = pm.getDefaultActivityIcon(); 635 Log.w(LOG_TAG, mLaunchComponent + " not found, using generic app icon"); 636 } 637 mSourceSelector.setSourceIcon(icon); 638 mSourceSelector.setVisibility(View.VISIBLE); 639 mSearchPlate.setPadding(SEARCH_PLATE_LEFT_PADDING_NON_GLOBAL, 640 mSearchPlate.getPaddingTop(), 641 mSearchPlate.getPaddingRight(), 642 mSearchPlate.getPaddingBottom()); 643 } 644 } 645 646 /** 647 * Setup the search "Badge" if requested by mode flags. 648 */ 649 private void updateSearchBadge() { 650 // assume both hidden 651 int visibility = View.GONE; 652 Drawable icon = null; 653 CharSequence text = null; 654 655 // optionally show one or the other. 656 if (mSearchable.useBadgeIcon()) { 657 icon = mActivityContext.getResources().getDrawable(mSearchable.getIconId()); 658 visibility = View.VISIBLE; 659 if (DBG) Log.d(LOG_TAG, "Using badge icon: " + mSearchable.getIconId()); 660 } else if (mSearchable.useBadgeLabel()) { 661 text = mActivityContext.getResources().getText(mSearchable.getLabelId()).toString(); 662 visibility = View.VISIBLE; 663 if (DBG) Log.d(LOG_TAG, "Using badge label: " + mSearchable.getLabelId()); 664 } 665 666 mBadgeLabel.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null); 667 mBadgeLabel.setText(text); 668 mBadgeLabel.setVisibility(visibility); 669 } 670 671 /** 672 * Update the hint in the query text field. 673 */ 674 private void updateQueryHint() { 675 if (isShowing()) { 676 String hint = null; 677 if (mSearchable != null) { 678 int hintId = mSearchable.getHintId(); 679 if (hintId != 0) { 680 hint = mActivityContext.getString(hintId); 681 } 682 } 683 mSearchAutoComplete.setHint(hint); 684 } 685 } 686 687 /** 688 * Update the visibility of the voice button. There are actually two voice search modes, 689 * either of which will activate the button. 690 */ 691 private void updateVoiceButton() { 692 int visibility = View.GONE; 693 if (mSearchable.getVoiceSearchEnabled()) { 694 Intent testIntent = null; 695 if (mSearchable.getVoiceSearchLaunchWebSearch()) { 696 testIntent = mVoiceWebSearchIntent; 697 } else if (mSearchable.getVoiceSearchLaunchRecognizer()) { 698 testIntent = mVoiceAppSearchIntent; 699 } 700 if (testIntent != null) { 701 ResolveInfo ri = getContext().getPackageManager(). 702 resolveActivity(testIntent, PackageManager.MATCH_DEFAULT_ONLY); 703 if (ri != null) { 704 visibility = View.VISIBLE; 705 } 706 } 707 } 708 mVoiceButton.setVisibility(visibility); 709 } 710 711 /** 712 * Hack to determine whether this is the browser, so we can remove the browser icon 713 * to the left of the search field, as a special requirement for Donut. 714 * 715 * TODO: For Eclair, reconcile this with the rest of the global search UI. 716 */ 717 private boolean isBrowserSearch() { 718 return mLaunchComponent.flattenToShortString().startsWith("com.android.browser/"); 719 } 720 721 /** 722 * Listeners of various types 723 */ 724 725 /** 726 * {@link Dialog#onTouchEvent(MotionEvent)} will cancel the dialog only when the 727 * touch is outside the window. But the window includes space for the drop-down, 728 * so we also cancel on taps outside the search bar when the drop-down is not showing. 729 */ 730 @Override 731 public boolean onTouchEvent(MotionEvent event) { 732 // cancel if the drop-down is not showing and the touch event was outside the search plate 733 if (!mSearchAutoComplete.isPopupShowing() && isOutOfBounds(mSearchPlate, event)) { 734 if (DBG) Log.d(LOG_TAG, "Pop-up not showing and outside of search plate."); 735 cancel(); 736 return true; 737 } 738 // Let Dialog handle events outside the window while the pop-up is showing. 739 return super.onTouchEvent(event); 740 } 741 742 private boolean isOutOfBounds(View v, MotionEvent event) { 743 final int x = (int) event.getX(); 744 final int y = (int) event.getY(); 745 final int slop = ViewConfiguration.get(mContext).getScaledWindowTouchSlop(); 746 return (x < -slop) || (y < -slop) 747 || (x > (v.getWidth()+slop)) 748 || (y > (v.getHeight()+slop)); 749 } 750 751 /** 752 * Dialog's OnKeyListener implements various search-specific functionality 753 * 754 * @param keyCode This is the keycode of the typed key, and is the same value as 755 * found in the KeyEvent parameter. 756 * @param event The complete event record for the typed key 757 * 758 * @return Return true if the event was handled here, or false if not. 759 */ 760 @Override 761 public boolean onKeyDown(int keyCode, KeyEvent event) { 762 if (DBG) Log.d(LOG_TAG, "onKeyDown(" + keyCode + "," + event + ")"); 763 if (mSearchable == null) { 764 return false; 765 } 766 767 if (keyCode == KeyEvent.KEYCODE_SEARCH && event.getRepeatCount() == 0) { 768 event.startTracking(); 769 // Consume search key for later use. 770 return true; 771 } 772 773 // if it's an action specified by the searchable activity, launch the 774 // entered query with the action key 775 SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode); 776 if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) { 777 launchQuerySearch(keyCode, actionKey.getQueryActionMsg()); 778 return true; 779 } 780 781 return super.onKeyDown(keyCode, event); 782 } 783 784 @Override 785 public boolean onKeyUp(int keyCode, KeyEvent event) { 786 if (DBG) Log.d(LOG_TAG, "onKeyUp(" + keyCode + "," + event + ")"); 787 if (mSearchable == null) { 788 return false; 789 } 790 791 if (keyCode == KeyEvent.KEYCODE_SEARCH && event.isTracking() 792 && !event.isCanceled()) { 793 // If the search key is pressed, toggle between global and in-app search. If we are 794 // currently doing global search and there is no in-app search context to toggle to, 795 // just don't do anything. 796 return toggleGlobalSearch(); 797 } 798 799 return super.onKeyUp(keyCode, event); 800 } 801 802 /** 803 * Callback to watch the textedit field for empty/non-empty 804 */ 805 private TextWatcher mTextWatcher = new TextWatcher() { 806 807 public void beforeTextChanged(CharSequence s, int start, int before, int after) { } 808 809 public void onTextChanged(CharSequence s, int start, 810 int before, int after) { 811 if (DBG_LOG_TIMING) { 812 dbgLogTiming("onTextChanged()"); 813 } 814 if (mSearchable == null) { 815 return; 816 } 817 updateWidgetState(); 818 if (!mSearchAutoComplete.isPerformingCompletion()) { 819 // The user changed the query, remember it. 820 mUserQuery = s == null ? "" : s.toString(); 821 mSourceSelector.setQuery(mUserQuery); 822 } 823 } 824 825 public void afterTextChanged(Editable s) { 826 if (mSearchable == null) { 827 return; 828 } 829 if (mSearchable.autoUrlDetect() && !mSearchAutoComplete.isPerformingCompletion()) { 830 // The user changed the query, check if it is a URL and if so change the search 831 // button in the soft keyboard to the 'Go' button. 832 int options = (mSearchAutoComplete.getImeOptions() & (~EditorInfo.IME_MASK_ACTION)); 833 if (Patterns.WEB_URL.matcher(mUserQuery).matches()) { 834 options = options | EditorInfo.IME_ACTION_GO; 835 } else { 836 options = options | EditorInfo.IME_ACTION_SEARCH; 837 } 838 if (options != mSearchAutoCompleteImeOptions) { 839 mSearchAutoCompleteImeOptions = options; 840 mSearchAutoComplete.setImeOptions(options); 841 // This call is required to update the soft keyboard UI with latest IME flags. 842 mSearchAutoComplete.setInputType(mSearchAutoComplete.getInputType()); 843 } 844 } 845 } 846 }; 847 848 /** 849 * Enable/Disable the cancel button based on edit text state (any text?) 850 */ 851 private void updateWidgetState() { 852 // enable the button if we have one or more non-space characters 853 boolean enabled = !mSearchAutoComplete.isEmpty(); 854 mGoButton.setEnabled(enabled); 855 mGoButton.setFocusable(enabled); 856 } 857 858 /** 859 * React to typing in the GO search button by refocusing to EditText. 860 * Continue typing the query. 861 */ 862 View.OnKeyListener mButtonsKeyListener = new View.OnKeyListener() { 863 public boolean onKey(View v, int keyCode, KeyEvent event) { 864 // guard against possible race conditions 865 if (mSearchable == null) { 866 return false; 867 } 868 869 if (!event.isSystem() && 870 (keyCode != KeyEvent.KEYCODE_DPAD_UP) && 871 (keyCode != KeyEvent.KEYCODE_DPAD_LEFT) && 872 (keyCode != KeyEvent.KEYCODE_DPAD_RIGHT) && 873 (keyCode != KeyEvent.KEYCODE_DPAD_CENTER)) { 874 // restore focus and give key to EditText ... 875 if (mSearchAutoComplete.requestFocus()) { 876 return mSearchAutoComplete.dispatchKeyEvent(event); 877 } 878 } 879 880 return false; 881 } 882 }; 883 884 /** 885 * React to a click in the GO button by launching a search. 886 */ 887 View.OnClickListener mGoButtonClickListener = new View.OnClickListener() { 888 public void onClick(View v) { 889 // guard against possible race conditions 890 if (mSearchable == null) { 891 return; 892 } 893 launchQuerySearch(); 894 } 895 }; 896 897 /** 898 * React to a click in the voice search button. 899 */ 900 View.OnClickListener mVoiceButtonClickListener = new View.OnClickListener() { 901 public void onClick(View v) { 902 // guard against possible race conditions 903 if (mSearchable == null) { 904 return; 905 } 906 try { 907 // First stop the existing search before starting voice search, or else we'll end 908 // up showing the search dialog again once we return to the app. 909 ((SearchManager) getContext().getSystemService(Context.SEARCH_SERVICE)). 910 stopSearch(); 911 912 if (mSearchable.getVoiceSearchLaunchWebSearch()) { 913 getContext().startActivity(mVoiceWebSearchIntent); 914 } else if (mSearchable.getVoiceSearchLaunchRecognizer()) { 915 Intent appSearchIntent = createVoiceAppSearchIntent(mVoiceAppSearchIntent); 916 getContext().startActivity(appSearchIntent); 917 } 918 } catch (ActivityNotFoundException e) { 919 // Should not happen, since we check the availability of 920 // voice search before showing the button. But just in case... 921 Log.w(LOG_TAG, "Could not find voice search activity"); 922 } 923 } 924 }; 925 926 /** 927 * Create and return an Intent that can launch the voice search activity, perform a specific 928 * voice transcription, and forward the results to the searchable activity. 929 * 930 * @param baseIntent The voice app search intent to start from 931 * @return A completely-configured intent ready to send to the voice search activity 932 */ 933 private Intent createVoiceAppSearchIntent(Intent baseIntent) { 934 ComponentName searchActivity = mSearchable.getSearchActivity(); 935 936 // create the necessary intent to set up a search-and-forward operation 937 // in the voice search system. We have to keep the bundle separate, 938 // because it becomes immutable once it enters the PendingIntent 939 Intent queryIntent = new Intent(Intent.ACTION_SEARCH); 940 queryIntent.setComponent(searchActivity); 941 PendingIntent pending = PendingIntent.getActivity( 942 getContext(), 0, queryIntent, PendingIntent.FLAG_ONE_SHOT); 943 944 // Now set up the bundle that will be inserted into the pending intent 945 // when it's time to do the search. We always build it here (even if empty) 946 // because the voice search activity will always need to insert "QUERY" into 947 // it anyway. 948 Bundle queryExtras = new Bundle(); 949 if (mAppSearchData != null) { 950 queryExtras.putBundle(SearchManager.APP_DATA, mAppSearchData); 951 } 952 953 // Now build the intent to launch the voice search. Add all necessary 954 // extras to launch the voice recognizer, and then all the necessary extras 955 // to forward the results to the searchable activity 956 Intent voiceIntent = new Intent(baseIntent); 957 958 // Add all of the configuration options supplied by the searchable's metadata 959 String languageModel = RecognizerIntent.LANGUAGE_MODEL_FREE_FORM; 960 String prompt = null; 961 String language = null; 962 int maxResults = 1; 963 Resources resources = mActivityContext.getResources(); 964 if (mSearchable.getVoiceLanguageModeId() != 0) { 965 languageModel = resources.getString(mSearchable.getVoiceLanguageModeId()); 966 } 967 if (mSearchable.getVoicePromptTextId() != 0) { 968 prompt = resources.getString(mSearchable.getVoicePromptTextId()); 969 } 970 if (mSearchable.getVoiceLanguageId() != 0) { 971 language = resources.getString(mSearchable.getVoiceLanguageId()); 972 } 973 if (mSearchable.getVoiceMaxResults() != 0) { 974 maxResults = mSearchable.getVoiceMaxResults(); 975 } 976 voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, languageModel); 977 voiceIntent.putExtra(RecognizerIntent.EXTRA_PROMPT, prompt); 978 voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, language); 979 voiceIntent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, maxResults); 980 voiceIntent.putExtra(EXTRA_CALLING_PACKAGE, 981 searchActivity == null ? null : searchActivity.toShortString()); 982 983 // Add the values that configure forwarding the results 984 voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT, pending); 985 voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT_BUNDLE, queryExtras); 986 987 return voiceIntent; 988 } 989 990 /** 991 * Corrects http/https typo errors in the given url string, and if the protocol specifier was 992 * not present defaults to http. 993 * 994 * @param inUrl URL to check and fix 995 * @return fixed URL string. 996 */ 997 private String fixUrl(String inUrl) { 998 if (inUrl.startsWith("http://") || inUrl.startsWith("https://")) 999 return inUrl; 1000 1001 if (inUrl.startsWith("http:") || inUrl.startsWith("https:")) { 1002 if (inUrl.startsWith("http:/") || inUrl.startsWith("https:/")) { 1003 inUrl = inUrl.replaceFirst("/", "//"); 1004 } else { 1005 inUrl = inUrl.replaceFirst(":", "://"); 1006 } 1007 } 1008 1009 if (inUrl.indexOf("://") == -1) { 1010 inUrl = "http://" + inUrl; 1011 } 1012 1013 return inUrl; 1014 } 1015 1016 /** 1017 * React to the user typing "enter" or other hardwired keys while typing in the search box. 1018 * This handles these special keys while the edit box has focus. 1019 */ 1020 View.OnKeyListener mTextKeyListener = new View.OnKeyListener() { 1021 public boolean onKey(View v, int keyCode, KeyEvent event) { 1022 // guard against possible race conditions 1023 if (mSearchable == null) { 1024 return false; 1025 } 1026 1027 if (DBG_LOG_TIMING) dbgLogTiming("doTextKey()"); 1028 if (DBG) { 1029 Log.d(LOG_TAG, "mTextListener.onKey(" + keyCode + "," + event 1030 + "), selection: " + mSearchAutoComplete.getListSelection()); 1031 } 1032 1033 // If a suggestion is selected, handle enter, search key, and action keys 1034 // as presses on the selected suggestion 1035 if (mSearchAutoComplete.isPopupShowing() && 1036 mSearchAutoComplete.getListSelection() != ListView.INVALID_POSITION) { 1037 return onSuggestionsKey(v, keyCode, event); 1038 } 1039 1040 // If there is text in the query box, handle enter, and action keys 1041 // The search key is handled by the dialog's onKeyDown(). 1042 if (!mSearchAutoComplete.isEmpty()) { 1043 if (keyCode == KeyEvent.KEYCODE_ENTER 1044 && event.getAction() == KeyEvent.ACTION_UP) { 1045 v.cancelLongPress(); 1046 1047 // If this is a url entered by the user & we displayed the 'Go' button which 1048 // the user clicked, launch the url instead of using it as a search query. 1049 if (mSearchable.autoUrlDetect() && 1050 (mSearchAutoCompleteImeOptions & EditorInfo.IME_MASK_ACTION) 1051 == EditorInfo.IME_ACTION_GO) { 1052 Uri uri = Uri.parse(fixUrl(mSearchAutoComplete.getText().toString())); 1053 Intent intent = new Intent(Intent.ACTION_VIEW, uri); 1054 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 1055 launchIntent(intent); 1056 } else { 1057 // Launch as a regular search. 1058 launchQuerySearch(); 1059 } 1060 return true; 1061 } 1062 if (event.getAction() == KeyEvent.ACTION_DOWN) { 1063 SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode); 1064 if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) { 1065 launchQuerySearch(keyCode, actionKey.getQueryActionMsg()); 1066 return true; 1067 } 1068 } 1069 } 1070 return false; 1071 } 1072 }; 1073 1074 @Override 1075 public void hide() { 1076 if (!isShowing()) return; 1077 1078 // We made sure the IME was displayed, so also make sure it is closed 1079 // when we go away. 1080 InputMethodManager imm = (InputMethodManager)getContext() 1081 .getSystemService(Context.INPUT_METHOD_SERVICE); 1082 if (imm != null) { 1083 imm.hideSoftInputFromWindow( 1084 getWindow().getDecorView().getWindowToken(), 0); 1085 } 1086 1087 super.hide(); 1088 } 1089 1090 /** 1091 * React to the user typing while in the suggestions list. First, check for action 1092 * keys. If not handled, try refocusing regular characters into the EditText. 1093 */ 1094 private boolean onSuggestionsKey(View v, int keyCode, KeyEvent event) { 1095 // guard against possible race conditions (late arrival after dismiss) 1096 if (mSearchable == null) { 1097 return false; 1098 } 1099 if (mSuggestionsAdapter == null) { 1100 return false; 1101 } 1102 if (event.getAction() == KeyEvent.ACTION_DOWN) { 1103 if (DBG_LOG_TIMING) { 1104 dbgLogTiming("onSuggestionsKey()"); 1105 } 1106 1107 // First, check for enter or search (both of which we'll treat as a "click") 1108 if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_SEARCH) { 1109 int position = mSearchAutoComplete.getListSelection(); 1110 return launchSuggestion(position); 1111 } 1112 1113 // Next, check for left/right moves, which we use to "return" the user to the edit view 1114 if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { 1115 // give "focus" to text editor, with cursor at the beginning if 1116 // left key, at end if right key 1117 // TODO: Reverse left/right for right-to-left languages, e.g. Arabic 1118 int selPoint = (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) ? 1119 0 : mSearchAutoComplete.length(); 1120 mSearchAutoComplete.setSelection(selPoint); 1121 mSearchAutoComplete.setListSelection(0); 1122 mSearchAutoComplete.clearListSelection(); 1123 mSearchAutoComplete.ensureImeVisible(); 1124 1125 return true; 1126 } 1127 1128 // Next, check for an "up and out" move 1129 if (keyCode == KeyEvent.KEYCODE_DPAD_UP 1130 && 0 == mSearchAutoComplete.getListSelection()) { 1131 restoreUserQuery(); 1132 // let ACTV complete the move 1133 return false; 1134 } 1135 1136 // Next, check for an "action key" 1137 SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode); 1138 if ((actionKey != null) && 1139 ((actionKey.getSuggestActionMsg() != null) || 1140 (actionKey.getSuggestActionMsgColumn() != null))) { 1141 // launch suggestion using action key column 1142 int position = mSearchAutoComplete.getListSelection(); 1143 if (position != ListView.INVALID_POSITION) { 1144 Cursor c = mSuggestionsAdapter.getCursor(); 1145 if (c.moveToPosition(position)) { 1146 final String actionMsg = getActionKeyMessage(c, actionKey); 1147 if (actionMsg != null && (actionMsg.length() > 0)) { 1148 return launchSuggestion(position, keyCode, actionMsg); 1149 } 1150 } 1151 } 1152 } 1153 } 1154 return false; 1155 } 1156 1157 /** 1158 * Launch a search for the text in the query text field. 1159 */ 1160 public void launchQuerySearch() { 1161 launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null); 1162 } 1163 1164 /** 1165 * Launch a search for the text in the query text field. 1166 * 1167 * @param actionKey The key code of the action key that was pressed, 1168 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none. 1169 * @param actionMsg The message for the action key that was pressed, 1170 * or <code>null</code> if none. 1171 */ 1172 protected void launchQuerySearch(int actionKey, String actionMsg) { 1173 String query = mSearchAutoComplete.getText().toString(); 1174 String action = mGlobalSearchMode ? Intent.ACTION_WEB_SEARCH : Intent.ACTION_SEARCH; 1175 Intent intent = createIntent(action, null, null, query, null, 1176 actionKey, actionMsg, null); 1177 // Allow GlobalSearch to log and create shortcut for searches launched by 1178 // the search button, enter key or an action key. 1179 if (mGlobalSearchMode) { 1180 mSuggestionsAdapter.reportSearch(query); 1181 } 1182 launchIntent(intent); 1183 } 1184 1185 /** 1186 * Launches an intent based on a suggestion. 1187 * 1188 * @param position The index of the suggestion to create the intent from. 1189 * @return true if a successful launch, false if could not (e.g. bad position). 1190 */ 1191 protected boolean launchSuggestion(int position) { 1192 return launchSuggestion(position, KeyEvent.KEYCODE_UNKNOWN, null); 1193 } 1194 1195 /** 1196 * Launches an intent based on a suggestion. 1197 * 1198 * @param position The index of the suggestion to create the intent from. 1199 * @param actionKey The key code of the action key that was pressed, 1200 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none. 1201 * @param actionMsg The message for the action key that was pressed, 1202 * or <code>null</code> if none. 1203 * @return true if a successful launch, false if could not (e.g. bad position). 1204 */ 1205 protected boolean launchSuggestion(int position, int actionKey, String actionMsg) { 1206 Cursor c = mSuggestionsAdapter.getCursor(); 1207 if ((c != null) && c.moveToPosition(position)) { 1208 1209 Intent intent = createIntentFromSuggestion(c, actionKey, actionMsg); 1210 1211 // report back about the click 1212 if (mGlobalSearchMode) { 1213 // in global search mode, do it via cursor 1214 mSuggestionsAdapter.callCursorOnClick(c, position, actionKey, actionMsg); 1215 } else if (intent != null 1216 && mPreviousComponents != null 1217 && !mPreviousComponents.isEmpty()) { 1218 // in-app search (and we have pivoted in as told by mPreviousComponents, 1219 // which is used for keeping track of what we pop back to when we are pivoting into 1220 // in app search.) 1221 reportInAppClickToGlobalSearch(c, intent); 1222 } 1223 1224 // launch the intent 1225 launchIntent(intent); 1226 1227 return true; 1228 } 1229 return false; 1230 } 1231 1232 /** 1233 * Report a click from an in app search result back to global search for shortcutting porpoises. 1234 * 1235 * @param c The cursor that is pointing to the clicked position. 1236 * @param intent The intent that will be launched for the click. 1237 */ 1238 private void reportInAppClickToGlobalSearch(Cursor c, Intent intent) { 1239 // for in app search, still tell global search via content provider 1240 Uri uri = getClickReportingUri(); 1241 final ContentValues cv = new ContentValues(); 1242 cv.put(SearchManager.SEARCH_CLICK_REPORT_COLUMN_QUERY, mUserQuery); 1243 final ComponentName source = mSearchable.getSearchActivity(); 1244 cv.put(SearchManager.SEARCH_CLICK_REPORT_COLUMN_COMPONENT, source.flattenToShortString()); 1245 1246 // grab the intent columns from the intent we created since it has additional 1247 // logic for falling back on the searchable default 1248 cv.put(SearchManager.SUGGEST_COLUMN_INTENT_ACTION, intent.getAction()); 1249 cv.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA, intent.getDataString()); 1250 cv.put(SearchManager.SUGGEST_COLUMN_INTENT_COMPONENT_NAME, 1251 intent.getComponent().flattenToShortString()); 1252 1253 // ensure the icons will work for global search 1254 cv.put(SearchManager.SUGGEST_COLUMN_ICON_1, 1255 wrapIconForPackage( 1256 mSearchable.getSuggestPackage(), 1257 getColumnString(c, SearchManager.SUGGEST_COLUMN_ICON_1))); 1258 cv.put(SearchManager.SUGGEST_COLUMN_ICON_2, 1259 wrapIconForPackage( 1260 mSearchable.getSuggestPackage(), 1261 getColumnString(c, SearchManager.SUGGEST_COLUMN_ICON_2))); 1262 1263 // the rest can be passed through directly 1264 cv.put(SearchManager.SUGGEST_COLUMN_FORMAT, 1265 getColumnString(c, SearchManager.SUGGEST_COLUMN_FORMAT)); 1266 cv.put(SearchManager.SUGGEST_COLUMN_TEXT_1, 1267 getColumnString(c, SearchManager.SUGGEST_COLUMN_TEXT_1)); 1268 cv.put(SearchManager.SUGGEST_COLUMN_TEXT_2, 1269 getColumnString(c, SearchManager.SUGGEST_COLUMN_TEXT_2)); 1270 cv.put(SearchManager.SUGGEST_COLUMN_QUERY, 1271 getColumnString(c, SearchManager.SUGGEST_COLUMN_QUERY)); 1272 cv.put(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID, 1273 getColumnString(c, SearchManager.SUGGEST_COLUMN_SHORTCUT_ID)); 1274 // note: deliberately omitting background color since it is only for global search 1275 // "more results" entries 1276 mContext.getContentResolver().insert(uri, cv); 1277 } 1278 1279 /** 1280 * @return A URI appropriate for reporting a click. 1281 */ 1282 private Uri getClickReportingUri() { 1283 Uri.Builder uriBuilder = new Uri.Builder() 1284 .scheme(ContentResolver.SCHEME_CONTENT) 1285 .authority(SearchManager.SEARCH_CLICK_REPORT_AUTHORITY); 1286 1287 uriBuilder.appendPath(SearchManager.SEARCH_CLICK_REPORT_URI_PATH); 1288 1289 return uriBuilder 1290 .query("") // TODO: Remove, workaround for a bug in Uri.writeToParcel() 1291 .fragment("") // TODO: Remove, workaround for a bug in Uri.writeToParcel() 1292 .build(); 1293 } 1294 1295 /** 1296 * Wraps an icon for a particular package. If the icon is a resource id, it is converted into 1297 * an android.resource:// URI. 1298 * 1299 * @param packageName The source of the icon 1300 * @param icon The icon retrieved from a suggestion column 1301 * @return An icon string appropriate for the package. 1302 */ 1303 private String wrapIconForPackage(String packageName, String icon) { 1304 if (icon == null || icon.length() == 0 || "0".equals(icon)) { 1305 // SearchManager specifies that null or zero can be returned to indicate 1306 // no icon. We also allow empty string. 1307 return null; 1308 } else if (!Character.isDigit(icon.charAt(0))){ 1309 return icon; 1310 } else { 1311 return new Uri.Builder() 1312 .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) 1313 .authority(packageName) 1314 .encodedPath(icon) 1315 .toString(); 1316 } 1317 } 1318 1319 /** 1320 * Launches an intent, including any special intent handling. 1321 */ 1322 private void launchIntent(Intent intent) { 1323 if (intent == null) { 1324 return; 1325 } 1326 if (handleSpecialIntent(intent)){ 1327 return; 1328 } 1329 Log.d(LOG_TAG, "launching " + intent); 1330 try { 1331 // in global search mode, we send the activity straight to the original suggestion 1332 // source. this is because GlobalSearch may not have permission to launch the 1333 // intent, and to avoid the extra step of going through GlobalSearch. 1334 if (mGlobalSearchMode) { 1335 launchGlobalSearchIntent(intent); 1336 if (mStoredComponentName != null) { 1337 // If we're embedded in an application, dismiss the dialog. 1338 // This ensures that if the intent is handled by the current 1339 // activity, it's not obscured by the dialog. 1340 dismiss(); 1341 } 1342 } else { 1343 // If the intent was created from a suggestion, it will always have an explicit 1344 // component here. 1345 Log.i(LOG_TAG, "Starting (as ourselves) " + intent.toURI()); 1346 getContext().startActivity(intent); 1347 // If the search switches to a different activity, 1348 // SearchDialogWrapper#performActivityResuming 1349 // will handle hiding the dialog when the next activity starts, but for 1350 // real in-app search, we still need to dismiss the dialog. 1351 if (isInRealAppSearch()) { 1352 dismiss(); 1353 } 1354 } 1355 } catch (RuntimeException ex) { 1356 Log.e(LOG_TAG, "Failed launch activity: " + intent, ex); 1357 } 1358 } 1359 1360 private void launchGlobalSearchIntent(Intent intent) { 1361 final String packageName; 1362 // GlobalSearch puts the original source of the suggestion in the 1363 // 'component name' column. If set, we send the intent to that activity. 1364 // We trust GlobalSearch to always set this to the suggestion source. 1365 String intentComponent = intent.getStringExtra(SearchManager.COMPONENT_NAME_KEY); 1366 if (intentComponent != null) { 1367 ComponentName componentName = ComponentName.unflattenFromString(intentComponent); 1368 intent.setComponent(componentName); 1369 intent.removeExtra(SearchManager.COMPONENT_NAME_KEY); 1370 // Launch the intent as the suggestion source. 1371 // This prevents sources from using the search dialog to launch 1372 // intents that they don't have permission for themselves. 1373 packageName = componentName.getPackageName(); 1374 } else { 1375 // If there is no component in the suggestion, it must be a built-in suggestion 1376 // from GlobalSearch (e.g. "Search the web for") or the intent 1377 // launched when pressing the search/go button in the search dialog. 1378 // Launch the intent with the permissions of GlobalSearch. 1379 packageName = mSearchable.getSearchActivity().getPackageName(); 1380 } 1381 1382 // Launch all global search suggestions as new tasks, since they don't relate 1383 // to the current task. 1384 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 1385 setBrowserApplicationId(intent); 1386 1387 startActivityInPackage(intent, packageName); 1388 } 1389 1390 /** 1391 * If the intent is to open an HTTP or HTTPS URL, we set 1392 * {@link Browser#EXTRA_APPLICATION_ID} so that any existing browser window that 1393 * has been opened by us for the same URL will be reused. 1394 */ 1395 private void setBrowserApplicationId(Intent intent) { 1396 Uri data = intent.getData(); 1397 if (Intent.ACTION_VIEW.equals(intent.getAction()) && data != null) { 1398 String scheme = data.getScheme(); 1399 if (scheme != null && scheme.startsWith("http")) { 1400 intent.putExtra(Browser.EXTRA_APPLICATION_ID, data.toString()); 1401 } 1402 } 1403 } 1404 1405 /** 1406 * Starts an activity as if it had been started by the given package. 1407 * 1408 * @param intent The description of the activity to start. 1409 * @param packageName 1410 * @throws ActivityNotFoundException If the intent could not be resolved to 1411 * and existing activity. 1412 * @throws SecurityException If the package does not have permission to start 1413 * start the activity. 1414 * @throws AndroidRuntimeException If some other error occurs. 1415 */ 1416 private void startActivityInPackage(Intent intent, String packageName) { 1417 try { 1418 int uid = ActivityThread.getPackageManager().getPackageUid(packageName); 1419 if (uid < 0) { 1420 throw new AndroidRuntimeException("Package UID not found " + packageName); 1421 } 1422 String resolvedType = intent.resolveTypeIfNeeded(getContext().getContentResolver()); 1423 IBinder resultTo = null; 1424 String resultWho = null; 1425 int requestCode = -1; 1426 boolean onlyIfNeeded = false; 1427 Log.i(LOG_TAG, "Starting (uid " + uid + ", " + packageName + ") " + intent.toURI()); 1428 int result = ActivityManagerNative.getDefault().startActivityInPackage( 1429 uid, intent, resolvedType, resultTo, resultWho, requestCode, onlyIfNeeded); 1430 checkStartActivityResult(result, intent); 1431 } catch (RemoteException ex) { 1432 throw new AndroidRuntimeException(ex); 1433 } 1434 } 1435 1436 // Stolen from Instrumentation.checkStartActivityResult() 1437 private static void checkStartActivityResult(int res, Intent intent) { 1438 if (res >= IActivityManager.START_SUCCESS) { 1439 return; 1440 } 1441 switch (res) { 1442 case IActivityManager.START_INTENT_NOT_RESOLVED: 1443 case IActivityManager.START_CLASS_NOT_FOUND: 1444 if (intent.getComponent() != null) 1445 throw new ActivityNotFoundException( 1446 "Unable to find explicit activity class " 1447 + intent.getComponent().toShortString() 1448 + "; have you declared this activity in your AndroidManifest.xml?"); 1449 throw new ActivityNotFoundException( 1450 "No Activity found to handle " + intent); 1451 case IActivityManager.START_PERMISSION_DENIED: 1452 throw new SecurityException("Not allowed to start activity " 1453 + intent); 1454 case IActivityManager.START_FORWARD_AND_REQUEST_CONFLICT: 1455 throw new AndroidRuntimeException( 1456 "FORWARD_RESULT_FLAG used while also requesting a result"); 1457 default: 1458 throw new AndroidRuntimeException("Unknown error code " 1459 + res + " when starting " + intent); 1460 } 1461 } 1462 1463 /** 1464 * Handles the special intent actions declared in {@link SearchManager}. 1465 * 1466 * @return <code>true</code> if the intent was handled. 1467 */ 1468 private boolean handleSpecialIntent(Intent intent) { 1469 String action = intent.getAction(); 1470 if (SearchManager.INTENT_ACTION_CHANGE_SEARCH_SOURCE.equals(action)) { 1471 handleChangeSourceIntent(intent); 1472 return true; 1473 } 1474 return false; 1475 } 1476 1477 /** 1478 * Handles {@link SearchManager#INTENT_ACTION_CHANGE_SEARCH_SOURCE}. 1479 */ 1480 private void handleChangeSourceIntent(Intent intent) { 1481 Uri dataUri = intent.getData(); 1482 if (dataUri == null) { 1483 Log.w(LOG_TAG, "SearchManager.INTENT_ACTION_CHANGE_SOURCE without intent data."); 1484 return; 1485 } 1486 ComponentName componentName = ComponentName.unflattenFromString(dataUri.toString()); 1487 if (componentName == null) { 1488 Log.w(LOG_TAG, "Invalid ComponentName: " + dataUri); 1489 return; 1490 } 1491 if (DBG) Log.d(LOG_TAG, "Switching to " + componentName); 1492 1493 pushPreviousComponent(mLaunchComponent); 1494 if (!show(componentName, mAppSearchData, false)) { 1495 Log.w(LOG_TAG, "Failed to switch to source " + componentName); 1496 popPreviousComponent(); 1497 return; 1498 } 1499 1500 String query = intent.getStringExtra(SearchManager.QUERY); 1501 setUserQuery(query); 1502 mSearchAutoComplete.showDropDown(); 1503 } 1504 1505 /** 1506 * Sets the list item selection in the AutoCompleteTextView's ListView. 1507 */ 1508 public void setListSelection(int index) { 1509 mSearchAutoComplete.setListSelection(index); 1510 } 1511 1512 /** 1513 * Checks if there are any previous searchable components in the history stack. 1514 */ 1515 private boolean hasPreviousComponent() { 1516 return mPreviousComponents != null && !mPreviousComponents.isEmpty(); 1517 } 1518 1519 /** 1520 * Saves the previous component that was searched, so that we can go 1521 * back to it. 1522 */ 1523 private void pushPreviousComponent(ComponentName componentName) { 1524 if (mPreviousComponents == null) { 1525 mPreviousComponents = new ArrayList<ComponentName>(); 1526 } 1527 mPreviousComponents.add(componentName); 1528 } 1529 1530 /** 1531 * Pops the previous component off the stack and returns it. 1532 * 1533 * @return The component name, or <code>null</code> if there was 1534 * no previous component. 1535 */ 1536 private ComponentName popPreviousComponent() { 1537 if (!hasPreviousComponent()) { 1538 return null; 1539 } 1540 return mPreviousComponents.remove(mPreviousComponents.size() - 1); 1541 } 1542 1543 /** 1544 * Goes back to the previous component that was searched, if any. 1545 * 1546 * @return <code>true</code> if there was a previous component that we could go back to. 1547 */ 1548 private boolean backToPreviousComponent() { 1549 ComponentName previous = popPreviousComponent(); 1550 if (previous == null) { 1551 return false; 1552 } 1553 1554 if (!show(previous, mAppSearchData, false)) { 1555 Log.w(LOG_TAG, "Failed to switch to source " + previous); 1556 return false; 1557 } 1558 1559 // must touch text to trigger suggestions 1560 // TODO: should this be the text as it was when the user left 1561 // the source that we are now going back to? 1562 String query = mSearchAutoComplete.getText().toString(); 1563 setUserQuery(query); 1564 return true; 1565 } 1566 1567 /** 1568 * When a particular suggestion has been selected, perform the various lookups required 1569 * to use the suggestion. This includes checking the cursor for suggestion-specific data, 1570 * and/or falling back to the XML for defaults; It also creates REST style Uri data when 1571 * the suggestion includes a data id. 1572 * 1573 * @param c The suggestions cursor, moved to the row of the user's selection 1574 * @param actionKey The key code of the action key that was pressed, 1575 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none. 1576 * @param actionMsg The message for the action key that was pressed, 1577 * or <code>null</code> if none. 1578 * @return An intent for the suggestion at the cursor's position. 1579 */ 1580 private Intent createIntentFromSuggestion(Cursor c, int actionKey, String actionMsg) { 1581 try { 1582 // use specific action if supplied, or default action if supplied, or fixed default 1583 String action = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_ACTION); 1584 1585 // some items are display only, or have effect via the cursor respond click reporting. 1586 if (SearchManager.INTENT_ACTION_NONE.equals(action)) { 1587 return null; 1588 } 1589 1590 if (action == null) { 1591 action = mSearchable.getSuggestIntentAction(); 1592 } 1593 if (action == null) { 1594 action = Intent.ACTION_SEARCH; 1595 } 1596 1597 // use specific data if supplied, or default data if supplied 1598 String data = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA); 1599 if (data == null) { 1600 data = mSearchable.getSuggestIntentData(); 1601 } 1602 // then, if an ID was provided, append it. 1603 if (data != null) { 1604 String id = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID); 1605 if (id != null) { 1606 data = data + "/" + Uri.encode(id); 1607 } 1608 } 1609 Uri dataUri = (data == null) ? null : Uri.parse(data); 1610 1611 String componentName = getColumnString( 1612 c, SearchManager.SUGGEST_COLUMN_INTENT_COMPONENT_NAME); 1613 1614 String query = getColumnString(c, SearchManager.SUGGEST_COLUMN_QUERY); 1615 String extraData = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA); 1616 String mode = mGlobalSearchMode ? SearchManager.MODE_GLOBAL_SEARCH_SUGGESTION : null; 1617 1618 return createIntent(action, dataUri, extraData, query, componentName, actionKey, 1619 actionMsg, mode); 1620 } catch (RuntimeException e ) { 1621 int rowNum; 1622 try { // be really paranoid now 1623 rowNum = c.getPosition(); 1624 } catch (RuntimeException e2 ) { 1625 rowNum = -1; 1626 } 1627 Log.w(LOG_TAG, "Search Suggestions cursor at row " + rowNum + 1628 " returned exception" + e.toString()); 1629 return null; 1630 } 1631 } 1632 1633 /** 1634 * Constructs an intent from the given information and the search dialog state. 1635 * 1636 * @param action Intent action. 1637 * @param data Intent data, or <code>null</code>. 1638 * @param extraData Data for {@link SearchManager#EXTRA_DATA_KEY} or <code>null</code>. 1639 * @param query Intent query, or <code>null</code>. 1640 * @param componentName Data for {@link SearchManager#COMPONENT_NAME_KEY} or <code>null</code>. 1641 * @param actionKey The key code of the action key that was pressed, 1642 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none. 1643 * @param actionMsg The message for the action key that was pressed, 1644 * or <code>null</code> if none. 1645 * @param mode The search mode, one of the acceptable values for 1646 * {@link SearchManager#SEARCH_MODE}, or {@code null}. 1647 * @return The intent. 1648 */ 1649 private Intent createIntent(String action, Uri data, String extraData, String query, 1650 String componentName, int actionKey, String actionMsg, String mode) { 1651 // Now build the Intent 1652 Intent intent = new Intent(action); 1653 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 1654 // We need CLEAR_TOP to avoid reusing an old task that has other activities 1655 // on top of the one we want. We don't want to do this in in-app search though, 1656 // as it can be destructive to the activity stack. 1657 if (mGlobalSearchMode) { 1658 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); 1659 } 1660 if (data != null) { 1661 intent.setData(data); 1662 } 1663 intent.putExtra(SearchManager.USER_QUERY, mUserQuery); 1664 if (query != null) { 1665 intent.putExtra(SearchManager.QUERY, query); 1666 } 1667 if (extraData != null) { 1668 intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData); 1669 } 1670 if (componentName != null) { 1671 intent.putExtra(SearchManager.COMPONENT_NAME_KEY, componentName); 1672 } 1673 if (mAppSearchData != null) { 1674 intent.putExtra(SearchManager.APP_DATA, mAppSearchData); 1675 } 1676 if (actionKey != KeyEvent.KEYCODE_UNKNOWN) { 1677 intent.putExtra(SearchManager.ACTION_KEY, actionKey); 1678 intent.putExtra(SearchManager.ACTION_MSG, actionMsg); 1679 } 1680 if (mode != null) { 1681 intent.putExtra(SearchManager.SEARCH_MODE, mode); 1682 } 1683 // Only allow 3rd-party intents from GlobalSearch 1684 if (!mGlobalSearchMode) { 1685 intent.setComponent(mSearchable.getSearchActivity()); 1686 } 1687 return intent; 1688 } 1689 1690 /** 1691 * For a given suggestion and a given cursor row, get the action message. If not provided 1692 * by the specific row/column, also check for a single definition (for the action key). 1693 * 1694 * @param c The cursor providing suggestions 1695 * @param actionKey The actionkey record being examined 1696 * 1697 * @return Returns a string, or null if no action key message for this suggestion 1698 */ 1699 private static String getActionKeyMessage(Cursor c, SearchableInfo.ActionKeyInfo actionKey) { 1700 String result = null; 1701 // check first in the cursor data, for a suggestion-specific message 1702 final String column = actionKey.getSuggestActionMsgColumn(); 1703 if (column != null) { 1704 result = SuggestionsAdapter.getColumnString(c, column); 1705 } 1706 // If the cursor didn't give us a message, see if there's a single message defined 1707 // for the actionkey (for all suggestions) 1708 if (result == null) { 1709 result = actionKey.getSuggestActionMsg(); 1710 } 1711 return result; 1712 } 1713 1714 /** 1715 * The root element in the search bar layout. This is a custom view just to override 1716 * the handling of the back button. 1717 */ 1718 public static class SearchBar extends LinearLayout { 1719 1720 private SearchDialog mSearchDialog; 1721 1722 public SearchBar(Context context, AttributeSet attrs) { 1723 super(context, attrs); 1724 } 1725 1726 public SearchBar(Context context) { 1727 super(context); 1728 } 1729 1730 public void setSearchDialog(SearchDialog searchDialog) { 1731 mSearchDialog = searchDialog; 1732 } 1733 1734 /** 1735 * Overrides the handling of the back key to move back to the previous sources or dismiss 1736 * the search dialog, instead of dismissing the input method. 1737 */ 1738 @Override 1739 public boolean dispatchKeyEventPreIme(KeyEvent event) { 1740 if (DBG) Log.d(LOG_TAG, "onKeyPreIme(" + event + ")"); 1741 if (mSearchDialog != null && event.getKeyCode() == KeyEvent.KEYCODE_BACK) { 1742 KeyEvent.DispatcherState state = getKeyDispatcherState(); 1743 if (state != null) { 1744 if (event.getAction() == KeyEvent.ACTION_DOWN 1745 && event.getRepeatCount() == 0) { 1746 state.startTracking(event, this); 1747 return true; 1748 } else if (event.getAction() == KeyEvent.ACTION_UP 1749 && !event.isCanceled() && state.isTracking(event)) { 1750 mSearchDialog.onBackPressed(); 1751 return true; 1752 } 1753 } 1754 } 1755 return super.dispatchKeyEventPreIme(event); 1756 } 1757 } 1758 1759 /** 1760 * Local subclass for AutoCompleteTextView. 1761 */ 1762 public static class SearchAutoComplete extends AutoCompleteTextView { 1763 1764 private int mThreshold; 1765 1766 public SearchAutoComplete(Context context) { 1767 super(context); 1768 mThreshold = getThreshold(); 1769 } 1770 1771 public SearchAutoComplete(Context context, AttributeSet attrs) { 1772 super(context, attrs); 1773 mThreshold = getThreshold(); 1774 } 1775 1776 public SearchAutoComplete(Context context, AttributeSet attrs, int defStyle) { 1777 super(context, attrs, defStyle); 1778 mThreshold = getThreshold(); 1779 } 1780 1781 @Override 1782 public void setThreshold(int threshold) { 1783 super.setThreshold(threshold); 1784 mThreshold = threshold; 1785 } 1786 1787 /** 1788 * Returns true if the text field is empty, or contains only whitespace. 1789 */ 1790 private boolean isEmpty() { 1791 return TextUtils.getTrimmedLength(getText()) == 0; 1792 } 1793 1794 /** 1795 * We override this method to avoid replacing the query box text 1796 * when a suggestion is clicked. 1797 */ 1798 @Override 1799 protected void replaceText(CharSequence text) { 1800 } 1801 1802 /** 1803 * We override this method to avoid an extra onItemClick being called on the 1804 * drop-down's OnItemClickListener by {@link AutoCompleteTextView#onKeyUp(int, KeyEvent)} 1805 * when an item is clicked with the trackball. 1806 */ 1807 @Override 1808 public void performCompletion() { 1809 } 1810 1811 /** 1812 * We override this method to be sure and show the soft keyboard if appropriate when 1813 * the TextView has focus. 1814 */ 1815 @Override 1816 public void onWindowFocusChanged(boolean hasWindowFocus) { 1817 super.onWindowFocusChanged(hasWindowFocus); 1818 1819 if (hasWindowFocus) { 1820 InputMethodManager inputManager = (InputMethodManager) 1821 getContext().getSystemService(Context.INPUT_METHOD_SERVICE); 1822 inputManager.showSoftInput(this, 0); 1823 } 1824 } 1825 1826 /** 1827 * We override this method so that we can allow a threshold of zero, which ACTV does not. 1828 */ 1829 @Override 1830 public boolean enoughToFilter() { 1831 return mThreshold <= 0 || super.enoughToFilter(); 1832 } 1833 1834 } 1835 1836 @Override 1837 public void onBackPressed() { 1838 // If the input method is covering the search dialog completely, 1839 // e.g. in landscape mode with no hard keyboard, dismiss just the input method 1840 InputMethodManager imm = (InputMethodManager)getContext() 1841 .getSystemService(Context.INPUT_METHOD_SERVICE); 1842 if (imm != null && imm.isFullscreenMode() && 1843 imm.hideSoftInputFromWindow(getWindow().getDecorView().getWindowToken(), 0)) { 1844 return; 1845 } 1846 // Otherwise, go back to any previous source (e.g. back to QSB when 1847 // pivoted into a source. 1848 if (!backToPreviousComponent()) { 1849 // If no previous source, close search dialog 1850 cancel(); 1851 } 1852 } 1853 1854 /** 1855 * Implements OnItemClickListener 1856 */ 1857 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 1858 if (DBG) Log.d(LOG_TAG, "onItemClick() position " + position); 1859 launchSuggestion(position); 1860 } 1861 1862 /** 1863 * Implements OnItemSelectedListener 1864 */ 1865 public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { 1866 if (DBG) Log.d(LOG_TAG, "onItemSelected() position " + position); 1867 // A suggestion has been selected, rewrite the query if possible, 1868 // otherwise the restore the original query. 1869 if (REWRITE_QUERIES) { 1870 rewriteQueryFromSuggestion(position); 1871 } 1872 } 1873 1874 /** 1875 * Implements OnItemSelectedListener 1876 */ 1877 public void onNothingSelected(AdapterView<?> parent) { 1878 if (DBG) Log.d(LOG_TAG, "onNothingSelected()"); 1879 } 1880 1881 /** 1882 * Query rewriting. 1883 */ 1884 1885 private void rewriteQueryFromSuggestion(int position) { 1886 Cursor c = mSuggestionsAdapter.getCursor(); 1887 if (c == null) { 1888 return; 1889 } 1890 if (c.moveToPosition(position)) { 1891 // Get the new query from the suggestion. 1892 CharSequence newQuery = mSuggestionsAdapter.convertToString(c); 1893 if (newQuery != null) { 1894 // The suggestion rewrites the query. 1895 if (DBG) Log.d(LOG_TAG, "Rewriting query to '" + newQuery + "'"); 1896 // Update the text field, without getting new suggestions. 1897 setQuery(newQuery); 1898 } else { 1899 // The suggestion does not rewrite the query, restore the user's query. 1900 if (DBG) Log.d(LOG_TAG, "Suggestion gives no rewrite, restoring user query."); 1901 restoreUserQuery(); 1902 } 1903 } else { 1904 // We got a bad position, restore the user's query. 1905 Log.w(LOG_TAG, "Bad suggestion position: " + position); 1906 restoreUserQuery(); 1907 } 1908 } 1909 1910 /** 1911 * Restores the query entered by the user if needed. 1912 */ 1913 private void restoreUserQuery() { 1914 if (DBG) Log.d(LOG_TAG, "Restoring query to '" + mUserQuery + "'"); 1915 setQuery(mUserQuery); 1916 } 1917 1918 /** 1919 * Sets the text in the query box, without updating the suggestions. 1920 */ 1921 private void setQuery(CharSequence query) { 1922 mSearchAutoComplete.setText(query, false); 1923 if (query != null) { 1924 mSearchAutoComplete.setSelection(query.length()); 1925 } 1926 } 1927 1928 /** 1929 * Sets the text in the query box, updating the suggestions. 1930 */ 1931 private void setUserQuery(String query) { 1932 if (query == null) { 1933 query = ""; 1934 } 1935 mUserQuery = query; 1936 mSourceSelector.setQuery(query); 1937 mSearchAutoComplete.setText(query); 1938 mSearchAutoComplete.setSelection(query.length()); 1939 } 1940 1941 /** 1942 * Debugging Support 1943 */ 1944 1945 /** 1946 * For debugging only, sample the millisecond clock and log it. 1947 * Uses AtomicLong so we can use in multiple threads 1948 */ 1949 private AtomicLong mLastLogTime = new AtomicLong(SystemClock.uptimeMillis()); 1950 private void dbgLogTiming(final String caller) { 1951 long millis = SystemClock.uptimeMillis(); 1952 long oldTime = mLastLogTime.getAndSet(millis); 1953 long delta = millis - oldTime; 1954 final String report = millis + " (+" + delta + ") ticks for Search keystroke in " + caller; 1955 Log.d(LOG_TAG,report); 1956 } 1957} 1958