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