SearchDialog.java revision 246529891ee289e8393ad4a486db785ef455c778
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 19 20import android.content.BroadcastReceiver; 21import android.content.ComponentName; 22import android.content.Context; 23import android.content.Intent; 24import android.content.IntentFilter; 25import android.content.pm.ActivityInfo; 26import android.content.pm.PackageManager; 27import android.content.pm.PackageManager.NameNotFoundException; 28import android.content.res.Configuration; 29import android.graphics.drawable.Drawable; 30import android.net.Uri; 31import android.os.Bundle; 32import android.speech.RecognizerIntent; 33import android.text.InputType; 34import android.text.TextUtils; 35import android.util.AttributeSet; 36import android.util.Log; 37import android.util.TypedValue; 38import android.view.ActionMode; 39import android.view.Gravity; 40import android.view.KeyEvent; 41import android.view.MotionEvent; 42import android.view.View; 43import android.view.ViewConfiguration; 44import android.view.ViewGroup; 45import android.view.Window; 46import android.view.WindowManager; 47import android.view.inputmethod.InputMethodManager; 48import android.widget.AutoCompleteTextView; 49import android.widget.ImageView; 50import android.widget.LinearLayout; 51import android.widget.SearchView; 52import android.widget.TextView; 53 54/** 55 * Search dialog. This is controlled by the 56 * SearchManager and runs in the current foreground process. 57 * 58 * @hide 59 */ 60public class SearchDialog extends Dialog { 61 62 // Debugging support 63 private static final boolean DBG = false; 64 private static final String LOG_TAG = "SearchDialog"; 65 66 private static final String INSTANCE_KEY_COMPONENT = "comp"; 67 private static final String INSTANCE_KEY_APPDATA = "data"; 68 private static final String INSTANCE_KEY_USER_QUERY = "uQry"; 69 70 // The string used for privateImeOptions to identify to the IME that it should not show 71 // a microphone button since one already exists in the search dialog. 72 private static final String IME_OPTION_NO_MICROPHONE = "nm"; 73 74 private static final int SEARCH_PLATE_LEFT_PADDING_NON_GLOBAL = 7; 75 76 // views & widgets 77 private TextView mBadgeLabel; 78 private ImageView mAppIcon; 79 private AutoCompleteTextView mSearchAutoComplete; 80 private View mSearchPlate; 81 private SearchView mSearchView; 82 private Drawable mWorkingSpinner; 83 private View mCloseSearch; 84 85 // interaction with searchable application 86 private SearchableInfo mSearchable; 87 private ComponentName mLaunchComponent; 88 private Bundle mAppSearchData; 89 private Context mActivityContext; 90 91 // For voice searching 92 private final Intent mVoiceWebSearchIntent; 93 private final Intent mVoiceAppSearchIntent; 94 95 // The query entered by the user. This is not changed when selecting a suggestion 96 // that modifies the contents of the text field. But if the user then edits 97 // the suggestion, the resulting string is saved. 98 private String mUserQuery; 99 100 // Last known IME options value for the search edit text. 101 private int mSearchAutoCompleteImeOptions; 102 103 private BroadcastReceiver mConfChangeListener = new BroadcastReceiver() { 104 @Override 105 public void onReceive(Context context, Intent intent) { 106 if (intent.getAction().equals(Intent.ACTION_CONFIGURATION_CHANGED)) { 107 onConfigurationChanged(); 108 } 109 } 110 }; 111 112 static int resolveDialogTheme(Context context) { 113 TypedValue outValue = new TypedValue(); 114 context.getTheme().resolveAttribute(com.android.internal.R.attr.searchDialogTheme, 115 outValue, true); 116 return outValue.resourceId; 117 } 118 119 /** 120 * Constructor - fires it up and makes it look like the search UI. 121 * 122 * @param context Application Context we can use for system acess 123 */ 124 public SearchDialog(Context context, SearchManager searchManager) { 125 super(context, resolveDialogTheme(context)); 126 127 // Save voice intent for later queries/launching 128 mVoiceWebSearchIntent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH); 129 mVoiceWebSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 130 mVoiceWebSearchIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, 131 RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH); 132 133 mVoiceAppSearchIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); 134 mVoiceAppSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 135 } 136 137 /** 138 * Create the search dialog and any resources that are used for the 139 * entire lifetime of the dialog. 140 */ 141 @Override 142 protected void onCreate(Bundle savedInstanceState) { 143 super.onCreate(savedInstanceState); 144 145 Window theWindow = getWindow(); 146 WindowManager.LayoutParams lp = theWindow.getAttributes(); 147 lp.width = ViewGroup.LayoutParams.MATCH_PARENT; 148 // taking up the whole window (even when transparent) is less than ideal, 149 // but necessary to show the popup window until the window manager supports 150 // having windows anchored by their parent but not clipped by them. 151 lp.height = ViewGroup.LayoutParams.MATCH_PARENT; 152 lp.gravity = Gravity.TOP | Gravity.FILL_HORIZONTAL; 153 lp.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; 154 theWindow.setAttributes(lp); 155 156 // Touching outside of the search dialog will dismiss it 157 setCanceledOnTouchOutside(true); 158 } 159 160 /** 161 * We recreate the dialog view each time it becomes visible so as to limit 162 * the scope of any problems with the contained resources. 163 */ 164 private void createContentView() { 165 setContentView(com.android.internal.R.layout.search_bar); 166 167 // get the view elements for local access 168 SearchBar searchBar = (SearchBar) findViewById(com.android.internal.R.id.search_bar); 169 searchBar.setSearchDialog(this); 170 mSearchView = (SearchView) findViewById(com.android.internal.R.id.search_view); 171 mSearchView.setOnCloseListener(mOnCloseListener); 172 mSearchView.setOnQueryTextListener(mOnQueryChangeListener); 173 mSearchView.setOnSuggestionListener(mOnSuggestionSelectionListener); 174 175 mCloseSearch = findViewById(com.android.internal.R.id.closeButton); 176 mCloseSearch.setOnClickListener(new View.OnClickListener() { 177 @Override 178 public void onClick(View v) { 179 dismiss(); 180 } 181 }); 182 183 // TODO: Move the badge logic to SearchView or move the badge to search_bar.xml 184 mBadgeLabel = (TextView) mSearchView.findViewById(com.android.internal.R.id.search_badge); 185 mSearchAutoComplete = (AutoCompleteTextView) 186 mSearchView.findViewById(com.android.internal.R.id.search_src_text); 187 mAppIcon = (ImageView) findViewById(com.android.internal.R.id.search_app_icon); 188 mSearchPlate = mSearchView.findViewById(com.android.internal.R.id.search_plate); 189 mWorkingSpinner = getContext().getResources(). 190 getDrawable(com.android.internal.R.drawable.search_spinner); 191 mSearchAutoComplete.setCompoundDrawablesWithIntrinsicBounds( 192 null, null, mWorkingSpinner, null); 193 setWorking(false); 194 195 // pre-hide all the extraneous elements 196 mBadgeLabel.setVisibility(View.GONE); 197 198 // Additional adjustments to make Dialog work for Search 199 mSearchAutoCompleteImeOptions = mSearchAutoComplete.getImeOptions(); 200 } 201 202 /** 203 * Set up the search dialog 204 * 205 * @return true if search dialog launched, false if not 206 */ 207 public boolean show(String initialQuery, boolean selectInitialQuery, 208 ComponentName componentName, Bundle appSearchData) { 209 boolean success = doShow(initialQuery, selectInitialQuery, componentName, appSearchData); 210 if (success) { 211 // Display the drop down as soon as possible instead of waiting for the rest of the 212 // pending UI stuff to get done, so that things appear faster to the user. 213 mSearchAutoComplete.showDropDownAfterLayout(); 214 } 215 return success; 216 } 217 218 /** 219 * Does the rest of the work required to show the search dialog. Called by 220 * {@link #show(String, boolean, ComponentName, Bundle)} and 221 * 222 * @return true if search dialog showed, false if not 223 */ 224 private boolean doShow(String initialQuery, boolean selectInitialQuery, 225 ComponentName componentName, Bundle appSearchData) { 226 // set up the searchable and show the dialog 227 if (!show(componentName, appSearchData)) { 228 return false; 229 } 230 231 // finally, load the user's initial text (which may trigger suggestions) 232 setUserQuery(initialQuery); 233 if (selectInitialQuery) { 234 mSearchAutoComplete.selectAll(); 235 } 236 237 return true; 238 } 239 240 /** 241 * Sets up the search dialog and shows it. 242 * 243 * @return <code>true</code> if search dialog launched 244 */ 245 private boolean show(ComponentName componentName, Bundle appSearchData) { 246 247 if (DBG) { 248 Log.d(LOG_TAG, "show(" + componentName + ", " 249 + appSearchData + ")"); 250 } 251 252 SearchManager searchManager = (SearchManager) 253 mContext.getSystemService(Context.SEARCH_SERVICE); 254 // Try to get the searchable info for the provided component. 255 mSearchable = searchManager.getSearchableInfo(componentName); 256 257 if (mSearchable == null) { 258 return false; 259 } 260 261 mLaunchComponent = componentName; 262 mAppSearchData = appSearchData; 263 mActivityContext = mSearchable.getActivityContext(getContext()); 264 265 // show the dialog. this will call onStart(). 266 if (!isShowing()) { 267 // Recreate the search bar view every time the dialog is shown, to get rid 268 // of any bad state in the AutoCompleteTextView etc 269 createContentView(); 270 mSearchView.setSearchableInfo(mSearchable); 271 mSearchView.setAppSearchData(mAppSearchData); 272 273 show(); 274 } 275 updateUI(); 276 277 return true; 278 } 279 280 @Override 281 public void onStart() { 282 super.onStart(); 283 284 // Register a listener for configuration change events. 285 IntentFilter filter = new IntentFilter(); 286 filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED); 287 getContext().registerReceiver(mConfChangeListener, filter); 288 } 289 290 /** 291 * The search dialog is being dismissed, so handle all of the local shutdown operations. 292 * 293 * This function is designed to be idempotent so that dismiss() can be safely called at any time 294 * (even if already closed) and more likely to really dump any memory. No leaks! 295 */ 296 @Override 297 public void onStop() { 298 super.onStop(); 299 300 getContext().unregisterReceiver(mConfChangeListener); 301 302 // dump extra memory we're hanging on to 303 mLaunchComponent = null; 304 mAppSearchData = null; 305 mSearchable = null; 306 mUserQuery = null; 307 } 308 309 /** 310 * Sets the search dialog to the 'working' state, which shows a working spinner in the 311 * right hand size of the text field. 312 * 313 * @param working true to show spinner, false to hide spinner 314 */ 315 public void setWorking(boolean working) { 316 mWorkingSpinner.setAlpha(working ? 255 : 0); 317 mWorkingSpinner.setVisible(working, false); 318 mWorkingSpinner.invalidateSelf(); 319 } 320 321 /** 322 * Save the minimal set of data necessary to recreate the search 323 * 324 * @return A bundle with the state of the dialog, or {@code null} if the search 325 * dialog is not showing. 326 */ 327 @Override 328 public Bundle onSaveInstanceState() { 329 if (!isShowing()) return null; 330 331 Bundle bundle = new Bundle(); 332 333 // setup info so I can recreate this particular search 334 bundle.putParcelable(INSTANCE_KEY_COMPONENT, mLaunchComponent); 335 bundle.putBundle(INSTANCE_KEY_APPDATA, mAppSearchData); 336 bundle.putString(INSTANCE_KEY_USER_QUERY, mUserQuery); 337 338 return bundle; 339 } 340 341 /** 342 * Restore the state of the dialog from a previously saved bundle. 343 * 344 * @param savedInstanceState The state of the dialog previously saved by 345 * {@link #onSaveInstanceState()}. 346 */ 347 @Override 348 public void onRestoreInstanceState(Bundle savedInstanceState) { 349 if (savedInstanceState == null) return; 350 351 ComponentName launchComponent = savedInstanceState.getParcelable(INSTANCE_KEY_COMPONENT); 352 Bundle appSearchData = savedInstanceState.getBundle(INSTANCE_KEY_APPDATA); 353 String userQuery = savedInstanceState.getString(INSTANCE_KEY_USER_QUERY); 354 355 // show the dialog. 356 if (!doShow(userQuery, false, launchComponent, appSearchData)) { 357 // for some reason, we couldn't re-instantiate 358 return; 359 } 360 } 361 362 /** 363 * Called after resources have changed, e.g. after screen rotation or locale change. 364 */ 365 public void onConfigurationChanged() { 366 if (mSearchable != null && isShowing()) { 367 // Redraw (resources may have changed) 368 updateSearchAppIcon(); 369 updateSearchBadge(); 370 if (isLandscapeMode(getContext())) { 371 mSearchAutoComplete.ensureImeVisible(true); 372 } 373 } 374 } 375 376 static boolean isLandscapeMode(Context context) { 377 return context.getResources().getConfiguration().orientation 378 == Configuration.ORIENTATION_LANDSCAPE; 379 } 380 381 /** 382 * Update the UI according to the info in the current value of {@link #mSearchable}. 383 */ 384 private void updateUI() { 385 if (mSearchable != null) { 386 mDecor.setVisibility(View.VISIBLE); 387 updateSearchAutoComplete(); 388 updateSearchAppIcon(); 389 updateSearchBadge(); 390 391 // In order to properly configure the input method (if one is being used), we 392 // need to let it know if we'll be providing suggestions. Although it would be 393 // difficult/expensive to know if every last detail has been configured properly, we 394 // can at least see if a suggestions provider has been configured, and use that 395 // as our trigger. 396 int inputType = mSearchable.getInputType(); 397 // We only touch this if the input type is set up for text (which it almost certainly 398 // should be, in the case of search!) 399 if ((inputType & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_TEXT) { 400 // The existence of a suggestions authority is the proxy for "suggestions 401 // are available here" 402 inputType &= ~InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE; 403 if (mSearchable.getSuggestAuthority() != null) { 404 inputType |= InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE; 405 } 406 } 407 mSearchAutoComplete.setInputType(inputType); 408 mSearchAutoCompleteImeOptions = mSearchable.getImeOptions(); 409 mSearchAutoComplete.setImeOptions(mSearchAutoCompleteImeOptions); 410 411 // If the search dialog is going to show a voice search button, then don't let 412 // the soft keyboard display a microphone button if it would have otherwise. 413 if (mSearchable.getVoiceSearchEnabled()) { 414 mSearchAutoComplete.setPrivateImeOptions(IME_OPTION_NO_MICROPHONE); 415 } else { 416 mSearchAutoComplete.setPrivateImeOptions(null); 417 } 418 } 419 } 420 421 /** 422 * Updates the auto-complete text view. 423 */ 424 private void updateSearchAutoComplete() { 425 // we dismiss the entire dialog instead 426 mSearchAutoComplete.setDropDownDismissedOnCompletion(false); 427 mSearchAutoComplete.setForceIgnoreOutsideTouch(false); 428 } 429 430 private void updateSearchAppIcon() { 431 PackageManager pm = getContext().getPackageManager(); 432 Drawable icon; 433 try { 434 ActivityInfo info = pm.getActivityInfo(mLaunchComponent, 0); 435 icon = pm.getApplicationIcon(info.applicationInfo); 436 if (DBG) 437 Log.d(LOG_TAG, "Using app-specific icon"); 438 } catch (NameNotFoundException e) { 439 icon = pm.getDefaultActivityIcon(); 440 Log.w(LOG_TAG, mLaunchComponent + " not found, using generic app icon"); 441 } 442 mAppIcon.setImageDrawable(icon); 443 mAppIcon.setVisibility(View.VISIBLE); 444 mSearchPlate.setPadding(SEARCH_PLATE_LEFT_PADDING_NON_GLOBAL, mSearchPlate.getPaddingTop(), mSearchPlate.getPaddingRight(), mSearchPlate.getPaddingBottom()); 445 } 446 447 /** 448 * Setup the search "Badge" if requested by mode flags. 449 */ 450 private void updateSearchBadge() { 451 // assume both hidden 452 int visibility = View.GONE; 453 Drawable icon = null; 454 CharSequence text = null; 455 456 // optionally show one or the other. 457 if (mSearchable.useBadgeIcon()) { 458 icon = mActivityContext.getResources().getDrawable(mSearchable.getIconId()); 459 visibility = View.VISIBLE; 460 if (DBG) Log.d(LOG_TAG, "Using badge icon: " + mSearchable.getIconId()); 461 } else if (mSearchable.useBadgeLabel()) { 462 text = mActivityContext.getResources().getText(mSearchable.getLabelId()).toString(); 463 visibility = View.VISIBLE; 464 if (DBG) Log.d(LOG_TAG, "Using badge label: " + mSearchable.getLabelId()); 465 } 466 467 mBadgeLabel.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null); 468 mBadgeLabel.setText(text); 469 mBadgeLabel.setVisibility(visibility); 470 } 471 472 /* 473 * Listeners of various types 474 */ 475 476 /** 477 * {@link Dialog#onTouchEvent(MotionEvent)} will cancel the dialog only when the 478 * touch is outside the window. But the window includes space for the drop-down, 479 * so we also cancel on taps outside the search bar when the drop-down is not showing. 480 */ 481 @Override 482 public boolean onTouchEvent(MotionEvent event) { 483 // cancel if the drop-down is not showing and the touch event was outside the search plate 484 if (!mSearchAutoComplete.isPopupShowing() && isOutOfBounds(mSearchPlate, event)) { 485 if (DBG) Log.d(LOG_TAG, "Pop-up not showing and outside of search plate."); 486 cancel(); 487 return true; 488 } 489 // Let Dialog handle events outside the window while the pop-up is showing. 490 return super.onTouchEvent(event); 491 } 492 493 private boolean isOutOfBounds(View v, MotionEvent event) { 494 final int x = (int) event.getX(); 495 final int y = (int) event.getY(); 496 final int slop = ViewConfiguration.get(mContext).getScaledWindowTouchSlop(); 497 return (x < -slop) || (y < -slop) 498 || (x > (v.getWidth()+slop)) 499 || (y > (v.getHeight()+slop)); 500 } 501 502 @Override 503 public void hide() { 504 if (!isShowing()) return; 505 506 // We made sure the IME was displayed, so also make sure it is closed 507 // when we go away. 508 InputMethodManager imm = (InputMethodManager)getContext() 509 .getSystemService(Context.INPUT_METHOD_SERVICE); 510 if (imm != null) { 511 imm.hideSoftInputFromWindow( 512 getWindow().getDecorView().getWindowToken(), 0); 513 } 514 515 super.hide(); 516 } 517 518 /** 519 * Launch a search for the text in the query text field. 520 */ 521 public void launchQuerySearch() { 522 launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null); 523 } 524 525 /** 526 * Launch a search for the text in the query text field. 527 * 528 * @param actionKey The key code of the action key that was pressed, 529 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none. 530 * @param actionMsg The message for the action key that was pressed, 531 * or <code>null</code> if none. 532 */ 533 protected void launchQuerySearch(int actionKey, String actionMsg) { 534 String query = mSearchAutoComplete.getText().toString(); 535 String action = Intent.ACTION_SEARCH; 536 Intent intent = createIntent(action, null, null, query, actionKey, actionMsg); 537 launchIntent(intent); 538 } 539 540 /** 541 * Launches an intent, including any special intent handling. 542 */ 543 private void launchIntent(Intent intent) { 544 if (intent == null) { 545 return; 546 } 547 Log.d(LOG_TAG, "launching " + intent); 548 try { 549 // If the intent was created from a suggestion, it will always have an explicit 550 // component here. 551 Log.i(LOG_TAG, "Starting (as ourselves) " + intent.toUri(0)); 552 getContext().startActivity(intent); 553 // If the search switches to a different activity, 554 // SearchDialogWrapper#performActivityResuming 555 // will handle hiding the dialog when the next activity starts, but for 556 // real in-app search, we still need to dismiss the dialog. 557 dismiss(); 558 } catch (RuntimeException ex) { 559 Log.e(LOG_TAG, "Failed launch activity: " + intent, ex); 560 } 561 } 562 563 /** 564 * Sets the list item selection in the AutoCompleteTextView's ListView. 565 */ 566 public void setListSelection(int index) { 567 mSearchAutoComplete.setListSelection(index); 568 } 569 570 /** 571 * Constructs an intent from the given information and the search dialog state. 572 * 573 * @param action Intent action. 574 * @param data Intent data, or <code>null</code>. 575 * @param extraData Data for {@link SearchManager#EXTRA_DATA_KEY} or <code>null</code>. 576 * @param query Intent query, or <code>null</code>. 577 * @param actionKey The key code of the action key that was pressed, 578 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none. 579 * @param actionMsg The message for the action key that was pressed, 580 * or <code>null</code> if none. 581 * @param mode The search mode, one of the acceptable values for 582 * {@link SearchManager#SEARCH_MODE}, or {@code null}. 583 * @return The intent. 584 */ 585 private Intent createIntent(String action, Uri data, String extraData, String query, 586 int actionKey, String actionMsg) { 587 // Now build the Intent 588 Intent intent = new Intent(action); 589 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 590 // We need CLEAR_TOP to avoid reusing an old task that has other activities 591 // on top of the one we want. We don't want to do this in in-app search though, 592 // as it can be destructive to the activity stack. 593 if (data != null) { 594 intent.setData(data); 595 } 596 intent.putExtra(SearchManager.USER_QUERY, mUserQuery); 597 if (query != null) { 598 intent.putExtra(SearchManager.QUERY, query); 599 } 600 if (extraData != null) { 601 intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData); 602 } 603 if (mAppSearchData != null) { 604 intent.putExtra(SearchManager.APP_DATA, mAppSearchData); 605 } 606 if (actionKey != KeyEvent.KEYCODE_UNKNOWN) { 607 intent.putExtra(SearchManager.ACTION_KEY, actionKey); 608 intent.putExtra(SearchManager.ACTION_MSG, actionMsg); 609 } 610 intent.setComponent(mSearchable.getSearchActivity()); 611 return intent; 612 } 613 614 /** 615 * The root element in the search bar layout. This is a custom view just to override 616 * the handling of the back button. 617 */ 618 public static class SearchBar extends LinearLayout { 619 620 private SearchDialog mSearchDialog; 621 622 public SearchBar(Context context, AttributeSet attrs) { 623 super(context, attrs); 624 } 625 626 public SearchBar(Context context) { 627 super(context); 628 } 629 630 public void setSearchDialog(SearchDialog searchDialog) { 631 mSearchDialog = searchDialog; 632 } 633 634 /** 635 * Overrides the handling of the back key to move back to the previous 636 * sources or dismiss the search dialog, instead of dismissing the input 637 * method. 638 */ 639 @Override 640 public boolean dispatchKeyEventPreIme(KeyEvent event) { 641 if (DBG) 642 Log.d(LOG_TAG, "onKeyPreIme(" + event + ")"); 643 if (mSearchDialog != null && event.getKeyCode() == KeyEvent.KEYCODE_BACK) { 644 KeyEvent.DispatcherState state = getKeyDispatcherState(); 645 if (state != null) { 646 if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) { 647 state.startTracking(event, this); 648 return true; 649 } else if (event.getAction() == KeyEvent.ACTION_UP && !event.isCanceled() 650 && state.isTracking(event)) { 651 mSearchDialog.onBackPressed(); 652 return true; 653 } 654 } 655 } 656 return super.dispatchKeyEventPreIme(event); 657 } 658 659 /** 660 * Don't allow action modes in a SearchBar, it looks silly. 661 */ 662 @Override 663 public ActionMode startActionModeForChild(View child, ActionMode.Callback callback) { 664 return null; 665 } 666 } 667 668 private boolean isEmpty(AutoCompleteTextView actv) { 669 return TextUtils.getTrimmedLength(actv.getText()) == 0; 670 } 671 672 @Override 673 public void onBackPressed() { 674 // If the input method is covering the search dialog completely, 675 // e.g. in landscape mode with no hard keyboard, dismiss just the input method 676 InputMethodManager imm = (InputMethodManager)getContext() 677 .getSystemService(Context.INPUT_METHOD_SERVICE); 678 if (imm != null && imm.isFullscreenMode() && 679 imm.hideSoftInputFromWindow(getWindow().getDecorView().getWindowToken(), 0)) { 680 return; 681 } 682 // Close search dialog 683 cancel(); 684 } 685 686 private boolean onClosePressed() { 687 // Dismiss the dialog if close button is pressed when there's no query text 688 if (isEmpty(mSearchAutoComplete)) { 689 dismiss(); 690 return true; 691 } 692 693 return false; 694 } 695 696 private final SearchView.OnCloseListener mOnCloseListener = new SearchView.OnCloseListener() { 697 698 public boolean onClose() { 699 return onClosePressed(); 700 } 701 }; 702 703 private final SearchView.OnQueryTextListener mOnQueryChangeListener = 704 new SearchView.OnQueryTextListener() { 705 706 public boolean onQueryTextSubmit(String query) { 707 dismiss(); 708 return false; 709 } 710 711 public boolean onQueryTextChange(String newText) { 712 return false; 713 } 714 }; 715 716 private final SearchView.OnSuggestionListener mOnSuggestionSelectionListener = 717 new SearchView.OnSuggestionListener() { 718 719 public boolean onSuggestionSelect(int position) { 720 return false; 721 } 722 723 public boolean onSuggestionClick(int position) { 724 dismiss(); 725 return false; 726 } 727 }; 728 729 /** 730 * Sets the text in the query box, updating the suggestions. 731 */ 732 private void setUserQuery(String query) { 733 if (query == null) { 734 query = ""; 735 } 736 mUserQuery = query; 737 mSearchAutoComplete.setText(query); 738 mSearchAutoComplete.setSelection(query.length()); 739 } 740} 741