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