1 /* 2 * Copyright (C) 2014 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.support.v7.widget; 18 19 import android.annotation.TargetApi; 20 import android.app.PendingIntent; 21 import android.app.SearchManager; 22 import android.app.SearchableInfo; 23 import android.content.ActivityNotFoundException; 24 import android.content.ComponentName; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.pm.PackageManager; 28 import android.content.pm.ResolveInfo; 29 import android.content.res.Configuration; 30 import android.content.res.Resources; 31 import android.database.Cursor; 32 import android.graphics.Rect; 33 import android.graphics.drawable.Drawable; 34 import android.net.Uri; 35 import android.os.Build; 36 import android.os.Bundle; 37 import android.os.ResultReceiver; 38 import android.speech.RecognizerIntent; 39 import android.support.v4.view.KeyEventCompat; 40 import android.support.v4.widget.CursorAdapter; 41 import android.support.v7.appcompat.R; 42 import android.support.v7.internal.widget.TintManager; 43 import android.support.v7.internal.widget.TintTypedArray; 44 import android.support.v7.internal.widget.ViewUtils; 45 import android.support.v7.view.CollapsibleActionView; 46 import android.text.Editable; 47 import android.text.InputType; 48 import android.text.Spannable; 49 import android.text.SpannableStringBuilder; 50 import android.text.TextUtils; 51 import android.text.TextWatcher; 52 import android.text.style.ImageSpan; 53 import android.util.AttributeSet; 54 import android.util.Log; 55 import android.view.KeyEvent; 56 import android.view.LayoutInflater; 57 import android.view.View; 58 import android.view.ViewTreeObserver; 59 import android.view.inputmethod.EditorInfo; 60 import android.view.inputmethod.InputMethodManager; 61 import android.widget.AdapterView; 62 import android.widget.AdapterView.OnItemClickListener; 63 import android.widget.AdapterView.OnItemSelectedListener; 64 import android.widget.AutoCompleteTextView; 65 import android.widget.ImageView; 66 import android.widget.ListView; 67 import android.widget.TextView; 68 import android.widget.TextView.OnEditorActionListener; 69 70 import java.lang.reflect.Method; 71 import java.util.WeakHashMap; 72 73 import static android.support.v7.widget.SuggestionsAdapter.getColumnString; 74 75 /** 76 * A widget that provides a user interface for the user to enter a search query and submit a request 77 * to a search provider. Shows a list of query suggestions or results, if available, and allows the 78 * user to pick a suggestion or result to launch into. 79 * 80 * <p class="note"><strong>Note:</strong> This class is included in the <a 81 * href="{@docRoot}tools/extras/support-library.html">support library</a> for compatibility 82 * with API level 7 and higher. If you're developing your app for API level 11 and higher 83 * <em>only</em>, you should instead use the framework {@link android.widget.SearchView} class.</p> 84 * 85 * <p> 86 * When the SearchView is used in an {@link android.support.v7.app.ActionBar} 87 * as an action view, it's collapsed by default, so you must provide an icon for the action. 88 * </p> 89 * <p> 90 * If you want the search field to always be visible, then call 91 * {@link #setIconifiedByDefault(boolean) setIconifiedByDefault(false)}. 92 * </p> 93 * 94 * <div class="special reference"> 95 * <h3>Developer Guides</h3> 96 * <p>For information about using {@code SearchView}, read the 97 * <a href="{@docRoot}guide/topics/search/index.html">Search</a> API guide. 98 * Additional information about action views is also available in the <<a 99 * href="{@docRoot}guide/topics/ui/actionbar.html#ActionView">Action Bar</a> API guide</p> 100 * </div> 101 * 102 * @see android.support.v4.view.MenuItemCompat#SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW 103 */ 104 public class SearchView extends LinearLayoutCompat implements CollapsibleActionView { 105 106 private static final boolean DBG = false; 107 private static final String LOG_TAG = "SearchView"; 108 109 private static final boolean IS_AT_LEAST_FROYO = Build.VERSION.SDK_INT >= 8; 110 111 /** 112 * Private constant for removing the microphone in the keyboard. 113 */ 114 private static final String IME_OPTION_NO_MICROPHONE = "nm"; 115 116 private final SearchAutoComplete mQueryTextView; 117 private final View mSearchEditFrame; 118 private final View mSearchPlate; 119 private final View mSubmitArea; 120 private final ImageView mSearchButton; 121 private final ImageView mSubmitButton; 122 private final ImageView mCloseButton; 123 private final ImageView mVoiceButton; 124 private final ImageView mSearchHintIcon; 125 private final View mDropDownAnchor; 126 private final int mSearchIconResId; 127 128 // Resources used by SuggestionsAdapter to display suggestions. 129 private final int mSuggestionRowLayout; 130 private final int mSuggestionCommitIconResId; 131 132 // Intents used for voice searching. 133 private final Intent mVoiceWebSearchIntent; 134 private final Intent mVoiceAppSearchIntent; 135 private OnQueryTextListener mOnQueryChangeListener; 136 private OnCloseListener mOnCloseListener; 137 private OnFocusChangeListener mOnQueryTextFocusChangeListener; 138 private OnSuggestionListener mOnSuggestionListener; 139 private OnClickListener mOnSearchClickListener; 140 141 private boolean mIconifiedByDefault; 142 private boolean mIconified; 143 private CursorAdapter mSuggestionsAdapter; 144 private boolean mSubmitButtonEnabled; 145 private CharSequence mQueryHint; 146 private boolean mQueryRefinement; 147 private boolean mClearingFocus; 148 private int mMaxWidth; 149 private boolean mVoiceButtonEnabled; 150 private CharSequence mOldQueryText; 151 private CharSequence mUserQuery; 152 private boolean mExpandedInActionView; 153 private int mCollapsedImeOptions; 154 155 private SearchableInfo mSearchable; 156 private Bundle mAppSearchData; 157 158 private final TintManager mTintManager; 159 160 static final AutoCompleteTextViewReflector HIDDEN_METHOD_INVOKER = new AutoCompleteTextViewReflector(); 161 162 /* 163 * SearchView can be set expanded before the IME is ready to be shown during 164 * initial UI setup. The show operation is asynchronous to account for this. 165 */ 166 private Runnable mShowImeRunnable = new Runnable() { 167 public void run() { 168 InputMethodManager imm = (InputMethodManager) 169 getContext().getSystemService(Context.INPUT_METHOD_SERVICE); 170 171 if (imm != null) { 172 HIDDEN_METHOD_INVOKER.showSoftInputUnchecked(imm, SearchView.this, 0); 173 } 174 } 175 }; 176 177 private final Runnable mUpdateDrawableStateRunnable = new Runnable() { 178 public void run() { 179 updateFocusedState(); 180 } 181 }; 182 183 private Runnable mReleaseCursorRunnable = new Runnable() { 184 public void run() { 185 if (mSuggestionsAdapter != null && mSuggestionsAdapter instanceof SuggestionsAdapter) { 186 mSuggestionsAdapter.changeCursor(null); 187 } 188 } 189 }; 190 191 // A weak map of drawables we've gotten from other packages, so we don't load them 192 // more than once. 193 private final WeakHashMap<String, Drawable.ConstantState> mOutsideDrawablesCache = 194 new WeakHashMap<String, Drawable.ConstantState>(); 195 196 /** 197 * Callbacks for changes to the query text. 198 */ 199 public interface OnQueryTextListener { 200 201 /** 202 * Called when the user submits the query. This could be due to a key press on the 203 * keyboard or due to pressing a submit button. 204 * The listener can override the standard behavior by returning true 205 * to indicate that it has handled the submit request. Otherwise return false to 206 * let the SearchView handle the submission by launching any associated intent. 207 * 208 * @param query the query text that is to be submitted 209 * 210 * @return true if the query has been handled by the listener, false to let the 211 * SearchView perform the default action. 212 */ onQueryTextSubmit(String query)213 boolean onQueryTextSubmit(String query); 214 215 /** 216 * Called when the query text is changed by the user. 217 * 218 * @param newText the new content of the query text field. 219 * 220 * @return false if the SearchView should perform the default action of showing any 221 * suggestions if available, true if the action was handled by the listener. 222 */ onQueryTextChange(String newText)223 boolean onQueryTextChange(String newText); 224 } 225 226 public interface OnCloseListener { 227 228 /** 229 * The user is attempting to close the SearchView. 230 * 231 * @return true if the listener wants to override the default behavior of clearing the 232 * text field and dismissing it, false otherwise. 233 */ onClose()234 boolean onClose(); 235 } 236 237 /** 238 * Callback interface for selection events on suggestions. These callbacks 239 * are only relevant when a SearchableInfo has been specified by {@link #setSearchableInfo}. 240 */ 241 public interface OnSuggestionListener { 242 243 /** 244 * Called when a suggestion was selected by navigating to it. 245 * @param position the absolute position in the list of suggestions. 246 * 247 * @return true if the listener handles the event and wants to override the default 248 * behavior of possibly rewriting the query based on the selected item, false otherwise. 249 */ onSuggestionSelect(int position)250 boolean onSuggestionSelect(int position); 251 252 /** 253 * Called when a suggestion was clicked. 254 * @param position the absolute position of the clicked item in the list of suggestions. 255 * 256 * @return true if the listener handles the event and wants to override the default 257 * behavior of launching any intent or submitting a search query specified on that item. 258 * Return false otherwise. 259 */ onSuggestionClick(int position)260 boolean onSuggestionClick(int position); 261 } 262 SearchView(Context context)263 public SearchView(Context context) { 264 this(context, null); 265 } 266 SearchView(Context context, AttributeSet attrs)267 public SearchView(Context context, AttributeSet attrs) { 268 this(context, attrs, R.attr.searchViewStyle); 269 } 270 SearchView(Context context, AttributeSet attrs, int defStyleAttr)271 public SearchView(Context context, AttributeSet attrs, int defStyleAttr) { 272 super(context, attrs, defStyleAttr); 273 274 final TintTypedArray a = TintTypedArray.obtainStyledAttributes(context, 275 attrs, R.styleable.SearchView, defStyleAttr, 0); 276 // Keep the TintManager in case we need it later 277 mTintManager = a.getTintManager(); 278 279 final LayoutInflater inflater = (LayoutInflater) context.getSystemService( 280 Context.LAYOUT_INFLATER_SERVICE); 281 final int layoutResId = a.getResourceId(R.styleable.SearchView_layout, 0); 282 inflater.inflate(layoutResId, this, true); 283 mQueryTextView = (SearchAutoComplete) findViewById(R.id.search_src_text); 284 mQueryTextView.setSearchView(this); 285 286 mSearchEditFrame = findViewById(R.id.search_edit_frame); 287 mSearchPlate = findViewById(R.id.search_plate); 288 mSubmitArea = findViewById(R.id.submit_area); 289 mSearchButton = (ImageView) findViewById(R.id.search_button); 290 mSubmitButton = (ImageView) findViewById(R.id.search_go_btn); 291 mCloseButton = (ImageView) findViewById(R.id.search_close_btn); 292 mVoiceButton = (ImageView) findViewById(R.id.search_voice_btn); 293 mSearchHintIcon = (ImageView) findViewById(R.id.search_mag_icon); 294 // Set up icons and backgrounds. 295 mSearchPlate.setBackgroundDrawable(a.getDrawable(R.styleable.SearchView_queryBackground)); 296 mSubmitArea.setBackgroundDrawable(a.getDrawable(R.styleable.SearchView_submitBackground)); 297 mSearchIconResId = a.getResourceId(R.styleable.SearchView_searchIcon, 0); 298 mSearchButton.setImageResource(mSearchIconResId); 299 mSubmitButton.setImageDrawable(a.getDrawable(R.styleable.SearchView_goIcon)); 300 mCloseButton.setImageDrawable(a.getDrawable(R.styleable.SearchView_closeIcon)); 301 mVoiceButton.setImageDrawable(a.getDrawable(R.styleable.SearchView_voiceIcon)); 302 mSearchHintIcon.setImageDrawable(a.getDrawable(R.styleable.SearchView_searchIcon)); 303 304 // Extract dropdown layout resource IDs for later use. 305 mSuggestionRowLayout = a.getResourceId(R.styleable.SearchView_suggestionRowLayout, 0); 306 mSuggestionCommitIconResId = a.getResourceId(R.styleable.SearchView_commitIcon, 0); 307 308 mSearchButton.setOnClickListener(mOnClickListener); 309 mCloseButton.setOnClickListener(mOnClickListener); 310 mSubmitButton.setOnClickListener(mOnClickListener); 311 mVoiceButton.setOnClickListener(mOnClickListener); 312 mQueryTextView.setOnClickListener(mOnClickListener); 313 314 mQueryTextView.addTextChangedListener(mTextWatcher); 315 mQueryTextView.setOnEditorActionListener(mOnEditorActionListener); 316 mQueryTextView.setOnItemClickListener(mOnItemClickListener); 317 mQueryTextView.setOnItemSelectedListener(mOnItemSelectedListener); 318 mQueryTextView.setOnKeyListener(mTextKeyListener); 319 // Inform any listener of focus changes 320 mQueryTextView.setOnFocusChangeListener(new OnFocusChangeListener() { 321 322 public void onFocusChange(View v, boolean hasFocus) { 323 if (mOnQueryTextFocusChangeListener != null) { 324 mOnQueryTextFocusChangeListener.onFocusChange(SearchView.this, hasFocus); 325 } 326 } 327 }); 328 setIconifiedByDefault(a.getBoolean(R.styleable.SearchView_iconifiedByDefault, true)); 329 330 final int maxWidth = a.getDimensionPixelSize(R.styleable.SearchView_android_maxWidth, -1); 331 if (maxWidth != -1) { 332 setMaxWidth(maxWidth); 333 } 334 final CharSequence queryHint = a.getText(R.styleable.SearchView_queryHint); 335 if (!TextUtils.isEmpty(queryHint)) { 336 setQueryHint(queryHint); 337 } 338 final int imeOptions = a.getInt(R.styleable.SearchView_android_imeOptions, -1); 339 if (imeOptions != -1) { 340 setImeOptions(imeOptions); 341 } 342 final int inputType = a.getInt(R.styleable.SearchView_android_inputType, -1); 343 if (inputType != -1) { 344 setInputType(inputType); 345 } 346 347 boolean focusable = true; 348 focusable = a.getBoolean(R.styleable.SearchView_android_focusable, focusable); 349 setFocusable(focusable); 350 351 a.recycle(); 352 353 // Save voice intent for later queries/launching 354 mVoiceWebSearchIntent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH); 355 mVoiceWebSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 356 mVoiceWebSearchIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, 357 RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH); 358 359 mVoiceAppSearchIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); 360 mVoiceAppSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 361 362 mDropDownAnchor = findViewById(mQueryTextView.getDropDownAnchor()); 363 if (mDropDownAnchor != null) { 364 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { 365 addOnLayoutChangeListenerToDropDownAnchorSDK11(); 366 } else { 367 addOnLayoutChangeListenerToDropDownAnchorBase(); 368 } 369 } 370 371 updateViewsVisibility(mIconifiedByDefault); 372 updateQueryHint(); 373 } 374 375 @TargetApi(Build.VERSION_CODES.HONEYCOMB) addOnLayoutChangeListenerToDropDownAnchorSDK11()376 private void addOnLayoutChangeListenerToDropDownAnchorSDK11() { 377 mDropDownAnchor.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { 378 @Override 379 public void onLayoutChange(View v, int left, int top, int right, int bottom, 380 int oldLeft, int oldTop, int oldRight, int oldBottom) { 381 adjustDropDownSizeAndPosition(); 382 } 383 }); 384 } 385 addOnLayoutChangeListenerToDropDownAnchorBase()386 private void addOnLayoutChangeListenerToDropDownAnchorBase() { 387 mDropDownAnchor.getViewTreeObserver() 388 .addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { 389 @Override 390 public void onGlobalLayout() { 391 adjustDropDownSizeAndPosition(); 392 } 393 }); 394 } 395 getSuggestionRowLayout()396 int getSuggestionRowLayout() { 397 return mSuggestionRowLayout; 398 } 399 getSuggestionCommitIconResId()400 int getSuggestionCommitIconResId() { 401 return mSuggestionCommitIconResId; 402 } 403 404 /** 405 * Sets the SearchableInfo for this SearchView. Properties in the SearchableInfo are used 406 * to display labels, hints, suggestions, create intents for launching search results screens 407 * and controlling other affordances such as a voice button. 408 * 409 * @param searchable a SearchableInfo can be retrieved from the SearchManager, for a specific 410 * activity or a global search provider. 411 */ setSearchableInfo(SearchableInfo searchable)412 public void setSearchableInfo(SearchableInfo searchable) { 413 mSearchable = searchable; 414 if (mSearchable != null) { 415 if (IS_AT_LEAST_FROYO) { 416 updateSearchAutoComplete(); 417 } 418 updateQueryHint(); 419 } 420 // Cache the voice search capability 421 mVoiceButtonEnabled = IS_AT_LEAST_FROYO && hasVoiceSearch(); 422 423 if (mVoiceButtonEnabled) { 424 // Disable the microphone on the keyboard, as a mic is displayed near the text box 425 // TODO: use imeOptions to disable voice input when the new API will be available 426 mQueryTextView.setPrivateImeOptions(IME_OPTION_NO_MICROPHONE); 427 } 428 updateViewsVisibility(isIconified()); 429 } 430 431 /** 432 * Sets the APP_DATA for legacy SearchDialog use. 433 * @param appSearchData bundle provided by the app when launching the search dialog 434 * @hide 435 */ setAppSearchData(Bundle appSearchData)436 public void setAppSearchData(Bundle appSearchData) { 437 mAppSearchData = appSearchData; 438 } 439 440 /** 441 * Sets the IME options on the query text field. 442 * 443 * @see TextView#setImeOptions(int) 444 * @param imeOptions the options to set on the query text field 445 */ setImeOptions(int imeOptions)446 public void setImeOptions(int imeOptions) { 447 mQueryTextView.setImeOptions(imeOptions); 448 } 449 450 /** 451 * Returns the IME options set on the query text field. 452 * @return the ime options 453 * @see TextView#setImeOptions(int) 454 */ getImeOptions()455 public int getImeOptions() { 456 return mQueryTextView.getImeOptions(); 457 } 458 459 /** 460 * Sets the input type on the query text field. 461 * 462 * @see TextView#setInputType(int) 463 * @param inputType the input type to set on the query text field 464 */ setInputType(int inputType)465 public void setInputType(int inputType) { 466 mQueryTextView.setInputType(inputType); 467 } 468 469 /** 470 * Returns the input type set on the query text field. 471 * @return the input type 472 */ getInputType()473 public int getInputType() { 474 return mQueryTextView.getInputType(); 475 } 476 477 /** @hide */ 478 @Override requestFocus(int direction, Rect previouslyFocusedRect)479 public boolean requestFocus(int direction, Rect previouslyFocusedRect) { 480 // Don't accept focus if in the middle of clearing focus 481 if (mClearingFocus) return false; 482 // Check if SearchView is focusable. 483 if (!isFocusable()) return false; 484 // If it is not iconified, then give the focus to the text field 485 if (!isIconified()) { 486 boolean result = mQueryTextView.requestFocus(direction, previouslyFocusedRect); 487 if (result) { 488 updateViewsVisibility(false); 489 } 490 return result; 491 } else { 492 return super.requestFocus(direction, previouslyFocusedRect); 493 } 494 } 495 496 /** @hide */ 497 @Override clearFocus()498 public void clearFocus() { 499 mClearingFocus = true; 500 setImeVisibility(false); 501 super.clearFocus(); 502 mQueryTextView.clearFocus(); 503 mClearingFocus = false; 504 } 505 506 /** 507 * Sets a listener for user actions within the SearchView. 508 * 509 * @param listener the listener object that receives callbacks when the user performs 510 * actions in the SearchView such as clicking on buttons or typing a query. 511 */ setOnQueryTextListener(OnQueryTextListener listener)512 public void setOnQueryTextListener(OnQueryTextListener listener) { 513 mOnQueryChangeListener = listener; 514 } 515 516 /** 517 * Sets a listener to inform when the user closes the SearchView. 518 * 519 * @param listener the listener to call when the user closes the SearchView. 520 */ setOnCloseListener(OnCloseListener listener)521 public void setOnCloseListener(OnCloseListener listener) { 522 mOnCloseListener = listener; 523 } 524 525 /** 526 * Sets a listener to inform when the focus of the query text field changes. 527 * 528 * @param listener the listener to inform of focus changes. 529 */ setOnQueryTextFocusChangeListener(OnFocusChangeListener listener)530 public void setOnQueryTextFocusChangeListener(OnFocusChangeListener listener) { 531 mOnQueryTextFocusChangeListener = listener; 532 } 533 534 /** 535 * Sets a listener to inform when a suggestion is focused or clicked. 536 * 537 * @param listener the listener to inform of suggestion selection events. 538 */ setOnSuggestionListener(OnSuggestionListener listener)539 public void setOnSuggestionListener(OnSuggestionListener listener) { 540 mOnSuggestionListener = listener; 541 } 542 543 /** 544 * Sets a listener to inform when the search button is pressed. This is only 545 * relevant when the text field is not visible by default. Calling {@link #setIconified 546 * setIconified(false)} can also cause this listener to be informed. 547 * 548 * @param listener the listener to inform when the search button is clicked or 549 * the text field is programmatically de-iconified. 550 */ setOnSearchClickListener(OnClickListener listener)551 public void setOnSearchClickListener(OnClickListener listener) { 552 mOnSearchClickListener = listener; 553 } 554 555 /** 556 * Returns the query string currently in the text field. 557 * 558 * @return the query string 559 */ getQuery()560 public CharSequence getQuery() { 561 return mQueryTextView.getText(); 562 } 563 564 /** 565 * Sets a query string in the text field and optionally submits the query as well. 566 * 567 * @param query the query string. This replaces any query text already present in the 568 * text field. 569 * @param submit whether to submit the query right now or only update the contents of 570 * text field. 571 */ setQuery(CharSequence query, boolean submit)572 public void setQuery(CharSequence query, boolean submit) { 573 mQueryTextView.setText(query); 574 if (query != null) { 575 mQueryTextView.setSelection(mQueryTextView.length()); 576 mUserQuery = query; 577 } 578 579 // If the query is not empty and submit is requested, submit the query 580 if (submit && !TextUtils.isEmpty(query)) { 581 onSubmitQuery(); 582 } 583 } 584 585 /** 586 * Sets the hint text to display in the query text field. This overrides any hint specified 587 * in the SearchableInfo. 588 * 589 * @param hint the hint text to display 590 */ setQueryHint(CharSequence hint)591 public void setQueryHint(CharSequence hint) { 592 mQueryHint = hint; 593 updateQueryHint(); 594 } 595 596 /** 597 * Gets the hint text to display in the query text field. 598 * @return the query hint text, if specified, null otherwise. 599 */ getQueryHint()600 public CharSequence getQueryHint() { 601 if (mQueryHint != null) { 602 return mQueryHint; 603 } else if (IS_AT_LEAST_FROYO && mSearchable != null) { 604 CharSequence hint = null; 605 int hintId = mSearchable.getHintId(); 606 if (hintId != 0) { 607 hint = getContext().getString(hintId); 608 } 609 return hint; 610 } 611 return null; 612 } 613 614 /** 615 * Sets the default or resting state of the search field. If true, a single search icon is 616 * shown by default and expands to show the text field and other buttons when pressed. Also, 617 * if the default state is iconified, then it collapses to that state when the close button 618 * is pressed. Changes to this property will take effect immediately. 619 * 620 * <p>The default value is true.</p> 621 * 622 * @param iconified whether the search field should be iconified by default 623 */ setIconifiedByDefault(boolean iconified)624 public void setIconifiedByDefault(boolean iconified) { 625 if (mIconifiedByDefault == iconified) return; 626 mIconifiedByDefault = iconified; 627 updateViewsVisibility(iconified); 628 updateQueryHint(); 629 } 630 631 /** 632 * Returns the default iconified state of the search field. 633 * @return 634 */ isIconfiedByDefault()635 public boolean isIconfiedByDefault() { 636 return mIconifiedByDefault; 637 } 638 639 /** 640 * Iconifies or expands the SearchView. Any query text is cleared when iconified. This is 641 * a temporary state and does not override the default iconified state set by 642 * {@link #setIconifiedByDefault(boolean)}. If the default state is iconified, then 643 * a false here will only be valid until the user closes the field. And if the default 644 * state is expanded, then a true here will only clear the text field and not close it. 645 * 646 * @param iconify a true value will collapse the SearchView to an icon, while a false will 647 * expand it. 648 */ setIconified(boolean iconify)649 public void setIconified(boolean iconify) { 650 if (iconify) { 651 onCloseClicked(); 652 } else { 653 onSearchClicked(); 654 } 655 } 656 657 /** 658 * Returns the current iconified state of the SearchView. 659 * 660 * @return true if the SearchView is currently iconified, false if the search field is 661 * fully visible. 662 */ isIconified()663 public boolean isIconified() { 664 return mIconified; 665 } 666 667 /** 668 * Enables showing a submit button when the query is non-empty. In cases where the SearchView 669 * is being used to filter the contents of the current activity and doesn't launch a separate 670 * results activity, then the submit button should be disabled. 671 * 672 * @param enabled true to show a submit button for submitting queries, false if a submit 673 * button is not required. 674 */ setSubmitButtonEnabled(boolean enabled)675 public void setSubmitButtonEnabled(boolean enabled) { 676 mSubmitButtonEnabled = enabled; 677 updateViewsVisibility(isIconified()); 678 } 679 680 /** 681 * Returns whether the submit button is enabled when necessary or never displayed. 682 * 683 * @return whether the submit button is enabled automatically when necessary 684 */ isSubmitButtonEnabled()685 public boolean isSubmitButtonEnabled() { 686 return mSubmitButtonEnabled; 687 } 688 689 /** 690 * Specifies if a query refinement button should be displayed alongside each suggestion 691 * or if it should depend on the flags set in the individual items retrieved from the 692 * suggestions provider. Clicking on the query refinement button will replace the text 693 * in the query text field with the text from the suggestion. This flag only takes effect 694 * if a SearchableInfo has been specified with {@link #setSearchableInfo(SearchableInfo)} 695 * and not when using a custom adapter. 696 * 697 * @param enable true if all items should have a query refinement button, false if only 698 * those items that have a query refinement flag set should have the button. 699 * 700 * @see SearchManager#SUGGEST_COLUMN_FLAGS 701 * @see SearchManager#FLAG_QUERY_REFINEMENT 702 */ setQueryRefinementEnabled(boolean enable)703 public void setQueryRefinementEnabled(boolean enable) { 704 mQueryRefinement = enable; 705 if (mSuggestionsAdapter instanceof SuggestionsAdapter) { 706 ((SuggestionsAdapter) mSuggestionsAdapter).setQueryRefinement( 707 enable ? SuggestionsAdapter.REFINE_ALL : SuggestionsAdapter.REFINE_BY_ENTRY); 708 } 709 } 710 711 /** 712 * Returns whether query refinement is enabled for all items or only specific ones. 713 * @return true if enabled for all items, false otherwise. 714 */ isQueryRefinementEnabled()715 public boolean isQueryRefinementEnabled() { 716 return mQueryRefinement; 717 } 718 719 /** 720 * You can set a custom adapter if you wish. Otherwise the default adapter is used to 721 * display the suggestions from the suggestions provider associated with the SearchableInfo. 722 * 723 * @see #setSearchableInfo(SearchableInfo) 724 */ setSuggestionsAdapter(CursorAdapter adapter)725 public void setSuggestionsAdapter(CursorAdapter adapter) { 726 mSuggestionsAdapter = adapter; 727 728 mQueryTextView.setAdapter(mSuggestionsAdapter); 729 } 730 731 /** 732 * Returns the adapter used for suggestions, if any. 733 * @return the suggestions adapter 734 */ getSuggestionsAdapter()735 public CursorAdapter getSuggestionsAdapter() { 736 return mSuggestionsAdapter; 737 } 738 739 /** 740 * Makes the view at most this many pixels wide 741 */ setMaxWidth(int maxpixels)742 public void setMaxWidth(int maxpixels) { 743 mMaxWidth = maxpixels; 744 745 requestLayout(); 746 } 747 748 /** 749 * Gets the specified maximum width in pixels, if set. Returns zero if 750 * no maximum width was specified. 751 * @return the maximum width of the view 752 */ getMaxWidth()753 public int getMaxWidth() { 754 return mMaxWidth; 755 } 756 757 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)758 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 759 // Let the standard measurements take effect in iconified state. 760 if (isIconified()) { 761 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 762 return; 763 } 764 765 int widthMode = MeasureSpec.getMode(widthMeasureSpec); 766 int width = MeasureSpec.getSize(widthMeasureSpec); 767 768 switch (widthMode) { 769 case MeasureSpec.AT_MOST: 770 // If there is an upper limit, don't exceed maximum width (explicit or implicit) 771 if (mMaxWidth > 0) { 772 width = Math.min(mMaxWidth, width); 773 } else { 774 width = Math.min(getPreferredWidth(), width); 775 } 776 break; 777 case MeasureSpec.EXACTLY: 778 // If an exact width is specified, still don't exceed any specified maximum width 779 if (mMaxWidth > 0) { 780 width = Math.min(mMaxWidth, width); 781 } 782 break; 783 case MeasureSpec.UNSPECIFIED: 784 // Use maximum width, if specified, else preferred width 785 width = mMaxWidth > 0 ? mMaxWidth : getPreferredWidth(); 786 break; 787 } 788 widthMode = MeasureSpec.EXACTLY; 789 super.onMeasure(MeasureSpec.makeMeasureSpec(width, widthMode), heightMeasureSpec); 790 } 791 getPreferredWidth()792 private int getPreferredWidth() { 793 return getContext().getResources() 794 .getDimensionPixelSize(R.dimen.abc_search_view_preferred_width); 795 } 796 updateViewsVisibility(final boolean collapsed)797 private void updateViewsVisibility(final boolean collapsed) { 798 mIconified = collapsed; 799 // Visibility of views that are visible when collapsed 800 final int visCollapsed = collapsed ? VISIBLE : GONE; 801 // Is there text in the query 802 final boolean hasText = !TextUtils.isEmpty(mQueryTextView.getText()); 803 804 mSearchButton.setVisibility(visCollapsed); 805 updateSubmitButton(hasText); 806 mSearchEditFrame.setVisibility(collapsed ? GONE : VISIBLE); 807 mSearchHintIcon.setVisibility(mIconifiedByDefault ? GONE : VISIBLE); 808 updateCloseButton(); 809 updateVoiceButton(!hasText); 810 updateSubmitArea(); 811 } 812 813 @TargetApi(Build.VERSION_CODES.FROYO) hasVoiceSearch()814 private boolean hasVoiceSearch() { 815 if (mSearchable != null && 816 mSearchable.getVoiceSearchEnabled()) { 817 Intent testIntent = null; 818 if (mSearchable.getVoiceSearchLaunchWebSearch()) { 819 testIntent = mVoiceWebSearchIntent; 820 } else if (mSearchable.getVoiceSearchLaunchRecognizer()) { 821 testIntent = mVoiceAppSearchIntent; 822 } 823 if (testIntent != null) { 824 ResolveInfo ri = getContext().getPackageManager().resolveActivity(testIntent, 825 PackageManager.MATCH_DEFAULT_ONLY); 826 return ri != null; 827 } 828 } 829 return false; 830 } 831 isSubmitAreaEnabled()832 private boolean isSubmitAreaEnabled() { 833 return (mSubmitButtonEnabled || mVoiceButtonEnabled) && !isIconified(); 834 } 835 updateSubmitButton(boolean hasText)836 private void updateSubmitButton(boolean hasText) { 837 int visibility = GONE; 838 if (mSubmitButtonEnabled && isSubmitAreaEnabled() && hasFocus() 839 && (hasText || !mVoiceButtonEnabled)) { 840 visibility = VISIBLE; 841 } 842 mSubmitButton.setVisibility(visibility); 843 } 844 updateSubmitArea()845 private void updateSubmitArea() { 846 int visibility = GONE; 847 if (isSubmitAreaEnabled() 848 && (mSubmitButton.getVisibility() == VISIBLE 849 || mVoiceButton.getVisibility() == VISIBLE)) { 850 visibility = VISIBLE; 851 } 852 mSubmitArea.setVisibility(visibility); 853 } 854 updateCloseButton()855 private void updateCloseButton() { 856 final boolean hasText = !TextUtils.isEmpty(mQueryTextView.getText()); 857 // Should we show the close button? It is not shown if there's no focus, 858 // field is not iconified by default and there is no text in it. 859 final boolean showClose = hasText || (mIconifiedByDefault && !mExpandedInActionView); 860 mCloseButton.setVisibility(showClose ? VISIBLE : GONE); 861 mCloseButton.getDrawable().setState(hasText ? ENABLED_STATE_SET : EMPTY_STATE_SET); 862 } 863 postUpdateFocusedState()864 private void postUpdateFocusedState() { 865 post(mUpdateDrawableStateRunnable); 866 } 867 updateFocusedState()868 private void updateFocusedState() { 869 boolean focused = mQueryTextView.hasFocus(); 870 mSearchPlate.getBackground().setState(focused ? ENABLED_FOCUSED_STATE_SET : EMPTY_STATE_SET); 871 mSubmitArea.getBackground().setState(focused ? ENABLED_FOCUSED_STATE_SET : EMPTY_STATE_SET); 872 invalidate(); 873 } 874 875 @Override onDetachedFromWindow()876 protected void onDetachedFromWindow() { 877 removeCallbacks(mUpdateDrawableStateRunnable); 878 post(mReleaseCursorRunnable); 879 super.onDetachedFromWindow(); 880 } 881 setImeVisibility(final boolean visible)882 private void setImeVisibility(final boolean visible) { 883 if (visible) { 884 post(mShowImeRunnable); 885 } else { 886 removeCallbacks(mShowImeRunnable); 887 InputMethodManager imm = (InputMethodManager) 888 getContext().getSystemService(Context.INPUT_METHOD_SERVICE); 889 890 if (imm != null) { 891 imm.hideSoftInputFromWindow(getWindowToken(), 0); 892 } 893 } 894 } 895 896 /** 897 * Called by the SuggestionsAdapter 898 * @hide 899 */ onQueryRefine(CharSequence queryText)900 /* package */void onQueryRefine(CharSequence queryText) { 901 setQuery(queryText); 902 } 903 904 private final OnClickListener mOnClickListener = new OnClickListener() { 905 906 public void onClick(View v) { 907 if (v == mSearchButton) { 908 onSearchClicked(); 909 } else if (v == mCloseButton) { 910 onCloseClicked(); 911 } else if (v == mSubmitButton) { 912 onSubmitQuery(); 913 } else if (v == mVoiceButton) { 914 if (IS_AT_LEAST_FROYO) { 915 onVoiceClicked(); 916 } 917 } else if (v == mQueryTextView) { 918 forceSuggestionQuery(); 919 } 920 } 921 }; 922 923 /** 924 * React to the user typing "enter" or other hardwired keys while typing in 925 * the search box. This handles these special keys while the edit box has 926 * focus. 927 */ 928 View.OnKeyListener mTextKeyListener = new View.OnKeyListener() { 929 public boolean onKey(View v, int keyCode, KeyEvent event) { 930 // guard against possible race conditions 931 if (mSearchable == null) { 932 return false; 933 } 934 935 if (DBG) { 936 Log.d(LOG_TAG, "mTextListener.onKey(" + keyCode + "," + event + "), selection: " 937 + mQueryTextView.getListSelection()); 938 } 939 940 // If a suggestion is selected, handle enter, search key, and action keys 941 // as presses on the selected suggestion 942 if (mQueryTextView.isPopupShowing() 943 && mQueryTextView.getListSelection() != ListView.INVALID_POSITION) { 944 return onSuggestionsKey(v, keyCode, event); 945 } 946 947 // If there is text in the query box, handle enter, and action keys 948 // The search key is handled by the dialog's onKeyDown(). 949 if (!mQueryTextView.isEmpty() && KeyEventCompat.hasNoModifiers(event)) { 950 if (event.getAction() == KeyEvent.ACTION_UP) { 951 if (keyCode == KeyEvent.KEYCODE_ENTER) { 952 v.cancelLongPress(); 953 954 // Launch as a regular search. 955 launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null, mQueryTextView.getText() 956 .toString()); 957 return true; 958 } 959 } 960 } 961 return false; 962 } 963 }; 964 965 /** 966 * React to the user typing while in the suggestions list. First, check for 967 * action keys. If not handled, try refocusing regular characters into the 968 * EditText. 969 */ onSuggestionsKey(View v, int keyCode, KeyEvent event)970 private boolean onSuggestionsKey(View v, int keyCode, KeyEvent event) { 971 // guard against possible race conditions (late arrival after dismiss) 972 if (mSearchable == null) { 973 return false; 974 } 975 if (mSuggestionsAdapter == null) { 976 return false; 977 } 978 if (event.getAction() == KeyEvent.ACTION_DOWN && KeyEventCompat.hasNoModifiers(event)) { 979 // First, check for enter or search (both of which we'll treat as a 980 // "click") 981 if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_SEARCH 982 || keyCode == KeyEvent.KEYCODE_TAB) { 983 int position = mQueryTextView.getListSelection(); 984 return onItemClicked(position, KeyEvent.KEYCODE_UNKNOWN, null); 985 } 986 987 // Next, check for left/right moves, which we use to "return" the 988 // user to the edit view 989 if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { 990 // give "focus" to text editor, with cursor at the beginning if 991 // left key, at end if right key 992 // TODO: Reverse left/right for right-to-left languages, e.g. 993 // Arabic 994 int selPoint = (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) ? 0 : mQueryTextView 995 .length(); 996 mQueryTextView.setSelection(selPoint); 997 mQueryTextView.setListSelection(0); 998 mQueryTextView.clearListSelection(); 999 HIDDEN_METHOD_INVOKER.ensureImeVisible(mQueryTextView, true); 1000 1001 return true; 1002 } 1003 1004 // Next, check for an "up and out" move 1005 if (keyCode == KeyEvent.KEYCODE_DPAD_UP && 0 == mQueryTextView.getListSelection()) { 1006 // TODO: restoreUserQuery(); 1007 // let ACTV complete the move 1008 return false; 1009 } 1010 } 1011 return false; 1012 } 1013 getDecoratedHint(CharSequence hintText)1014 private CharSequence getDecoratedHint(CharSequence hintText) { 1015 // If the field is always expanded, then don't add the search icon to the hint 1016 if (!mIconifiedByDefault) { 1017 return hintText; 1018 } 1019 1020 final Drawable searchIcon = mTintManager.getDrawable(mSearchIconResId); 1021 final int textSize = (int) (mQueryTextView.getTextSize() * 1.25); 1022 searchIcon.setBounds(0, 0, textSize, textSize); 1023 1024 final SpannableStringBuilder ssb = new SpannableStringBuilder(" "); // for the icon 1025 ssb.append(hintText); 1026 ssb.setSpan(new ImageSpan(searchIcon), 1, 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 1027 return ssb; 1028 } 1029 updateQueryHint()1030 private void updateQueryHint() { 1031 if (mQueryHint != null) { 1032 mQueryTextView.setHint(getDecoratedHint(mQueryHint)); 1033 } else if (IS_AT_LEAST_FROYO && mSearchable != null) { 1034 CharSequence hint = null; 1035 int hintId = mSearchable.getHintId(); 1036 if (hintId != 0) { 1037 hint = getContext().getString(hintId); 1038 } 1039 if (hint != null) { 1040 mQueryTextView.setHint(getDecoratedHint(hint)); 1041 } 1042 } else { 1043 mQueryTextView.setHint(getDecoratedHint("")); 1044 } 1045 } 1046 1047 /** 1048 * Updates the auto-complete text view. 1049 */ 1050 @TargetApi(Build.VERSION_CODES.FROYO) updateSearchAutoComplete()1051 private void updateSearchAutoComplete() { 1052 mQueryTextView.setThreshold(mSearchable.getSuggestThreshold()); 1053 mQueryTextView.setImeOptions(mSearchable.getImeOptions()); 1054 int inputType = mSearchable.getInputType(); 1055 // We only touch this if the input type is set up for text (which it almost certainly 1056 // should be, in the case of search!) 1057 if ((inputType & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_TEXT) { 1058 // The existence of a suggestions authority is the proxy for "suggestions 1059 // are available here" 1060 inputType &= ~InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE; 1061 if (mSearchable.getSuggestAuthority() != null) { 1062 inputType |= InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE; 1063 // TYPE_TEXT_FLAG_AUTO_COMPLETE means that the text editor is performing 1064 // auto-completion based on its own semantics, which it will present to the user 1065 // as they type. This generally means that the input method should not show its 1066 // own candidates, and the spell checker should not be in action. The text editor 1067 // supplies its candidates by calling InputMethodManager.displayCompletions(), 1068 // which in turn will call InputMethodSession.displayCompletions(). 1069 inputType |= InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS; 1070 } 1071 } 1072 mQueryTextView.setInputType(inputType); 1073 if (mSuggestionsAdapter != null) { 1074 mSuggestionsAdapter.changeCursor(null); 1075 } 1076 // attach the suggestions adapter, if suggestions are available 1077 // The existence of a suggestions authority is the proxy for "suggestions available here" 1078 if (mSearchable.getSuggestAuthority() != null) { 1079 mSuggestionsAdapter = new SuggestionsAdapter(getContext(), 1080 this, mSearchable, mOutsideDrawablesCache); 1081 mQueryTextView.setAdapter(mSuggestionsAdapter); 1082 ((SuggestionsAdapter) mSuggestionsAdapter).setQueryRefinement( 1083 mQueryRefinement ? SuggestionsAdapter.REFINE_ALL 1084 : SuggestionsAdapter.REFINE_BY_ENTRY); 1085 } 1086 } 1087 1088 /** 1089 * Update the visibility of the voice button. There are actually two voice search modes, 1090 * either of which will activate the button. 1091 * @param empty whether the search query text field is empty. If it is, then the other 1092 * criteria apply to make the voice button visible. 1093 */ updateVoiceButton(boolean empty)1094 private void updateVoiceButton(boolean empty) { 1095 int visibility = GONE; 1096 if (mVoiceButtonEnabled && !isIconified() && empty) { 1097 visibility = VISIBLE; 1098 mSubmitButton.setVisibility(GONE); 1099 } 1100 mVoiceButton.setVisibility(visibility); 1101 } 1102 1103 private final OnEditorActionListener mOnEditorActionListener = new OnEditorActionListener() { 1104 1105 /** 1106 * Called when the input method default action key is pressed. 1107 */ 1108 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 1109 onSubmitQuery(); 1110 return true; 1111 } 1112 }; 1113 onTextChanged(CharSequence newText)1114 private void onTextChanged(CharSequence newText) { 1115 CharSequence text = mQueryTextView.getText(); 1116 mUserQuery = text; 1117 boolean hasText = !TextUtils.isEmpty(text); 1118 updateSubmitButton(hasText); 1119 updateVoiceButton(!hasText); 1120 updateCloseButton(); 1121 updateSubmitArea(); 1122 if (mOnQueryChangeListener != null && !TextUtils.equals(newText, mOldQueryText)) { 1123 mOnQueryChangeListener.onQueryTextChange(newText.toString()); 1124 } 1125 mOldQueryText = newText.toString(); 1126 } 1127 onSubmitQuery()1128 private void onSubmitQuery() { 1129 CharSequence query = mQueryTextView.getText(); 1130 if (query != null && TextUtils.getTrimmedLength(query) > 0) { 1131 if (mOnQueryChangeListener == null 1132 || !mOnQueryChangeListener.onQueryTextSubmit(query.toString())) { 1133 if (mSearchable != null) { 1134 launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null, query.toString()); 1135 } 1136 setImeVisibility(false); 1137 dismissSuggestions(); 1138 } 1139 } 1140 } 1141 dismissSuggestions()1142 private void dismissSuggestions() { 1143 mQueryTextView.dismissDropDown(); 1144 } 1145 onCloseClicked()1146 private void onCloseClicked() { 1147 CharSequence text = mQueryTextView.getText(); 1148 if (TextUtils.isEmpty(text)) { 1149 if (mIconifiedByDefault) { 1150 // If the app doesn't override the close behavior 1151 if (mOnCloseListener == null || !mOnCloseListener.onClose()) { 1152 // hide the keyboard and remove focus 1153 clearFocus(); 1154 // collapse the search field 1155 updateViewsVisibility(true); 1156 } 1157 } 1158 } else { 1159 mQueryTextView.setText(""); 1160 mQueryTextView.requestFocus(); 1161 setImeVisibility(true); 1162 } 1163 1164 } 1165 onSearchClicked()1166 private void onSearchClicked() { 1167 updateViewsVisibility(false); 1168 mQueryTextView.requestFocus(); 1169 setImeVisibility(true); 1170 if (mOnSearchClickListener != null) { 1171 mOnSearchClickListener.onClick(this); 1172 } 1173 } 1174 1175 @TargetApi(Build.VERSION_CODES.FROYO) onVoiceClicked()1176 private void onVoiceClicked() { 1177 // guard against possible race conditions 1178 if (mSearchable == null) { 1179 return; 1180 } 1181 SearchableInfo searchable = mSearchable; 1182 try { 1183 if (searchable.getVoiceSearchLaunchWebSearch()) { 1184 Intent webSearchIntent = createVoiceWebSearchIntent(mVoiceWebSearchIntent, 1185 searchable); 1186 getContext().startActivity(webSearchIntent); 1187 } else if (searchable.getVoiceSearchLaunchRecognizer()) { 1188 Intent appSearchIntent = createVoiceAppSearchIntent(mVoiceAppSearchIntent, 1189 searchable); 1190 getContext().startActivity(appSearchIntent); 1191 } 1192 } catch (ActivityNotFoundException e) { 1193 // Should not happen, since we check the availability of 1194 // voice search before showing the button. But just in case... 1195 Log.w(LOG_TAG, "Could not find voice search activity"); 1196 } 1197 } 1198 onTextFocusChanged()1199 void onTextFocusChanged() { 1200 updateViewsVisibility(isIconified()); 1201 // Delayed update to make sure that the focus has settled down and window focus changes 1202 // don't affect it. A synchronous update was not working. 1203 postUpdateFocusedState(); 1204 if (mQueryTextView.hasFocus()) { 1205 forceSuggestionQuery(); 1206 } 1207 } 1208 1209 @Override onWindowFocusChanged(boolean hasWindowFocus)1210 public void onWindowFocusChanged(boolean hasWindowFocus) { 1211 super.onWindowFocusChanged(hasWindowFocus); 1212 1213 postUpdateFocusedState(); 1214 } 1215 1216 /** 1217 * {@inheritDoc} 1218 */ 1219 @Override onActionViewCollapsed()1220 public void onActionViewCollapsed() { 1221 setQuery("", false); 1222 clearFocus(); 1223 updateViewsVisibility(true); 1224 mQueryTextView.setImeOptions(mCollapsedImeOptions); 1225 mExpandedInActionView = false; 1226 } 1227 1228 /** 1229 * {@inheritDoc} 1230 */ 1231 @Override onActionViewExpanded()1232 public void onActionViewExpanded() { 1233 if (mExpandedInActionView) return; 1234 1235 mExpandedInActionView = true; 1236 mCollapsedImeOptions = mQueryTextView.getImeOptions(); 1237 mQueryTextView.setImeOptions(mCollapsedImeOptions | EditorInfo.IME_FLAG_NO_FULLSCREEN); 1238 mQueryTextView.setText(""); 1239 setIconified(false); 1240 } 1241 1242 adjustDropDownSizeAndPosition()1243 private void adjustDropDownSizeAndPosition() { 1244 if (mDropDownAnchor.getWidth() > 1) { 1245 Resources res = getContext().getResources(); 1246 int anchorPadding = mSearchPlate.getPaddingLeft(); 1247 Rect dropDownPadding = new Rect(); 1248 final boolean isLayoutRtl = ViewUtils.isLayoutRtl(this); 1249 int iconOffset = mIconifiedByDefault 1250 ? res.getDimensionPixelSize(R.dimen.abc_dropdownitem_icon_width) 1251 + res.getDimensionPixelSize(R.dimen.abc_dropdownitem_text_padding_left) 1252 : 0; 1253 mQueryTextView.getDropDownBackground().getPadding(dropDownPadding); 1254 int offset; 1255 if (isLayoutRtl) { 1256 offset = - dropDownPadding.left; 1257 } else { 1258 offset = anchorPadding - (dropDownPadding.left + iconOffset); 1259 } 1260 mQueryTextView.setDropDownHorizontalOffset(offset); 1261 final int width = mDropDownAnchor.getWidth() + dropDownPadding.left 1262 + dropDownPadding.right + iconOffset - anchorPadding; 1263 mQueryTextView.setDropDownWidth(width); 1264 } 1265 } 1266 onItemClicked(int position, int actionKey, String actionMsg)1267 private boolean onItemClicked(int position, int actionKey, String actionMsg) { 1268 if (mOnSuggestionListener == null 1269 || !mOnSuggestionListener.onSuggestionClick(position)) { 1270 launchSuggestion(position, KeyEvent.KEYCODE_UNKNOWN, null); 1271 setImeVisibility(false); 1272 dismissSuggestions(); 1273 return true; 1274 } 1275 return false; 1276 } 1277 onItemSelected(int position)1278 private boolean onItemSelected(int position) { 1279 if (mOnSuggestionListener == null 1280 || !mOnSuggestionListener.onSuggestionSelect(position)) { 1281 rewriteQueryFromSuggestion(position); 1282 return true; 1283 } 1284 return false; 1285 } 1286 1287 private final OnItemClickListener mOnItemClickListener = new OnItemClickListener() { 1288 1289 /** 1290 * Implements OnItemClickListener 1291 */ 1292 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 1293 if (DBG) Log.d(LOG_TAG, "onItemClick() position " + position); 1294 onItemClicked(position, KeyEvent.KEYCODE_UNKNOWN, null); 1295 } 1296 }; 1297 1298 private final OnItemSelectedListener mOnItemSelectedListener = new OnItemSelectedListener() { 1299 1300 /** 1301 * Implements OnItemSelectedListener 1302 */ 1303 public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { 1304 if (DBG) Log.d(LOG_TAG, "onItemSelected() position " + position); 1305 SearchView.this.onItemSelected(position); 1306 } 1307 1308 /** 1309 * Implements OnItemSelectedListener 1310 */ 1311 public void onNothingSelected(AdapterView<?> parent) { 1312 if (DBG) 1313 Log.d(LOG_TAG, "onNothingSelected()"); 1314 } 1315 }; 1316 1317 /** 1318 * Query rewriting. 1319 */ rewriteQueryFromSuggestion(int position)1320 private void rewriteQueryFromSuggestion(int position) { 1321 CharSequence oldQuery = mQueryTextView.getText(); 1322 Cursor c = mSuggestionsAdapter.getCursor(); 1323 if (c == null) { 1324 return; 1325 } 1326 if (c.moveToPosition(position)) { 1327 // Get the new query from the suggestion. 1328 CharSequence newQuery = mSuggestionsAdapter.convertToString(c); 1329 if (newQuery != null) { 1330 // The suggestion rewrites the query. 1331 // Update the text field, without getting new suggestions. 1332 setQuery(newQuery); 1333 } else { 1334 // The suggestion does not rewrite the query, restore the user's query. 1335 setQuery(oldQuery); 1336 } 1337 } else { 1338 // We got a bad position, restore the user's query. 1339 setQuery(oldQuery); 1340 } 1341 } 1342 1343 /** 1344 * Launches an intent based on a suggestion. 1345 * 1346 * @param position The index of the suggestion to create the intent from. 1347 * @param actionKey The key code of the action key that was pressed, 1348 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none. 1349 * @param actionMsg The message for the action key that was pressed, 1350 * or <code>null</code> if none. 1351 * @return true if a successful launch, false if could not (e.g. bad position). 1352 */ launchSuggestion(int position, int actionKey, String actionMsg)1353 private boolean launchSuggestion(int position, int actionKey, String actionMsg) { 1354 Cursor c = mSuggestionsAdapter.getCursor(); 1355 if ((c != null) && c.moveToPosition(position)) { 1356 1357 Intent intent = createIntentFromSuggestion(c, actionKey, actionMsg); 1358 1359 // launch the intent 1360 launchIntent(intent); 1361 1362 return true; 1363 } 1364 return false; 1365 } 1366 1367 /** 1368 * Launches an intent, including any special intent handling. 1369 */ launchIntent(Intent intent)1370 private void launchIntent(Intent intent) { 1371 if (intent == null) { 1372 return; 1373 } 1374 try { 1375 // If the intent was created from a suggestion, it will always have an explicit 1376 // component here. 1377 getContext().startActivity(intent); 1378 } catch (RuntimeException ex) { 1379 Log.e(LOG_TAG, "Failed launch activity: " + intent, ex); 1380 } 1381 } 1382 1383 /** 1384 * Sets the text in the query box, without updating the suggestions. 1385 */ setQuery(CharSequence query)1386 private void setQuery(CharSequence query) { 1387 mQueryTextView.setText(query); 1388 // Move the cursor to the end 1389 mQueryTextView.setSelection(TextUtils.isEmpty(query) ? 0 : query.length()); 1390 } 1391 launchQuerySearch(int actionKey, String actionMsg, String query)1392 private void launchQuerySearch(int actionKey, String actionMsg, String query) { 1393 String action = Intent.ACTION_SEARCH; 1394 Intent intent = createIntent(action, null, null, query, actionKey, actionMsg); 1395 getContext().startActivity(intent); 1396 } 1397 1398 /** 1399 * Constructs an intent from the given information and the search dialog state. 1400 * 1401 * @param action Intent action. 1402 * @param data Intent data, or <code>null</code>. 1403 * @param extraData Data for {@link SearchManager#EXTRA_DATA_KEY} or <code>null</code>. 1404 * @param query Intent query, or <code>null</code>. 1405 * @param actionKey The key code of the action key that was pressed, 1406 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none. 1407 * @param actionMsg The message for the action key that was pressed, 1408 * or <code>null</code> if none. 1409 * @return The intent. 1410 */ createIntent(String action, Uri data, String extraData, String query, int actionKey, String actionMsg)1411 private Intent createIntent(String action, Uri data, String extraData, String query, 1412 int actionKey, String actionMsg) { 1413 // Now build the Intent 1414 Intent intent = new Intent(action); 1415 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 1416 // We need CLEAR_TOP to avoid reusing an old task that has other activities 1417 // on top of the one we want. We don't want to do this in in-app search though, 1418 // as it can be destructive to the activity stack. 1419 if (data != null) { 1420 intent.setData(data); 1421 } 1422 intent.putExtra(SearchManager.USER_QUERY, mUserQuery); 1423 if (query != null) { 1424 intent.putExtra(SearchManager.QUERY, query); 1425 } 1426 if (extraData != null) { 1427 intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData); 1428 } 1429 if (mAppSearchData != null) { 1430 intent.putExtra(SearchManager.APP_DATA, mAppSearchData); 1431 } 1432 if (actionKey != KeyEvent.KEYCODE_UNKNOWN) { 1433 intent.putExtra(SearchManager.ACTION_KEY, actionKey); 1434 intent.putExtra(SearchManager.ACTION_MSG, actionMsg); 1435 } 1436 if (IS_AT_LEAST_FROYO) { 1437 intent.setComponent(mSearchable.getSearchActivity()); 1438 } 1439 return intent; 1440 } 1441 1442 /** 1443 * Create and return an Intent that can launch the voice search activity for web search. 1444 */ 1445 @TargetApi(Build.VERSION_CODES.FROYO) createVoiceWebSearchIntent(Intent baseIntent, SearchableInfo searchable)1446 private Intent createVoiceWebSearchIntent(Intent baseIntent, SearchableInfo searchable) { 1447 Intent voiceIntent = new Intent(baseIntent); 1448 ComponentName searchActivity = searchable.getSearchActivity(); 1449 voiceIntent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, searchActivity == null ? null 1450 : searchActivity.flattenToShortString()); 1451 return voiceIntent; 1452 } 1453 1454 /** 1455 * Create and return an Intent that can launch the voice search activity, perform a specific 1456 * voice transcription, and forward the results to the searchable activity. 1457 * 1458 * @param baseIntent The voice app search intent to start from 1459 * @return A completely-configured intent ready to send to the voice search activity 1460 */ 1461 @TargetApi(Build.VERSION_CODES.FROYO) createVoiceAppSearchIntent(Intent baseIntent, SearchableInfo searchable)1462 private Intent createVoiceAppSearchIntent(Intent baseIntent, SearchableInfo searchable) { 1463 ComponentName searchActivity = searchable.getSearchActivity(); 1464 1465 // create the necessary intent to set up a search-and-forward operation 1466 // in the voice search system. We have to keep the bundle separate, 1467 // because it becomes immutable once it enters the PendingIntent 1468 Intent queryIntent = new Intent(Intent.ACTION_SEARCH); 1469 queryIntent.setComponent(searchActivity); 1470 PendingIntent pending = PendingIntent.getActivity(getContext(), 0, queryIntent, 1471 PendingIntent.FLAG_ONE_SHOT); 1472 1473 // Now set up the bundle that will be inserted into the pending intent 1474 // when it's time to do the search. We always build it here (even if empty) 1475 // because the voice search activity will always need to insert "QUERY" into 1476 // it anyway. 1477 Bundle queryExtras = new Bundle(); 1478 if (mAppSearchData != null) { 1479 queryExtras.putParcelable(SearchManager.APP_DATA, mAppSearchData); 1480 } 1481 1482 // Now build the intent to launch the voice search. Add all necessary 1483 // extras to launch the voice recognizer, and then all the necessary extras 1484 // to forward the results to the searchable activity 1485 Intent voiceIntent = new Intent(baseIntent); 1486 1487 // Add all of the configuration options supplied by the searchable's metadata 1488 String languageModel = RecognizerIntent.LANGUAGE_MODEL_FREE_FORM; 1489 String prompt = null; 1490 String language = null; 1491 int maxResults = 1; 1492 1493 if (Build.VERSION.SDK_INT >= 8) { 1494 Resources resources = getResources(); 1495 if (searchable.getVoiceLanguageModeId() != 0) { 1496 languageModel = resources.getString(searchable.getVoiceLanguageModeId()); 1497 } 1498 if (searchable.getVoicePromptTextId() != 0) { 1499 prompt = resources.getString(searchable.getVoicePromptTextId()); 1500 } 1501 if (searchable.getVoiceLanguageId() != 0) { 1502 language = resources.getString(searchable.getVoiceLanguageId()); 1503 } 1504 if (searchable.getVoiceMaxResults() != 0) { 1505 maxResults = searchable.getVoiceMaxResults(); 1506 } 1507 } 1508 voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, languageModel); 1509 voiceIntent.putExtra(RecognizerIntent.EXTRA_PROMPT, prompt); 1510 voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, language); 1511 voiceIntent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, maxResults); 1512 voiceIntent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, searchActivity == null ? null 1513 : searchActivity.flattenToShortString()); 1514 1515 // Add the values that configure forwarding the results 1516 voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT, pending); 1517 voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT_BUNDLE, queryExtras); 1518 1519 return voiceIntent; 1520 } 1521 1522 /** 1523 * When a particular suggestion has been selected, perform the various lookups required 1524 * to use the suggestion. This includes checking the cursor for suggestion-specific data, 1525 * and/or falling back to the XML for defaults; It also creates REST style Uri data when 1526 * the suggestion includes a data id. 1527 * 1528 * @param c The suggestions cursor, moved to the row of the user's selection 1529 * @param actionKey The key code of the action key that was pressed, 1530 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none. 1531 * @param actionMsg The message for the action key that was pressed, 1532 * or <code>null</code> if none. 1533 * @return An intent for the suggestion at the cursor's position. 1534 */ createIntentFromSuggestion(Cursor c, int actionKey, String actionMsg)1535 private Intent createIntentFromSuggestion(Cursor c, int actionKey, String actionMsg) { 1536 try { 1537 // use specific action if supplied, or default action if supplied, or fixed default 1538 String action = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_ACTION); 1539 1540 if (action == null && Build.VERSION.SDK_INT >= 8) { 1541 action = mSearchable.getSuggestIntentAction(); 1542 } 1543 if (action == null) { 1544 action = Intent.ACTION_SEARCH; 1545 } 1546 1547 // use specific data if supplied, or default data if supplied 1548 String data = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA); 1549 if (IS_AT_LEAST_FROYO && data == null) { 1550 data = mSearchable.getSuggestIntentData(); 1551 } 1552 // then, if an ID was provided, append it. 1553 if (data != null) { 1554 String id = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID); 1555 if (id != null) { 1556 data = data + "/" + Uri.encode(id); 1557 } 1558 } 1559 Uri dataUri = (data == null) ? null : Uri.parse(data); 1560 1561 String query = getColumnString(c, SearchManager.SUGGEST_COLUMN_QUERY); 1562 String extraData = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA); 1563 1564 return createIntent(action, dataUri, extraData, query, actionKey, actionMsg); 1565 } catch (RuntimeException e ) { 1566 int rowNum; 1567 try { // be really paranoid now 1568 rowNum = c.getPosition(); 1569 } catch (RuntimeException e2 ) { 1570 rowNum = -1; 1571 } 1572 Log.w(LOG_TAG, "Search suggestions cursor at row " + rowNum + 1573 " returned exception.", e); 1574 return null; 1575 } 1576 } 1577 forceSuggestionQuery()1578 private void forceSuggestionQuery() { 1579 HIDDEN_METHOD_INVOKER.doBeforeTextChanged(mQueryTextView); 1580 HIDDEN_METHOD_INVOKER.doAfterTextChanged(mQueryTextView); 1581 } 1582 isLandscapeMode(Context context)1583 static boolean isLandscapeMode(Context context) { 1584 return context.getResources().getConfiguration().orientation 1585 == Configuration.ORIENTATION_LANDSCAPE; 1586 } 1587 1588 /** 1589 * Callback to watch the text field for empty/non-empty 1590 */ 1591 private TextWatcher mTextWatcher = new TextWatcher() { 1592 1593 public void beforeTextChanged(CharSequence s, int start, int before, int after) { } 1594 1595 public void onTextChanged(CharSequence s, int start, 1596 int before, int after) { 1597 SearchView.this.onTextChanged(s); 1598 } 1599 1600 public void afterTextChanged(Editable s) { 1601 } 1602 }; 1603 1604 /** 1605 * Local subclass for AutoCompleteTextView. 1606 * @hide 1607 */ 1608 public static class SearchAutoComplete extends AutoCompleteTextView { 1609 1610 private final int[] POPUP_WINDOW_ATTRS = { 1611 android.R.attr.popupBackground 1612 }; 1613 1614 private int mThreshold; 1615 private SearchView mSearchView; 1616 1617 private final TintManager mTintManager; 1618 SearchAutoComplete(Context context)1619 public SearchAutoComplete(Context context) { 1620 this(context, null); 1621 } 1622 SearchAutoComplete(Context context, AttributeSet attrs)1623 public SearchAutoComplete(Context context, AttributeSet attrs) { 1624 this(context, attrs, android.R.attr.autoCompleteTextViewStyle); 1625 } 1626 SearchAutoComplete(Context context, AttributeSet attrs, int defStyle)1627 public SearchAutoComplete(Context context, AttributeSet attrs, int defStyle) { 1628 super(context, attrs, defStyle); 1629 mThreshold = getThreshold(); 1630 1631 TintTypedArray a = TintTypedArray.obtainStyledAttributes(context, attrs, 1632 POPUP_WINDOW_ATTRS, defStyle, 0); 1633 if (a.hasValue(0)) { 1634 setDropDownBackgroundDrawable(a.getDrawable(0)); 1635 } 1636 a.recycle(); 1637 1638 // Keep the TintManager in case we need it later 1639 mTintManager = a.getTintManager(); 1640 } 1641 setSearchView(SearchView searchView)1642 void setSearchView(SearchView searchView) { 1643 mSearchView = searchView; 1644 } 1645 1646 @Override setThreshold(int threshold)1647 public void setThreshold(int threshold) { 1648 super.setThreshold(threshold); 1649 mThreshold = threshold; 1650 } 1651 1652 @Override setDropDownBackgroundResource(int id)1653 public void setDropDownBackgroundResource(int id) { 1654 setDropDownBackgroundDrawable(mTintManager.getDrawable(id)); 1655 } 1656 1657 /** 1658 * Returns true if the text field is empty, or contains only whitespace. 1659 */ isEmpty()1660 private boolean isEmpty() { 1661 return TextUtils.getTrimmedLength(getText()) == 0; 1662 } 1663 1664 /** 1665 * We override this method to avoid replacing the query box text when a 1666 * suggestion is clicked. 1667 */ 1668 @Override replaceText(CharSequence text)1669 protected void replaceText(CharSequence text) { 1670 } 1671 1672 /** 1673 * We override this method to avoid an extra onItemClick being called on 1674 * the drop-down's OnItemClickListener by 1675 * {@link AutoCompleteTextView#onKeyUp(int, KeyEvent)} when an item is 1676 * clicked with the trackball. 1677 */ 1678 @Override performCompletion()1679 public void performCompletion() { 1680 } 1681 1682 /** 1683 * We override this method to be sure and show the soft keyboard if 1684 * appropriate when the TextView has focus. 1685 */ 1686 @Override onWindowFocusChanged(boolean hasWindowFocus)1687 public void onWindowFocusChanged(boolean hasWindowFocus) { 1688 super.onWindowFocusChanged(hasWindowFocus); 1689 1690 if (hasWindowFocus && mSearchView.hasFocus() && getVisibility() == VISIBLE) { 1691 InputMethodManager inputManager = (InputMethodManager) getContext() 1692 .getSystemService(Context.INPUT_METHOD_SERVICE); 1693 inputManager.showSoftInput(this, 0); 1694 // If in landscape mode, then make sure that 1695 // the ime is in front of the dropdown. 1696 if (isLandscapeMode(getContext())) { 1697 HIDDEN_METHOD_INVOKER.ensureImeVisible(this, true); 1698 } 1699 } 1700 } 1701 1702 @Override onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect)1703 protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { 1704 super.onFocusChanged(focused, direction, previouslyFocusedRect); 1705 mSearchView.onTextFocusChanged(); 1706 } 1707 1708 /** 1709 * We override this method so that we can allow a threshold of zero, 1710 * which ACTV does not. 1711 */ 1712 @Override enoughToFilter()1713 public boolean enoughToFilter() { 1714 return mThreshold <= 0 || super.enoughToFilter(); 1715 } 1716 1717 @Override onKeyPreIme(int keyCode, KeyEvent event)1718 public boolean onKeyPreIme(int keyCode, KeyEvent event) { 1719 if (keyCode == KeyEvent.KEYCODE_BACK) { 1720 // special case for the back key, we do not even try to send it 1721 // to the drop down list but instead, consume it immediately 1722 if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) { 1723 KeyEvent.DispatcherState state = getKeyDispatcherState(); 1724 if (state != null) { 1725 state.startTracking(event, this); 1726 } 1727 return true; 1728 } else if (event.getAction() == KeyEvent.ACTION_UP) { 1729 KeyEvent.DispatcherState state = getKeyDispatcherState(); 1730 if (state != null) { 1731 state.handleUpEvent(event); 1732 } 1733 if (event.isTracking() && !event.isCanceled()) { 1734 mSearchView.clearFocus(); 1735 mSearchView.setImeVisibility(false); 1736 return true; 1737 } 1738 } 1739 } 1740 return super.onKeyPreIme(keyCode, event); 1741 } 1742 } 1743 1744 private static class AutoCompleteTextViewReflector { 1745 private Method doBeforeTextChanged, doAfterTextChanged; 1746 private Method ensureImeVisible; 1747 private Method showSoftInputUnchecked; 1748 AutoCompleteTextViewReflector()1749 AutoCompleteTextViewReflector() { 1750 try { 1751 doBeforeTextChanged = AutoCompleteTextView.class 1752 .getDeclaredMethod("doBeforeTextChanged"); 1753 doBeforeTextChanged.setAccessible(true); 1754 } catch (NoSuchMethodException e) { 1755 // Ah well. 1756 } 1757 try { 1758 doAfterTextChanged = AutoCompleteTextView.class 1759 .getDeclaredMethod("doAfterTextChanged"); 1760 doAfterTextChanged.setAccessible(true); 1761 } catch (NoSuchMethodException e) { 1762 // Ah well. 1763 } 1764 try { 1765 ensureImeVisible = AutoCompleteTextView.class 1766 .getMethod("ensureImeVisible", boolean.class); 1767 ensureImeVisible.setAccessible(true); 1768 } catch (NoSuchMethodException e) { 1769 // Ah well. 1770 } 1771 try { 1772 showSoftInputUnchecked = InputMethodManager.class.getMethod( 1773 "showSoftInputUnchecked", int.class, ResultReceiver.class); 1774 showSoftInputUnchecked.setAccessible(true); 1775 } catch (NoSuchMethodException e) { 1776 // Ah well. 1777 } 1778 } 1779 doBeforeTextChanged(AutoCompleteTextView view)1780 void doBeforeTextChanged(AutoCompleteTextView view) { 1781 if (doBeforeTextChanged != null) { 1782 try { 1783 doBeforeTextChanged.invoke(view); 1784 } catch (Exception e) { 1785 } 1786 } 1787 } 1788 doAfterTextChanged(AutoCompleteTextView view)1789 void doAfterTextChanged(AutoCompleteTextView view) { 1790 if (doAfterTextChanged != null) { 1791 try { 1792 doAfterTextChanged.invoke(view); 1793 } catch (Exception e) { 1794 } 1795 } 1796 } 1797 ensureImeVisible(AutoCompleteTextView view, boolean visible)1798 void ensureImeVisible(AutoCompleteTextView view, boolean visible) { 1799 if (ensureImeVisible != null) { 1800 try { 1801 ensureImeVisible.invoke(view, visible); 1802 } catch (Exception e) { 1803 } 1804 } 1805 } 1806 showSoftInputUnchecked(InputMethodManager imm, View view, int flags)1807 void showSoftInputUnchecked(InputMethodManager imm, View view, int flags) { 1808 if (showSoftInputUnchecked != null) { 1809 try { 1810 showSoftInputUnchecked.invoke(imm, flags, null); 1811 return; 1812 } catch (Exception e) { 1813 } 1814 } 1815 1816 // Hidden method failed, call public version instead 1817 imm.showSoftInput(view, flags); 1818 } 1819 } 1820 }