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