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