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