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