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