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