Java tutorial
/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.delexus.imitationzhihu; import android.app.PendingIntent; import android.app.SearchManager; import android.app.SearchableInfo; import android.content.ActivityNotFoundException; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.Configuration; import android.content.res.Resources; import android.database.Cursor; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import android.os.ResultReceiver; import android.speech.RecognizerIntent; import android.support.annotation.Nullable; import android.support.annotation.RestrictTo; import android.support.v4.content.res.ConfigurationHelper; import android.support.v4.os.ParcelableCompat; import android.support.v4.os.ParcelableCompatCreatorCallbacks; import android.support.v4.view.AbsSavedState; import android.support.v4.view.KeyEventCompat; import android.support.v4.widget.CursorAdapter; import android.support.v7.view.CollapsibleActionView; import android.support.v7.widget.AppCompatAutoCompleteTextView; import android.support.v7.widget.LinearLayoutCompat; import android.support.v7.widget.TintTypedArray; import android.text.Editable; import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.TextUtils; import android.text.TextWatcher; import android.text.style.ImageSpan; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.util.Log; import android.util.TypedValue; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.TouchDelegate; import android.view.View; import android.view.ViewConfiguration; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.AutoCompleteTextView; import android.widget.ImageView; import android.widget.TextView; import android.widget.TextView.OnEditorActionListener; import java.lang.reflect.Method; import java.util.WeakHashMap; import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP; /** * A widget that provides a user interface for the user to enter a search query and submit a request * to a search provider. Shows a list of query suggestions or results, if available, and allows the * user to pick a suggestion or result to launch into. * * <p class="note"><strong>Note:</strong> This class is included in the <a * href="{@docRoot}tools/extras/support-library.html">support library</a> for compatibility * with API level 7 and higher. If you're developing your app for API level 11 and higher * <em>only</em>, you should instead use the framework {@link android.widget.SearchView} class.</p> * * <p> * When the SearchView is used in an {@link android.support.v7.app.ActionBar} * as an action view, it's collapsed by default, so you must provide an icon for the action. * </p> * <p> * If you want the search field to always be visible, then call * {@link #setIconifiedByDefault(boolean) setIconifiedByDefault(false)}. * </p> * * <div class="special reference"> * <h3>Developer Guides</h3> * <p>For information about using {@code SearchView}, read the * <a href="{@docRoot}guide/topics/search/index.html">Search</a> API guide. * Additional information about action views is also available in the <<a * href="{@docRoot}guide/topics/ui/actionbar.html#ActionView">Action Bar</a> API guide</p> * </div> * * @see android.support.v4.view.MenuItemCompat#SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW */ public class MySearchView extends LinearLayoutCompat implements CollapsibleActionView { static final boolean DBG = false; static final String LOG_TAG = "SearchView"; /** * Private constant for removing the microphone in the keyboard. */ private static final String IME_OPTION_NO_MICROPHONE = "nm"; private final SearchAutoComplete mSearchSrcTextView; private final ImageView mNavigationButton; final ImageView mGoButton; final ImageView mCloseButton; final ImageView mVoiceButton; private UpdatableTouchDelegate mTouchDelegate; private Rect mSearchSrcTextViewBounds = new Rect(); private Rect mSearchSrtTextViewBoundsExpanded = new Rect(); private int[] mTemp = new int[2]; private int[] mTemp2 = new int[2]; /** Drawable used as an EditText hint. */ private final Drawable mSearchHintIcon; // Intents used for voice searching. private final Intent mVoiceWebSearchIntent; private final Intent mVoiceAppSearchIntent; private final CharSequence mDefaultQueryHint; private OnQueryTextListener mOnQueryChangeListener; private OnCloseListener mOnCloseListener; OnFocusChangeListener mOnQueryTextFocusChangeListener; private OnClickListener mOnSearchClickListener; private OnNavigateBackListener mOnNavigateBackListener; private boolean mIconifiedByDefault; private boolean mIconified; CursorAdapter mSuggestionsAdapter; private boolean mSubmitButtonEnabled; private CharSequence mQueryHint; private boolean mQueryRefinement; private boolean mClearingFocus; private int mMaxWidth; private boolean mVoiceButtonEnabled; private CharSequence mOldQueryText; private CharSequence mUserQuery; private boolean mExpandedInActionView; private int mCollapsedImeOptions; SearchableInfo mSearchable; private Bundle mAppSearchData; static final AutoCompleteTextViewReflector HIDDEN_METHOD_INVOKER = new AutoCompleteTextViewReflector(); /* * SearchView can be set expanded before the IME is ready to be shown during * initial UI setup. The show operation is asynchronous to account for this. */ private Runnable mShowImeRunnable = new Runnable() { @Override public void run() { InputMethodManager imm = (InputMethodManager) getContext() .getSystemService(Context.INPUT_METHOD_SERVICE); if (imm != null) { HIDDEN_METHOD_INVOKER.showSoftInputUnchecked(imm, MySearchView.this, 0); } } }; // A weak map of drawables we've gotten from other packages, so we don't load them // more than once. private final WeakHashMap<String, Drawable.ConstantState> mOutsideDrawablesCache = new WeakHashMap<String, Drawable.ConstantState>(); /** * Callbacks for changes to the query text. */ public interface OnQueryTextListener { /** * Called when the user submits the query. This could be due to a key press on the * keyboard or due to pressing a submit button. * The listener can override the standard behavior by returning true * to indicate that it has handled the submit request. Otherwise return false to * let the SearchView handle the submission by launching any associated intent. * * @param query the query text that is to be submitted * * @return true if the query has been handled by the listener, false to let the * SearchView perform the default action. */ boolean onQueryTextSubmit(String query); /** * Called when the query text is changed by the user. * * @param newText the new content of the query text field. * * @return false if the SearchView should perform the default action of showing any * suggestions if available, true if the action was handled by the listener. */ boolean onQueryTextChange(String newText); } public interface OnCloseListener { /** * The user is attempting to close the SearchView. * * @return true if the listener wants to override the default behavior of clearing the * text field and dismissing it, false otherwise. */ boolean onClose(); } public interface OnNavigateBackListener { boolean onNavigateBack(); } public MySearchView(Context context) { this(context, null); } public MySearchView(Context context, AttributeSet attrs) { this(context, attrs, R.attr.searchViewStyle); } public MySearchView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); final TintTypedArray a = TintTypedArray.obtainStyledAttributes(context, attrs, R.styleable.MySearchView, defStyleAttr, 0); final LayoutInflater inflater = LayoutInflater.from(context); inflater.inflate(R.layout.search_view, this); mSearchSrcTextView = (SearchAutoComplete) findViewById(R.id.my_search_src_text); mSearchSrcTextView.setSearchView(this); mNavigationButton = (ImageView) findViewById(R.id.my_search_button); mGoButton = (ImageView) findViewById(R.id.my_search_go_btn); mCloseButton = (ImageView) findViewById(R.id.my_search_close_btn); mVoiceButton = (ImageView) findViewById(R.id.my_search_voice_btn); // Set up icons and backgrounds. mNavigationButton.setImageDrawable(a.getDrawable(R.styleable.MySearchView_navigateIcon)); mGoButton.setImageDrawable(a.getDrawable(R.styleable.MySearchView_goIcon)); mCloseButton.setImageDrawable(a.getDrawable(R.styleable.MySearchView_closeIcon)); mVoiceButton.setImageDrawable(a.getDrawable(R.styleable.MySearchView_voiceIcon)); mSearchHintIcon = a.getDrawable(R.styleable.MySearchView_searchHintIcon); mNavigationButton.setOnClickListener(mOnClickListener); mCloseButton.setOnClickListener(mOnClickListener); mGoButton.setOnClickListener(mOnClickListener); mVoiceButton.setOnClickListener(mOnClickListener); mSearchSrcTextView.setOnClickListener(mOnClickListener); mSearchSrcTextView.addTextChangedListener(mTextWatcher); mSearchSrcTextView.setOnEditorActionListener(mOnEditorActionListener); mSearchSrcTextView.setOnKeyListener(mTextKeyListener); // Inform any listener of focus changes mSearchSrcTextView.setOnFocusChangeListener(new OnFocusChangeListener() { @Override public void onFocusChange(View v, boolean hasFocus) { if (mOnQueryTextFocusChangeListener != null) { mOnQueryTextFocusChangeListener.onFocusChange(MySearchView.this, hasFocus); } } }); setIconifiedByDefault(a.getBoolean(R.styleable.MySearchView_iconifiedByDefault, true)); final int maxWidth = a.getDimensionPixelSize(R.styleable.MySearchView_android_maxWidth, -1); if (maxWidth != -1) { setMaxWidth(maxWidth); } mDefaultQueryHint = a.getText(R.styleable.MySearchView_defaultQueryHint); mQueryHint = a.getText(R.styleable.MySearchView_queryHint); final int imeOptions = a.getInt(R.styleable.MySearchView_android_imeOptions, -1); if (imeOptions != -1) { setImeOptions(imeOptions); } final int inputType = a.getInt(R.styleable.MySearchView_android_inputType, -1); if (inputType != -1) { setInputType(inputType); } boolean focusable = true; focusable = a.getBoolean(R.styleable.MySearchView_android_inputType, focusable); setFocusable(focusable); a.recycle(); // Save voice intent for later queries/launching mVoiceWebSearchIntent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH); mVoiceWebSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); mVoiceWebSearchIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH); mVoiceAppSearchIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); mVoiceAppSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); updateViewsVisibility(mIconifiedByDefault); updateQueryHint(); } /** * Sets the APP_DATA for legacy SearchDialog use. * @param appSearchData bundle provided by the app when launching the search dialog * @hide */ @RestrictTo(LIBRARY_GROUP) public void setAppSearchData(Bundle appSearchData) { mAppSearchData = appSearchData; } /** * Sets the IME options on the query text field. * * @see TextView#setImeOptions(int) * @param imeOptions the options to set on the query text field * * @attr ref android.support.v7.appcompat.R.styleable#SearchView_android_imeOptions */ public void setImeOptions(int imeOptions) { mSearchSrcTextView.setImeOptions(imeOptions); } /** * Returns the IME options set on the query text field. * @return the ime options * @see TextView#setImeOptions(int) * * @attr ref android.support.v7.appcompat.R.styleable#SearchView_android_imeOptions */ public int getImeOptions() { return mSearchSrcTextView.getImeOptions(); } /** * Sets the input type on the query text field. * * @see TextView#setInputType(int) * @param inputType the input type to set on the query text field * * @attr ref android.support.v7.appcompat.R.styleable#SearchView_android_inputType */ public void setInputType(int inputType) { mSearchSrcTextView.setInputType(inputType); } /** * Returns the input type set on the query text field. * @return the input type * * @attr ref android.support.v7.appcompat.R.styleable#SearchView_android_inputType */ public int getInputType() { return mSearchSrcTextView.getInputType(); } @Override public boolean requestFocus(int direction, Rect previouslyFocusedRect) { // Don't accept focus if in the middle of clearing focus if (mClearingFocus) return false; // Check if SearchView is focusable. if (!isFocusable()) return false; // If it is not iconified, then give the focus to the text field if (!isIconified()) { boolean result = mSearchSrcTextView.requestFocus(direction, previouslyFocusedRect); if (result) { updateViewsVisibility(false); } return result; } else { return super.requestFocus(direction, previouslyFocusedRect); } } @Override public void clearFocus() { mClearingFocus = true; setImeVisibility(false); super.clearFocus(); mSearchSrcTextView.clearFocus(); mClearingFocus = false; } public void setOnNavigateBackLister(OnNavigateBackListener listener) { mOnNavigateBackListener = listener; } /** * Sets a listener for user actions within the SearchView. * * @param listener the listener object that receives callbacks when the user performs * actions in the SearchView such as clicking on buttons or typing a query. */ public void setOnQueryTextListener(OnQueryTextListener listener) { mOnQueryChangeListener = listener; } /** * Sets a listener to inform when the user closes the SearchView. * * @param listener the listener to call when the user closes the SearchView. */ public void setOnCloseListener(OnCloseListener listener) { mOnCloseListener = listener; } /** * Sets a listener to inform when the focus of the query text field changes. * * @param listener the listener to inform of focus changes. */ public void setOnQueryTextFocusChangeListener(OnFocusChangeListener listener) { mOnQueryTextFocusChangeListener = listener; } /** * Sets a listener to inform when the search button is pressed. This is only * relevant when the text field is not visible by default. Calling {@link #setIconified * setIconified(false)} can also cause this listener to be informed. * * @param listener the listener to inform when the search button is clicked or * the text field is programmatically de-iconified. */ public void setOnSearchClickListener(OnClickListener listener) { mOnSearchClickListener = listener; } /** * Returns the query string currently in the text field. * * @return the query string */ public CharSequence getQuery() { return mSearchSrcTextView.getText(); } /** * Sets a query string in the text field and optionally submits the query as well. * * @param query the query string. This replaces any query text already present in the * text field. * @param submit whether to submit the query right now or only update the contents of * text field. */ public void setQuery(CharSequence query, boolean submit) { mSearchSrcTextView.setText(query); if (query != null) { mSearchSrcTextView.setSelection(mSearchSrcTextView.length()); mUserQuery = query; } // If the query is not empty and submit is requested, submit the query if (submit && !TextUtils.isEmpty(query)) { onSubmitQuery(); } } /** * Sets the hint text to display in the query text field. This overrides * any hint specified in the {@link SearchableInfo}. * <p> * This value may be specified as an empty string to prevent any query hint * from being displayed. * * @param hint the hint text to display or {@code null} to clear * @attr ref android.support.v7.appcompat.R.styleable#SearchView_queryHint */ public void setQueryHint(@Nullable CharSequence hint) { mQueryHint = hint; updateQueryHint(); } /** * Returns the hint text that will be displayed in the query text field. * <p> * The displayed query hint is chosen in the following order: * <ol> * <li>Non-null value set with {@link #setQueryHint(CharSequence)} * <li>Value specified in XML using {@code app:queryHint} * <li>Valid string resource ID exposed by the {@link SearchableInfo} via * {@link SearchableInfo#getHintId()} * <li>Default hint provided by the theme against which the view was * inflated * </ol> * * * * @return the displayed query hint text, or {@code null} if none set * @attr ref android.support.v7.appcompat.R.styleable#SearchView_queryHint */ @Nullable public CharSequence getQueryHint() { final CharSequence hint; if (mQueryHint != null) { hint = mQueryHint; } else if (mSearchable != null && mSearchable.getHintId() != 0) { hint = getContext().getText(mSearchable.getHintId()); } else { hint = mDefaultQueryHint; } return hint; } /** * Sets the default or resting state of the search field. If true, a single search icon is * shown by default and expands to show the text field and other buttons when pressed. Also, * if the default state is iconified, then it collapses to that state when the close button * is pressed. Changes to this property will take effect immediately. * * <p>The default value is true.</p> * * @param iconified whether the search field should be iconified by default * * @attr ref android.support.v7.appcompat.R.styleable#SearchView_iconifiedByDefault */ public void setIconifiedByDefault(boolean iconified) { if (mIconifiedByDefault == iconified) return; mIconifiedByDefault = iconified; updateViewsVisibility(iconified); updateQueryHint(); } /** * Returns the default iconified state of the search field. * @return * * @attr ref android.support.v7.appcompat.R.styleable#SearchView_iconifiedByDefault */ public boolean isIconfiedByDefault() { return mIconifiedByDefault; } /** * Iconifies or expands the SearchView. Any query text is cleared when iconified. This is * a temporary state and does not override the default iconified state set by * {@link #setIconifiedByDefault(boolean)}. If the default state is iconified, then * a false here will only be valid until the user closes the field. And if the default * state is expanded, then a true here will only clear the text field and not close it. * * @param iconify a true value will collapse the SearchView to an icon, while a false will * expand it. */ public void setIconified(boolean iconify) { if (iconify) { onCloseClicked(); } else { onSearchClicked(); } } /** * Returns the current iconified state of the SearchView. * * @return true if the SearchView is currently iconified, false if the search field is * fully visible. */ public boolean isIconified() { return mIconified; } /** * Enables showing a submit button when the query is non-empty. In cases where the SearchView * is being used to filter the contents of the current activity and doesn't launch a separate * results activity, then the submit button should be disabled. * * @param enabled true to show a submit button for submitting queries, false if a submit * button is not required. */ public void setSubmitButtonEnabled(boolean enabled) { mSubmitButtonEnabled = enabled; updateViewsVisibility(isIconified()); } /** * Returns whether the submit button is enabled when necessary or never displayed. * * @return whether the submit button is enabled automatically when necessary */ public boolean isSubmitButtonEnabled() { return mSubmitButtonEnabled; } /** * Returns whether query refinement is enabled for all items or only specific ones. * @return true if enabled for all items, false otherwise. */ public boolean isQueryRefinementEnabled() { return mQueryRefinement; } /** * Returns the adapter used for suggestions, if any. * @return the suggestions adapter */ public CursorAdapter getSuggestionsAdapter() { return mSuggestionsAdapter; } /** * Makes the view at most this many pixels wide * * @attr ref android.support.v7.appcompat.R.styleable#SearchView_android_maxWidth */ public void setMaxWidth(int maxpixels) { mMaxWidth = maxpixels; requestLayout(); } /** * Gets the specified maximum width in pixels, if set. Returns zero if * no maximum width was specified. * @return the maximum width of the view * * @attr ref android.support.v7.appcompat.R.styleable#SearchView_android_maxWidth */ public int getMaxWidth() { return mMaxWidth; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // Let the standard measurements take effect in iconified state. if (isIconified()) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); return; } int widthMode = MeasureSpec.getMode(widthMeasureSpec); int width = MeasureSpec.getSize(widthMeasureSpec); switch (widthMode) { case MeasureSpec.AT_MOST: // If there is an upper limit, don't exceed maximum width (explicit or implicit) if (mMaxWidth > 0) { width = Math.min(mMaxWidth, width); } else { width = Math.min(getPreferredWidth(), width); } break; case MeasureSpec.EXACTLY: // If an exact width is specified, still don't exceed any specified maximum width if (mMaxWidth > 0) { width = Math.min(mMaxWidth, width); } break; case MeasureSpec.UNSPECIFIED: // Use maximum width, if specified, else preferred width width = mMaxWidth > 0 ? mMaxWidth : getPreferredWidth(); break; } widthMode = MeasureSpec.EXACTLY; int heightMode = MeasureSpec.getMode(heightMeasureSpec); int height = MeasureSpec.getSize(heightMeasureSpec); switch (heightMode) { case MeasureSpec.AT_MOST: height = Math.min(getPreferredHeight(), height); break; case MeasureSpec.UNSPECIFIED: height = getPreferredHeight(); break; } heightMode = MeasureSpec.EXACTLY; super.onMeasure(MeasureSpec.makeMeasureSpec(width, widthMode), MeasureSpec.makeMeasureSpec(height, heightMode)); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); if (changed) { // Expand mSearchSrcTextView touch target to be the height of the parent in order to // allow it to be up to 48dp. getChildBoundsWithinSearchView(mSearchSrcTextView, mSearchSrcTextViewBounds); mSearchSrtTextViewBoundsExpanded.set(mSearchSrcTextViewBounds.left, 0, mSearchSrcTextViewBounds.right, bottom - top); if (mTouchDelegate == null) { mTouchDelegate = new UpdatableTouchDelegate(mSearchSrtTextViewBoundsExpanded, mSearchSrcTextViewBounds, mSearchSrcTextView); setTouchDelegate(mTouchDelegate); } else { mTouchDelegate.setBounds(mSearchSrtTextViewBoundsExpanded, mSearchSrcTextViewBounds); } } } private void getChildBoundsWithinSearchView(View view, Rect rect) { view.getLocationInWindow(mTemp); getLocationInWindow(mTemp2); final int top = mTemp[1] - mTemp2[1]; final int left = mTemp[0] - mTemp2[0]; rect.set(left, top, left + view.getWidth(), top + view.getHeight()); } private int getPreferredWidth() { return getContext().getResources().getDimensionPixelSize(R.dimen.abc_search_view_preferred_width); } private int getPreferredHeight() { return getContext().getResources().getDimensionPixelSize(R.dimen.abc_search_view_preferred_height); } private void updateViewsVisibility(final boolean collapsed) { mIconified = collapsed; // Visibility of views that are visible when collapsed final int visCollapsed = collapsed ? GONE : VISIBLE; // Is there text in the query final boolean hasText = !TextUtils.isEmpty(mSearchSrcTextView.getText()); mNavigationButton.setVisibility(visCollapsed); updateSubmitButton(hasText); updateCloseButton(); updateVoiceButton(!hasText); } private boolean hasVoiceSearch() { if (mSearchable != null && mSearchable.getVoiceSearchEnabled()) { Intent testIntent = null; if (mSearchable.getVoiceSearchLaunchWebSearch()) { testIntent = mVoiceWebSearchIntent; } else if (mSearchable.getVoiceSearchLaunchRecognizer()) { testIntent = mVoiceAppSearchIntent; } if (testIntent != null) { ResolveInfo ri = getContext().getPackageManager().resolveActivity(testIntent, PackageManager.MATCH_DEFAULT_ONLY); return ri != null; } } return false; } private void updateSubmitButton(boolean hasText) { int visibility = GONE; if (mSubmitButtonEnabled && hasFocus() && (hasText || !mVoiceButtonEnabled)) { visibility = VISIBLE; } mGoButton.setVisibility(visibility); } private void updateCloseButton() { final boolean hasText = !TextUtils.isEmpty(mSearchSrcTextView.getText()); // Should we show the close button? It is not shown if there's no focus, // field is not iconified by default and there is no text in it. final boolean showClose = hasText || (mIconifiedByDefault && !mExpandedInActionView); mCloseButton.setVisibility(showClose ? VISIBLE : GONE); final Drawable closeButtonImg = mCloseButton.getDrawable(); if (closeButtonImg != null) { closeButtonImg.setState(hasText ? ENABLED_STATE_SET : EMPTY_STATE_SET); } } void setImeVisibility(final boolean visible) { if (visible) { post(mShowImeRunnable); } else { removeCallbacks(mShowImeRunnable); InputMethodManager imm = (InputMethodManager) getContext() .getSystemService(Context.INPUT_METHOD_SERVICE); if (imm != null) { imm.hideSoftInputFromWindow(getWindowToken(), 0); } } } /** * Called by the SuggestionsAdapter */ void onQueryRefine(CharSequence queryText) { setQuery(queryText); } private final OnClickListener mOnClickListener = new OnClickListener() { @Override public void onClick(View v) { if (v == mNavigationButton) { if (mOnNavigateBackListener != null) { mOnNavigateBackListener.onNavigateBack(); } } else if (v == mCloseButton) { onCloseClicked(); } else if (v == mGoButton) { onSubmitQuery(); } else if (v == mVoiceButton) { onVoiceClicked(); } else if (v == mSearchSrcTextView) { forceSuggestionQuery(); } } }; /** * React to the user typing "enter" or other hardwired keys while typing in * the search box. This handles these special keys while the edit box has * focus. */ View.OnKeyListener mTextKeyListener = new View.OnKeyListener() { @Override public boolean onKey(View v, int keyCode, KeyEvent event) { // If there is text in the query box, handle enter, and action keys // The search key is handled by the dialog's onKeyDown(). if (!mSearchSrcTextView.isEmpty() && KeyEventCompat.hasNoModifiers(event)) { if (event.getAction() == KeyEvent.ACTION_UP) { if (keyCode == KeyEvent.KEYCODE_ENTER) { v.cancelLongPress(); // Launch as a regular search. launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null, mSearchSrcTextView.getText().toString()); return true; } } } return false; } }; private CharSequence getDecoratedHint(CharSequence hintText) { // If the field is always expanded or we don't have a search hint icon, // then don't add the search icon to the hint. if (!mIconifiedByDefault || mSearchHintIcon == null) { return hintText; } final int textSize = (int) (mSearchSrcTextView.getTextSize() * 1.25); mSearchHintIcon.setBounds(0, 0, textSize, textSize); final SpannableStringBuilder ssb = new SpannableStringBuilder(" "); ssb.setSpan(new ImageSpan(mSearchHintIcon), 1, 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); ssb.append(hintText); return ssb; } private void updateQueryHint() { final CharSequence hint = getQueryHint(); mSearchSrcTextView.setHint(getDecoratedHint(hint == null ? "" : hint)); } /** * Update the visibility of the voice button. There are actually two voice search modes, * either of which will activate the button. * @param empty whether the search query text field is empty. If it is, then the other * criteria apply to make the voice button visible. */ private void updateVoiceButton(boolean empty) { int visibility = GONE; if (mVoiceButtonEnabled && !isIconified() && empty) { visibility = VISIBLE; mGoButton.setVisibility(GONE); } mVoiceButton.setVisibility(visibility); } private final OnEditorActionListener mOnEditorActionListener = new OnEditorActionListener() { /** * Called when the input method default action key is pressed. */ @Override public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { onSubmitQuery(); return true; } }; void onTextChanged(CharSequence newText) { CharSequence text = mSearchSrcTextView.getText(); mUserQuery = text; boolean hasText = !TextUtils.isEmpty(text); updateSubmitButton(hasText); updateVoiceButton(!hasText); updateCloseButton(); if (mOnQueryChangeListener != null && !TextUtils.equals(newText, mOldQueryText)) { mOnQueryChangeListener.onQueryTextChange(newText.toString()); } mOldQueryText = newText.toString(); } void onSubmitQuery() { CharSequence query = mSearchSrcTextView.getText(); if (query != null && TextUtils.getTrimmedLength(query) > 0) { if (mOnQueryChangeListener == null || !mOnQueryChangeListener.onQueryTextSubmit(query.toString())) { if (mSearchable != null) { launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null, query.toString()); } setImeVisibility(false); dismissSuggestions(); } } } private void dismissSuggestions() { mSearchSrcTextView.dismissDropDown(); } void onCloseClicked() { CharSequence text = mSearchSrcTextView.getText(); if (TextUtils.isEmpty(text)) { if (mIconifiedByDefault) { // If the app doesn't override the close behavior if (mOnCloseListener == null || !mOnCloseListener.onClose()) { // hide the keyboard and remove focus clearFocus(); // collapse the search field updateViewsVisibility(true); } } } else { mSearchSrcTextView.setText(""); mSearchSrcTextView.requestFocus(); setImeVisibility(true); } } void onSearchClicked() { updateViewsVisibility(false); mSearchSrcTextView.requestFocus(); setImeVisibility(true); if (mOnSearchClickListener != null) { mOnSearchClickListener.onClick(this); } } void onVoiceClicked() { // guard against possible race conditions if (mSearchable == null) { return; } SearchableInfo searchable = mSearchable; try { if (searchable.getVoiceSearchLaunchWebSearch()) { Intent webSearchIntent = createVoiceWebSearchIntent(mVoiceWebSearchIntent, searchable); getContext().startActivity(webSearchIntent); } else if (searchable.getVoiceSearchLaunchRecognizer()) { Intent appSearchIntent = createVoiceAppSearchIntent(mVoiceAppSearchIntent, searchable); getContext().startActivity(appSearchIntent); } } catch (ActivityNotFoundException e) { // Should not happen, since we check the availability of // voice search before showing the button. But just in case... Log.w(LOG_TAG, "Could not find voice search activity"); } } void onTextFocusChanged() { updateViewsVisibility(isIconified()); // Delayed update to make sure that the focus has settled down and window focus changes // don't affect it. A synchronous update was not working. if (mSearchSrcTextView.hasFocus()) { forceSuggestionQuery(); } } /** * {@inheritDoc} */ @Override public void onActionViewCollapsed() { setQuery("", false); clearFocus(); updateViewsVisibility(true); mSearchSrcTextView.setImeOptions(mCollapsedImeOptions); mExpandedInActionView = false; } /** * {@inheritDoc} */ @Override public void onActionViewExpanded() { if (mExpandedInActionView) return; mExpandedInActionView = true; mCollapsedImeOptions = mSearchSrcTextView.getImeOptions(); mSearchSrcTextView.setImeOptions(mCollapsedImeOptions | EditorInfo.IME_FLAG_NO_FULLSCREEN); mSearchSrcTextView.setText(""); setIconified(false); } static class SavedState extends AbsSavedState { boolean isIconified; SavedState(Parcelable superState) { super(superState); } public SavedState(Parcel source, ClassLoader loader) { super(source, loader); isIconified = (Boolean) source.readValue(null); } @Override public void writeToParcel(Parcel dest, int flags) { super.writeToParcel(dest, flags); dest.writeValue(isIconified); } @Override public String toString() { return "SearchView.SavedState{" + Integer.toHexString(System.identityHashCode(this)) + " isIconified=" + isIconified + "}"; } public static final Parcelable.Creator<SavedState> CREATOR = ParcelableCompat .newCreator(new ParcelableCompatCreatorCallbacks<SavedState>() { @Override public SavedState createFromParcel(Parcel in, ClassLoader loader) { return new SavedState(in, loader); } @Override public SavedState[] newArray(int size) { return new SavedState[size]; } }); } @Override protected Parcelable onSaveInstanceState() { Parcelable superState = super.onSaveInstanceState(); SavedState ss = new SavedState(superState); ss.isIconified = isIconified(); return ss; } @Override protected void onRestoreInstanceState(Parcelable state) { if (!(state instanceof SavedState)) { super.onRestoreInstanceState(state); return; } SavedState ss = (SavedState) state; super.onRestoreInstanceState(ss.getSuperState()); updateViewsVisibility(ss.isIconified); requestLayout(); } /** * Query rewriting. */ private void rewriteQueryFromSuggestion(int position) { CharSequence oldQuery = mSearchSrcTextView.getText(); Cursor c = mSuggestionsAdapter.getCursor(); if (c == null) { return; } if (c.moveToPosition(position)) { // Get the new query from the suggestion. CharSequence newQuery = mSuggestionsAdapter.convertToString(c); if (newQuery != null) { // The suggestion rewrites the query. // Update the text field, without getting new suggestions. setQuery(newQuery); } else { // The suggestion does not rewrite the query, restore the user's query. setQuery(oldQuery); } } else { // We got a bad position, restore the user's query. setQuery(oldQuery); } } /** * Launches an intent based on a suggestion. * * @param position The index of the suggestion to create the intent from. * @param actionKey The key code of the action key that was pressed, * or {@link KeyEvent#KEYCODE_UNKNOWN} if none. * @param actionMsg The message for the action key that was pressed, * or <code>null</code> if none. * @return true if a successful launch, false if could not (e.g. bad position). */ private boolean launchSuggestion(int position, int actionKey, String actionMsg) { Cursor c = mSuggestionsAdapter.getCursor(); if ((c != null) && c.moveToPosition(position)) { Intent intent = createIntentFromSuggestion(c, actionKey, actionMsg); // launch the intent launchIntent(intent); return true; } return false; } /** * Launches an intent, including any special intent handling. */ private void launchIntent(Intent intent) { if (intent == null) { return; } try { // If the intent was created from a suggestion, it will always have an explicit // component here. getContext().startActivity(intent); } catch (RuntimeException ex) { Log.e(LOG_TAG, "Failed launch activity: " + intent, ex); } } /** * Sets the text in the query box, without updating the suggestions. */ private void setQuery(CharSequence query) { mSearchSrcTextView.setText(query); // Move the cursor to the end mSearchSrcTextView.setSelection(TextUtils.isEmpty(query) ? 0 : query.length()); } void launchQuerySearch(int actionKey, String actionMsg, String query) { String action = Intent.ACTION_SEARCH; Intent intent = createIntent(action, null, null, query, actionKey, actionMsg); getContext().startActivity(intent); } /** * Constructs an intent from the given information and the search dialog state. * * @param action Intent action. * @param data Intent data, or <code>null</code>. * @param extraData Data for {@link SearchManager#EXTRA_DATA_KEY} or <code>null</code>. * @param query Intent query, or <code>null</code>. * @param actionKey The key code of the action key that was pressed, * or {@link KeyEvent#KEYCODE_UNKNOWN} if none. * @param actionMsg The message for the action key that was pressed, * or <code>null</code> if none. * @return The intent. */ private Intent createIntent(String action, Uri data, String extraData, String query, int actionKey, String actionMsg) { // Now build the Intent Intent intent = new Intent(action); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); // We need CLEAR_TOP to avoid reusing an old task that has other activities // on top of the one we want. We don't want to do this in in-app search though, // as it can be destructive to the activity stack. if (data != null) { intent.setData(data); } intent.putExtra(SearchManager.USER_QUERY, mUserQuery); if (query != null) { intent.putExtra(SearchManager.QUERY, query); } if (extraData != null) { intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData); } if (mAppSearchData != null) { intent.putExtra(SearchManager.APP_DATA, mAppSearchData); } if (actionKey != KeyEvent.KEYCODE_UNKNOWN) { intent.putExtra(SearchManager.ACTION_KEY, actionKey); intent.putExtra(SearchManager.ACTION_MSG, actionMsg); } intent.setComponent(mSearchable.getSearchActivity()); return intent; } /** * Create and return an Intent that can launch the voice search activity for web search. */ private Intent createVoiceWebSearchIntent(Intent baseIntent, SearchableInfo searchable) { Intent voiceIntent = new Intent(baseIntent); ComponentName searchActivity = searchable.getSearchActivity(); voiceIntent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, searchActivity == null ? null : searchActivity.flattenToShortString()); return voiceIntent; } /** * Create and return an Intent that can launch the voice search activity, perform a specific * voice transcription, and forward the results to the searchable activity. * * @param baseIntent The voice app search intent to start from * @return A completely-configured intent ready to send to the voice search activity */ private Intent createVoiceAppSearchIntent(Intent baseIntent, SearchableInfo searchable) { ComponentName searchActivity = searchable.getSearchActivity(); // create the necessary intent to set up a search-and-forward operation // in the voice search system. We have to keep the bundle separate, // because it becomes immutable once it enters the PendingIntent Intent queryIntent = new Intent(Intent.ACTION_SEARCH); queryIntent.setComponent(searchActivity); PendingIntent pending = PendingIntent.getActivity(getContext(), 0, queryIntent, PendingIntent.FLAG_ONE_SHOT); // Now set up the bundle that will be inserted into the pending intent // when it's time to do the search. We always build it here (even if empty) // because the voice search activity will always need to insert "QUERY" into // it anyway. Bundle queryExtras = new Bundle(); if (mAppSearchData != null) { queryExtras.putParcelable(SearchManager.APP_DATA, mAppSearchData); } // Now build the intent to launch the voice search. Add all necessary // extras to launch the voice recognizer, and then all the necessary extras // to forward the results to the searchable activity Intent voiceIntent = new Intent(baseIntent); // Add all of the configuration options supplied by the searchable's metadata String languageModel = RecognizerIntent.LANGUAGE_MODEL_FREE_FORM; String prompt = null; String language = null; int maxResults = 1; Resources resources = getResources(); if (searchable.getVoiceLanguageModeId() != 0) { languageModel = resources.getString(searchable.getVoiceLanguageModeId()); } if (searchable.getVoicePromptTextId() != 0) { prompt = resources.getString(searchable.getVoicePromptTextId()); } if (searchable.getVoiceLanguageId() != 0) { language = resources.getString(searchable.getVoiceLanguageId()); } if (searchable.getVoiceMaxResults() != 0) { maxResults = searchable.getVoiceMaxResults(); } voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, languageModel); voiceIntent.putExtra(RecognizerIntent.EXTRA_PROMPT, prompt); voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, language); voiceIntent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, maxResults); voiceIntent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, searchActivity == null ? null : searchActivity.flattenToShortString()); // Add the values that configure forwarding the results voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT, pending); voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT_BUNDLE, queryExtras); return voiceIntent; } /** * When a particular suggestion has been selected, perform the various lookups required * to use the suggestion. This includes checking the cursor for suggestion-specific data, * and/or falling back to the XML for defaults; It also creates REST style Uri data when * the suggestion includes a data id. * * @param c The suggestions cursor, moved to the row of the user's selection * @param actionKey The key code of the action key that was pressed, * or {@link KeyEvent#KEYCODE_UNKNOWN} if none. * @param actionMsg The message for the action key that was pressed, * or <code>null</code> if none. * @return An intent for the suggestion at the cursor's position. */ private Intent createIntentFromSuggestion(Cursor c, int actionKey, String actionMsg) { try { // use specific action if supplied, or default action if supplied, or fixed default String action = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_ACTION); if (action == null) { action = mSearchable.getSuggestIntentAction(); } if (action == null) { action = Intent.ACTION_SEARCH; } // use specific data if supplied, or default data if supplied String data = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA); if (data == null) { data = mSearchable.getSuggestIntentData(); } // then, if an ID was provided, append it. if (data != null) { String id = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID); if (id != null) { data = data + "/" + Uri.encode(id); } } Uri dataUri = (data == null) ? null : Uri.parse(data); String query = getColumnString(c, SearchManager.SUGGEST_COLUMN_QUERY); String extraData = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA); return createIntent(action, dataUri, extraData, query, actionKey, actionMsg); } catch (RuntimeException e) { int rowNum; try { // be really paranoid now rowNum = c.getPosition(); } catch (RuntimeException e2) { rowNum = -1; } Log.w(LOG_TAG, "Search suggestions cursor at row " + rowNum + " returned exception.", e); return null; } } /** * Gets the value of a string column by name. * * @param cursor Cursor to read the value from. * @param columnName The name of the column to read. * @return The value of the given column, or <code>null</null> * if the cursor does not contain the given column. */ public static String getColumnString(Cursor cursor, String columnName) { int col = cursor.getColumnIndex(columnName); return getStringOrNull(cursor, col); } private static String getStringOrNull(Cursor cursor, int col) { if (col == -1) { return null; } try { return cursor.getString(col); } catch (Exception e) { Log.e(LOG_TAG, "unexpected error retrieving valid column from cursor, " + "did the remote process die?", e); return null; } } void forceSuggestionQuery() { HIDDEN_METHOD_INVOKER.doBeforeTextChanged(mSearchSrcTextView); HIDDEN_METHOD_INVOKER.doAfterTextChanged(mSearchSrcTextView); } static boolean isLandscapeMode(Context context) { return context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; } public SearchAutoComplete getSearchSrcTextView() { return mSearchSrcTextView; } /** * Callback to watch the text field for empty/non-empty */ private TextWatcher mTextWatcher = new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int before, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int after) { MySearchView.this.onTextChanged(s); } @Override public void afterTextChanged(Editable s) { } }; private static class UpdatableTouchDelegate extends TouchDelegate { /** * View that should receive forwarded touch events */ private final View mDelegateView; /** * Bounds in local coordinates of the containing view that should be mapped to the delegate * view. This rect is used for initial hit testing. */ private final Rect mTargetBounds; /** * Bounds in local coordinates of the containing view that are actual bounds of the delegate * view. This rect is used for event coordinate mapping. */ private final Rect mActualBounds; /** * mTargetBounds inflated to include some slop. This rect is to track whether the motion events * should be considered to be be within the delegate view. */ private final Rect mSlopBounds; private final int mSlop; /** * True if the delegate had been targeted on a down event (intersected mTargetBounds). */ private boolean mDelegateTargeted; public UpdatableTouchDelegate(Rect targetBounds, Rect actualBounds, View delegateView) { super(targetBounds, delegateView); mSlop = ViewConfiguration.get(delegateView.getContext()).getScaledTouchSlop(); mTargetBounds = new Rect(); mSlopBounds = new Rect(); mActualBounds = new Rect(); setBounds(targetBounds, actualBounds); mDelegateView = delegateView; } public void setBounds(Rect desiredBounds, Rect actualBounds) { mTargetBounds.set(desiredBounds); mSlopBounds.set(desiredBounds); mSlopBounds.inset(-mSlop, -mSlop); mActualBounds.set(actualBounds); } @Override public boolean onTouchEvent(MotionEvent event) { final int x = (int) event.getX(); final int y = (int) event.getY(); boolean sendToDelegate = false; boolean hit = true; boolean handled = false; switch (event.getAction()) { case MotionEvent.ACTION_DOWN: if (mTargetBounds.contains(x, y)) { mDelegateTargeted = true; sendToDelegate = true; } break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_MOVE: sendToDelegate = mDelegateTargeted; if (sendToDelegate) { if (!mSlopBounds.contains(x, y)) { hit = false; } } break; case MotionEvent.ACTION_CANCEL: sendToDelegate = mDelegateTargeted; mDelegateTargeted = false; break; } if (sendToDelegate) { if (hit && !mActualBounds.contains(x, y)) { // Offset event coordinates to be in the center of the target view since we // are within the targetBounds, but not inside the actual bounds of // mDelegateView event.setLocation(mDelegateView.getWidth() / 2, mDelegateView.getHeight() / 2); } else { // Offset event coordinates to the target view coordinates. event.setLocation(x - mActualBounds.left, y - mActualBounds.top); } handled = mDelegateView.dispatchTouchEvent(event); } return handled; } } /** * Local subclass for AutoCompleteTextView. */ public static class SearchAutoComplete extends AppCompatAutoCompleteTextView { private MySearchView mMySearchView; public SearchAutoComplete(Context context) { this(context, null); } public SearchAutoComplete(Context context, AttributeSet attrs) { this(context, attrs, R.attr.autoCompleteTextViewStyle); } public SearchAutoComplete(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } @Override protected void onFinishInflate() { super.onFinishInflate(); DisplayMetrics metrics = getResources().getDisplayMetrics(); setMinWidth((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, getSearchViewTextMinWidthDp(), metrics)); } void setSearchView(MySearchView mySearchView) { mMySearchView = mySearchView; } /** * Returns true if the text field is empty, or contains only whitespace. */ private boolean isEmpty() { return TextUtils.getTrimmedLength(getText()) == 0; } /** * We override this method to avoid replacing the query box text when a * suggestion is clicked. */ @Override protected void replaceText(CharSequence text) { } /** * We override this method to avoid an extra onItemClick being called on * the drop-down's OnItemClickListener by * {@link AutoCompleteTextView#onKeyUp(int, KeyEvent)} when an item is * clicked with the trackball. */ @Override public void performCompletion() { } /** * We override this method to be sure and show the soft keyboard if * appropriate when the TextView has focus. */ @Override public void onWindowFocusChanged(boolean hasWindowFocus) { super.onWindowFocusChanged(hasWindowFocus); if (hasWindowFocus && mMySearchView.hasFocus() && getVisibility() == VISIBLE) { InputMethodManager inputManager = (InputMethodManager) getContext() .getSystemService(Context.INPUT_METHOD_SERVICE); inputManager.showSoftInput(this, 0); // If in landscape mode, then make sure that // the ime is in front of the dropdown. if (isLandscapeMode(getContext())) { HIDDEN_METHOD_INVOKER.ensureImeVisible(this, true); } } } @Override protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { super.onFocusChanged(focused, direction, previouslyFocusedRect); mMySearchView.onTextFocusChanged(); } @Override public boolean onKeyPreIme(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_BACK) { // special case for the back key, we do not even try to send it // to the drop down list but instead, consume it immediately if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) { KeyEvent.DispatcherState state = getKeyDispatcherState(); if (state != null) { state.startTracking(event, this); } return true; } else if (event.getAction() == KeyEvent.ACTION_UP) { KeyEvent.DispatcherState state = getKeyDispatcherState(); if (state != null) { state.handleUpEvent(event); } if (event.isTracking() && !event.isCanceled()) { mMySearchView.clearFocus(); mMySearchView.setImeVisibility(false); return true; } } } return super.onKeyPreIme(keyCode, event); } /** * Get minimum width of the search view text entry area. */ private int getSearchViewTextMinWidthDp() { final Configuration config = getResources().getConfiguration(); final int widthDp = ConfigurationHelper.getScreenWidthDp(getResources()); final int heightDp = ConfigurationHelper.getScreenHeightDp(getResources()); if (widthDp >= 960 && heightDp >= 720 && config.orientation == Configuration.ORIENTATION_LANDSCAPE) { return 256; } else if (widthDp >= 600 || (widthDp >= 640 && heightDp >= 480)) { return 192; } return 160; } } private static class AutoCompleteTextViewReflector { private Method doBeforeTextChanged, doAfterTextChanged; private Method ensureImeVisible; private Method showSoftInputUnchecked; AutoCompleteTextViewReflector() { try { doBeforeTextChanged = AutoCompleteTextView.class.getDeclaredMethod("doBeforeTextChanged"); doBeforeTextChanged.setAccessible(true); } catch (NoSuchMethodException e) { // Ah well. } try { doAfterTextChanged = AutoCompleteTextView.class.getDeclaredMethod("doAfterTextChanged"); doAfterTextChanged.setAccessible(true); } catch (NoSuchMethodException e) { // Ah well. } try { ensureImeVisible = AutoCompleteTextView.class.getMethod("ensureImeVisible", boolean.class); ensureImeVisible.setAccessible(true); } catch (NoSuchMethodException e) { // Ah well. } try { showSoftInputUnchecked = InputMethodManager.class.getMethod("showSoftInputUnchecked", int.class, ResultReceiver.class); showSoftInputUnchecked.setAccessible(true); } catch (NoSuchMethodException e) { // Ah well. } } void doBeforeTextChanged(AutoCompleteTextView view) { if (doBeforeTextChanged != null) { try { doBeforeTextChanged.invoke(view); } catch (Exception e) { } } } void doAfterTextChanged(AutoCompleteTextView view) { if (doAfterTextChanged != null) { try { doAfterTextChanged.invoke(view); } catch (Exception e) { } } } void ensureImeVisible(AutoCompleteTextView view, boolean visible) { if (ensureImeVisible != null) { try { ensureImeVisible.invoke(view, visible); } catch (Exception e) { } } } void showSoftInputUnchecked(InputMethodManager imm, View view, int flags) { if (showSoftInputUnchecked != null) { try { showSoftInputUnchecked.invoke(imm, flags, null); return; } catch (Exception e) { } } // Hidden method failed, call public version instead imm.showSoftInput(view, flags); } } }