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