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