Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 1 | /* |
| 2 | * Copyright (C) 2010 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.widget; |
| 18 | |
| 19 | import static android.widget.SuggestionsAdapter.getColumnString; |
| 20 | |
Alan Viverette | b4004df | 2015-04-29 16:55:42 -0700 | [diff] [blame] | 21 | import android.annotation.Nullable; |
Amith Yamasani | ebcf5a3a | 2010-10-13 11:35:24 -0700 | [diff] [blame] | 22 | import android.app.PendingIntent; |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 23 | import android.app.SearchManager; |
| 24 | import android.app.SearchableInfo; |
Artur Satayev | ed5a6ae | 2019-12-10 17:47:54 +0000 | [diff] [blame] | 25 | import android.compat.annotation.UnsupportedAppUsage; |
Amith Yamasani | ebcf5a3a | 2010-10-13 11:35:24 -0700 | [diff] [blame] | 26 | import android.content.ActivityNotFoundException; |
| 27 | import android.content.ComponentName; |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 28 | import android.content.Context; |
| 29 | import android.content.Intent; |
Amith Yamasani | ebcf5a3a | 2010-10-13 11:35:24 -0700 | [diff] [blame] | 30 | import android.content.pm.PackageManager; |
| 31 | import android.content.pm.ResolveInfo; |
Amith Yamasani | 968ec93 | 2010-12-02 14:00:47 -0800 | [diff] [blame] | 32 | import android.content.res.Configuration; |
Amith Yamasani | ebcf5a3a | 2010-10-13 11:35:24 -0700 | [diff] [blame] | 33 | import android.content.res.Resources; |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 34 | import android.content.res.TypedArray; |
| 35 | import android.database.Cursor; |
repo sync | 6a81b82 | 2010-09-28 13:00:05 -0700 | [diff] [blame] | 36 | import android.graphics.Rect; |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 37 | import android.graphics.drawable.Drawable; |
| 38 | import android.net.Uri; |
Mathew Inwood | 8c854f8 | 2018-09-14 12:35:36 +0100 | [diff] [blame] | 39 | import android.os.Build; |
Amith Yamasani | ebcf5a3a | 2010-10-13 11:35:24 -0700 | [diff] [blame] | 40 | import android.os.Bundle; |
Aurimas Liutikas | 13fdea0 | 2016-02-11 10:10:10 -0800 | [diff] [blame] | 41 | import android.os.Parcel; |
| 42 | import android.os.Parcelable; |
Amith Yamasani | ebcf5a3a | 2010-10-13 11:35:24 -0700 | [diff] [blame] | 43 | import android.speech.RecognizerIntent; |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 44 | import android.text.Editable; |
Amith Yamasani | 5607a38 | 2011-08-09 14:16:37 -0700 | [diff] [blame] | 45 | import android.text.InputType; |
Amith Yamasani | b4569fb | 2011-07-08 15:25:39 -0700 | [diff] [blame] | 46 | import android.text.Spannable; |
| 47 | import android.text.SpannableStringBuilder; |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 48 | import android.text.TextUtils; |
| 49 | import android.text.TextWatcher; |
Amith Yamasani | b4569fb | 2011-07-08 15:25:39 -0700 | [diff] [blame] | 50 | import android.text.style.ImageSpan; |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 51 | import android.util.AttributeSet; |
Filip Gruszczynski | b635fda | 2015-12-03 18:37:38 -0800 | [diff] [blame] | 52 | import android.util.DisplayMetrics; |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 53 | import android.util.Log; |
Filip Gruszczynski | b635fda | 2015-12-03 18:37:38 -0800 | [diff] [blame] | 54 | import android.util.TypedValue; |
Amith Yamasani | 763bc07 | 2011-07-22 11:53:47 -0700 | [diff] [blame] | 55 | import android.view.CollapsibleActionView; |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 56 | import android.view.KeyEvent; |
| 57 | import android.view.LayoutInflater; |
Aurimas Liutikas | c8fd00a | 2016-04-15 13:55:53 -0700 | [diff] [blame] | 58 | import android.view.MotionEvent; |
| 59 | import android.view.TouchDelegate; |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 60 | import android.view.View; |
Aurimas Liutikas | c8fd00a | 2016-04-15 13:55:53 -0700 | [diff] [blame] | 61 | import android.view.ViewConfiguration; |
Amith Yamasani | 5607a38 | 2011-08-09 14:16:37 -0700 | [diff] [blame] | 62 | import android.view.inputmethod.EditorInfo; |
Yohei Yukawa | 5cfc1b4 | 2017-03-07 00:52:09 -0800 | [diff] [blame] | 63 | import android.view.inputmethod.InputConnection; |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 64 | import android.view.inputmethod.InputMethodManager; |
Ashley Rose | 55f9f92 | 2019-01-28 19:29:36 -0500 | [diff] [blame] | 65 | import android.view.inspector.InspectableProperty; |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 66 | import android.widget.AdapterView.OnItemClickListener; |
| 67 | import android.widget.AdapterView.OnItemSelectedListener; |
| 68 | import android.widget.TextView.OnEditorActionListener; |
| 69 | |
Amith Yamasani | b4569fb | 2011-07-08 15:25:39 -0700 | [diff] [blame] | 70 | import com.android.internal.R; |
| 71 | |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 72 | import java.util.WeakHashMap; |
| 73 | |
| 74 | /** |
Amith Yamasani | 763bc07 | 2011-07-22 11:53:47 -0700 | [diff] [blame] | 75 | * A widget that provides a user interface for the user to enter a search query and submit a request |
| 76 | * to a search provider. Shows a list of query suggestions or results, if available, and allows the |
| 77 | * user to pick a suggestion or result to launch into. |
Amith Yamasani | 5931b1f | 2010-10-18 16:13:14 -0700 | [diff] [blame] | 78 | * |
Amith Yamasani | 763bc07 | 2011-07-22 11:53:47 -0700 | [diff] [blame] | 79 | * <p> |
| 80 | * When the SearchView is used in an ActionBar as an action view for a collapsible menu item, it |
| 81 | * needs to be set to iconified by default using {@link #setIconifiedByDefault(boolean) |
| 82 | * setIconifiedByDefault(true)}. This is the default, so nothing needs to be done. |
| 83 | * </p> |
| 84 | * <p> |
| 85 | * If you want the search field to always be visible, then call setIconifiedByDefault(false). |
| 86 | * </p> |
Amith Yamasani | 5931b1f | 2010-10-18 16:13:14 -0700 | [diff] [blame] | 87 | * |
Joe Fernandez | 3aef8e1d | 2011-12-20 10:38:34 -0800 | [diff] [blame] | 88 | * <div class="special reference"> |
| 89 | * <h3>Developer Guides</h3> |
| 90 | * <p>For information about using {@code SearchView}, read the |
| 91 | * <a href="{@docRoot}guide/topics/search/index.html">Search</a> developer guide.</p> |
| 92 | * </div> |
Amith Yamasani | 763bc07 | 2011-07-22 11:53:47 -0700 | [diff] [blame] | 93 | * |
| 94 | * @see android.view.MenuItem#SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW |
Amith Yamasani | 5931b1f | 2010-10-18 16:13:14 -0700 | [diff] [blame] | 95 | * @attr ref android.R.styleable#SearchView_iconifiedByDefault |
Amith Yamasani | 5607a38 | 2011-08-09 14:16:37 -0700 | [diff] [blame] | 96 | * @attr ref android.R.styleable#SearchView_imeOptions |
| 97 | * @attr ref android.R.styleable#SearchView_inputType |
Amith Yamasani | 5931b1f | 2010-10-18 16:13:14 -0700 | [diff] [blame] | 98 | * @attr ref android.R.styleable#SearchView_maxWidth |
Scott Main | abdf0d5 | 2011-02-08 10:20:27 -0800 | [diff] [blame] | 99 | * @attr ref android.R.styleable#SearchView_queryHint |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 100 | */ |
Amith Yamasani | 763bc07 | 2011-07-22 11:53:47 -0700 | [diff] [blame] | 101 | public class SearchView extends LinearLayout implements CollapsibleActionView { |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 102 | |
| 103 | private static final boolean DBG = false; |
| 104 | private static final String LOG_TAG = "SearchView"; |
| 105 | |
Luca Zanolin | 535698c | 2011-10-06 13:36:15 +0100 | [diff] [blame] | 106 | /** |
| 107 | * Private constant for removing the microphone in the keyboard. |
| 108 | */ |
| 109 | private static final String IME_OPTION_NO_MICROPHONE = "nm"; |
| 110 | |
Mathew Inwood | 978c6e2 | 2018-08-21 15:58:55 +0100 | [diff] [blame] | 111 | @UnsupportedAppUsage |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 112 | private final SearchAutoComplete mSearchSrcTextView; |
Mathew Inwood | 978c6e2 | 2018-08-21 15:58:55 +0100 | [diff] [blame] | 113 | @UnsupportedAppUsage |
Alan Viverette | 5dddb70 | 2014-07-02 15:46:04 -0700 | [diff] [blame] | 114 | private final View mSearchEditFrame; |
Mathew Inwood | 978c6e2 | 2018-08-21 15:58:55 +0100 | [diff] [blame] | 115 | @UnsupportedAppUsage |
Alan Viverette | 5dddb70 | 2014-07-02 15:46:04 -0700 | [diff] [blame] | 116 | private final View mSearchPlate; |
Mathew Inwood | 978c6e2 | 2018-08-21 15:58:55 +0100 | [diff] [blame] | 117 | @UnsupportedAppUsage |
Alan Viverette | 5dddb70 | 2014-07-02 15:46:04 -0700 | [diff] [blame] | 118 | private final View mSubmitArea; |
Mathew Inwood | 978c6e2 | 2018-08-21 15:58:55 +0100 | [diff] [blame] | 119 | @UnsupportedAppUsage |
Alan Viverette | 5dddb70 | 2014-07-02 15:46:04 -0700 | [diff] [blame] | 120 | private final ImageView mSearchButton; |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 121 | private final ImageView mGoButton; |
Mathew Inwood | 978c6e2 | 2018-08-21 15:58:55 +0100 | [diff] [blame] | 122 | @UnsupportedAppUsage |
Alan Viverette | 5dddb70 | 2014-07-02 15:46:04 -0700 | [diff] [blame] | 123 | private final ImageView mCloseButton; |
Mathew Inwood | 978c6e2 | 2018-08-21 15:58:55 +0100 | [diff] [blame] | 124 | @UnsupportedAppUsage |
Alan Viverette | 5dddb70 | 2014-07-02 15:46:04 -0700 | [diff] [blame] | 125 | private final ImageView mVoiceButton; |
Alan Viverette | 5dddb70 | 2014-07-02 15:46:04 -0700 | [diff] [blame] | 126 | private final View mDropDownAnchor; |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 127 | |
Aurimas Liutikas | c8fd00a | 2016-04-15 13:55:53 -0700 | [diff] [blame] | 128 | private UpdatableTouchDelegate mTouchDelegate; |
| 129 | private Rect mSearchSrcTextViewBounds = new Rect(); |
| 130 | private Rect mSearchSrtTextViewBoundsExpanded = new Rect(); |
| 131 | private int[] mTemp = new int[2]; |
| 132 | private int[] mTemp2 = new int[2]; |
| 133 | |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 134 | /** Icon optionally displayed when the SearchView is collapsed. */ |
| 135 | private final ImageView mCollapsedIcon; |
| 136 | |
| 137 | /** Drawable used as an EditText hint. */ |
Mathew Inwood | 978c6e2 | 2018-08-21 15:58:55 +0100 | [diff] [blame] | 138 | @UnsupportedAppUsage |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 139 | private final Drawable mSearchHintIcon; |
Alan Viverette | 5dddb70 | 2014-07-02 15:46:04 -0700 | [diff] [blame] | 140 | |
| 141 | // Resources used by SuggestionsAdapter to display suggestions. |
| 142 | private final int mSuggestionRowLayout; |
| 143 | private final int mSuggestionCommitIconResId; |
| 144 | |
| 145 | // Intents used for voice searching. |
| 146 | private final Intent mVoiceWebSearchIntent; |
| 147 | private final Intent mVoiceAppSearchIntent; |
| 148 | |
Alan Viverette | b4004df | 2015-04-29 16:55:42 -0700 | [diff] [blame] | 149 | private final CharSequence mDefaultQueryHint; |
| 150 | |
Mathew Inwood | 978c6e2 | 2018-08-21 15:58:55 +0100 | [diff] [blame] | 151 | @UnsupportedAppUsage |
Adam Powell | 01f2135 | 2011-01-20 18:30:10 -0800 | [diff] [blame] | 152 | private OnQueryTextListener mOnQueryChangeListener; |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 153 | private OnCloseListener mOnCloseListener; |
Amith Yamasani | 0594476 | 2010-10-08 13:52:38 -0700 | [diff] [blame] | 154 | private OnFocusChangeListener mOnQueryTextFocusChangeListener; |
Adam Powell | 01f2135 | 2011-01-20 18:30:10 -0800 | [diff] [blame] | 155 | private OnSuggestionListener mOnSuggestionListener; |
Amith Yamasani | 4838548 | 2010-12-03 14:43:52 -0800 | [diff] [blame] | 156 | private OnClickListener mOnSearchClickListener; |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 157 | |
Mathew Inwood | 978c6e2 | 2018-08-21 15:58:55 +0100 | [diff] [blame] | 158 | @UnsupportedAppUsage |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 159 | private boolean mIconifiedByDefault; |
Mathew Inwood | 978c6e2 | 2018-08-21 15:58:55 +0100 | [diff] [blame] | 160 | @UnsupportedAppUsage |
Amith Yamasani | 9322775 | 2010-09-14 10:10:54 -0700 | [diff] [blame] | 161 | private boolean mIconified; |
Mathew Inwood | 978c6e2 | 2018-08-21 15:58:55 +0100 | [diff] [blame] | 162 | @UnsupportedAppUsage |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 163 | private CursorAdapter mSuggestionsAdapter; |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 164 | private boolean mSubmitButtonEnabled; |
| 165 | private CharSequence mQueryHint; |
Amith Yamasani | e678f46 | 2010-09-15 16:13:43 -0700 | [diff] [blame] | 166 | private boolean mQueryRefinement; |
Mathew Inwood | 978c6e2 | 2018-08-21 15:58:55 +0100 | [diff] [blame] | 167 | @UnsupportedAppUsage |
Amith Yamasani | 0594476 | 2010-10-08 13:52:38 -0700 | [diff] [blame] | 168 | private boolean mClearingFocus; |
Amith Yamasani | 5931b1f | 2010-10-18 16:13:14 -0700 | [diff] [blame] | 169 | private int mMaxWidth; |
Mathew Inwood | 978c6e2 | 2018-08-21 15:58:55 +0100 | [diff] [blame] | 170 | @UnsupportedAppUsage |
Amith Yamasani | 9b2e302 | 2011-01-14 11:34:12 -0800 | [diff] [blame] | 171 | private boolean mVoiceButtonEnabled; |
Amith Yamasani | b47c4fd | 2011-08-04 14:30:07 -0700 | [diff] [blame] | 172 | private CharSequence mOldQueryText; |
Mathew Inwood | 978c6e2 | 2018-08-21 15:58:55 +0100 | [diff] [blame] | 173 | @UnsupportedAppUsage |
Amith Yamasani | 068d73c | 2011-05-27 15:15:14 -0700 | [diff] [blame] | 174 | private CharSequence mUserQuery; |
Mathew Inwood | 978c6e2 | 2018-08-21 15:58:55 +0100 | [diff] [blame] | 175 | @UnsupportedAppUsage |
Amith Yamasani | 763bc07 | 2011-07-22 11:53:47 -0700 | [diff] [blame] | 176 | private boolean mExpandedInActionView; |
Mathew Inwood | 978c6e2 | 2018-08-21 15:58:55 +0100 | [diff] [blame] | 177 | @UnsupportedAppUsage |
Adam Powell | 53f56c4 | 2011-09-25 13:46:15 -0700 | [diff] [blame] | 178 | private int mCollapsedImeOptions; |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 179 | |
| 180 | private SearchableInfo mSearchable; |
Amith Yamasani | 940ef38 | 2011-03-02 18:43:23 -0800 | [diff] [blame] | 181 | private Bundle mAppSearchData; |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 182 | |
Amith Yamasani | a95e488 | 2011-08-17 11:41:37 -0700 | [diff] [blame] | 183 | private Runnable mUpdateDrawableStateRunnable = new Runnable() { |
| 184 | public void run() { |
| 185 | updateFocusedState(); |
| 186 | } |
| 187 | }; |
| 188 | |
Amith Yamasani | 8790764 | 2011-11-03 11:32:44 -0700 | [diff] [blame] | 189 | private Runnable mReleaseCursorRunnable = new Runnable() { |
| 190 | public void run() { |
| 191 | if (mSuggestionsAdapter != null && mSuggestionsAdapter instanceof SuggestionsAdapter) { |
| 192 | mSuggestionsAdapter.changeCursor(null); |
| 193 | } |
| 194 | } |
| 195 | }; |
| 196 | |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 197 | // A weak map of drawables we've gotten from other packages, so we don't load them |
| 198 | // more than once. |
| 199 | private final WeakHashMap<String, Drawable.ConstantState> mOutsideDrawablesCache = |
| 200 | new WeakHashMap<String, Drawable.ConstantState>(); |
| 201 | |
| 202 | /** |
| 203 | * Callbacks for changes to the query text. |
| 204 | */ |
Adam Powell | 01f2135 | 2011-01-20 18:30:10 -0800 | [diff] [blame] | 205 | public interface OnQueryTextListener { |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 206 | |
| 207 | /** |
| 208 | * Called when the user submits the query. This could be due to a key press on the |
| 209 | * keyboard or due to pressing a submit button. |
| 210 | * The listener can override the standard behavior by returning true |
| 211 | * to indicate that it has handled the submit request. Otherwise return false to |
| 212 | * let the SearchView handle the submission by launching any associated intent. |
| 213 | * |
| 214 | * @param query the query text that is to be submitted |
| 215 | * |
| 216 | * @return true if the query has been handled by the listener, false to let the |
| 217 | * SearchView perform the default action. |
| 218 | */ |
Adam Powell | 01f2135 | 2011-01-20 18:30:10 -0800 | [diff] [blame] | 219 | boolean onQueryTextSubmit(String query); |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 220 | |
| 221 | /** |
| 222 | * Called when the query text is changed by the user. |
| 223 | * |
| 224 | * @param newText the new content of the query text field. |
| 225 | * |
| 226 | * @return false if the SearchView should perform the default action of showing any |
| 227 | * suggestions if available, true if the action was handled by the listener. |
| 228 | */ |
Adam Powell | 01f2135 | 2011-01-20 18:30:10 -0800 | [diff] [blame] | 229 | boolean onQueryTextChange(String newText); |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 230 | } |
| 231 | |
| 232 | public interface OnCloseListener { |
| 233 | |
| 234 | /** |
| 235 | * The user is attempting to close the SearchView. |
| 236 | * |
| 237 | * @return true if the listener wants to override the default behavior of clearing the |
| 238 | * text field and dismissing it, false otherwise. |
| 239 | */ |
| 240 | boolean onClose(); |
| 241 | } |
| 242 | |
Amith Yamasani | 0594476 | 2010-10-08 13:52:38 -0700 | [diff] [blame] | 243 | /** |
| 244 | * Callback interface for selection events on suggestions. These callbacks |
| 245 | * are only relevant when a SearchableInfo has been specified by {@link #setSearchableInfo}. |
| 246 | */ |
Adam Powell | 01f2135 | 2011-01-20 18:30:10 -0800 | [diff] [blame] | 247 | public interface OnSuggestionListener { |
Amith Yamasani | 0594476 | 2010-10-08 13:52:38 -0700 | [diff] [blame] | 248 | |
| 249 | /** |
| 250 | * Called when a suggestion was selected by navigating to it. |
| 251 | * @param position the absolute position in the list of suggestions. |
| 252 | * |
| 253 | * @return true if the listener handles the event and wants to override the default |
| 254 | * behavior of possibly rewriting the query based on the selected item, false otherwise. |
| 255 | */ |
Adam Powell | 01f2135 | 2011-01-20 18:30:10 -0800 | [diff] [blame] | 256 | boolean onSuggestionSelect(int position); |
Amith Yamasani | 0594476 | 2010-10-08 13:52:38 -0700 | [diff] [blame] | 257 | |
| 258 | /** |
| 259 | * Called when a suggestion was clicked. |
| 260 | * @param position the absolute position of the clicked item in the list of suggestions. |
| 261 | * |
| 262 | * @return true if the listener handles the event and wants to override the default |
| 263 | * behavior of launching any intent or submitting a search query specified on that item. |
| 264 | * Return false otherwise. |
| 265 | */ |
Adam Powell | 01f2135 | 2011-01-20 18:30:10 -0800 | [diff] [blame] | 266 | boolean onSuggestionClick(int position); |
Amith Yamasani | 0594476 | 2010-10-08 13:52:38 -0700 | [diff] [blame] | 267 | } |
| 268 | |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 269 | public SearchView(Context context) { |
| 270 | this(context, null); |
| 271 | } |
| 272 | |
| 273 | public SearchView(Context context, AttributeSet attrs) { |
Alan Viverette | 5dddb70 | 2014-07-02 15:46:04 -0700 | [diff] [blame] | 274 | this(context, attrs, R.attr.searchViewStyle); |
Alan Viverette | 617feb9 | 2013-09-09 18:09:13 -0700 | [diff] [blame] | 275 | } |
| 276 | |
| 277 | public SearchView(Context context, AttributeSet attrs, int defStyleAttr) { |
| 278 | this(context, attrs, defStyleAttr, 0); |
| 279 | } |
| 280 | |
| 281 | public SearchView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { |
| 282 | super(context, attrs, defStyleAttr, defStyleRes); |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 283 | |
Alan Viverette | 5dddb70 | 2014-07-02 15:46:04 -0700 | [diff] [blame] | 284 | final TypedArray a = context.obtainStyledAttributes( |
| 285 | attrs, R.styleable.SearchView, defStyleAttr, defStyleRes); |
Aurimas Liutikas | ab324cf | 2019-02-07 16:46:38 -0800 | [diff] [blame] | 286 | saveAttributeDataForStyleable(context, R.styleable.SearchView, |
| 287 | attrs, a, defStyleAttr, defStyleRes); |
Alan Viverette | 5dddb70 | 2014-07-02 15:46:04 -0700 | [diff] [blame] | 288 | final LayoutInflater inflater = (LayoutInflater) context.getSystemService( |
| 289 | Context.LAYOUT_INFLATER_SERVICE); |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 290 | final int layoutResId = a.getResourceId( |
| 291 | R.styleable.SearchView_layout, R.layout.search_view); |
Alan Viverette | 5dddb70 | 2014-07-02 15:46:04 -0700 | [diff] [blame] | 292 | inflater.inflate(layoutResId, this, true); |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 293 | |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 294 | mSearchSrcTextView = (SearchAutoComplete) findViewById(R.id.search_src_text); |
| 295 | mSearchSrcTextView.setSearchView(this); |
Amith Yamasani | 968ec93 | 2010-12-02 14:00:47 -0800 | [diff] [blame] | 296 | |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 297 | mSearchEditFrame = findViewById(R.id.search_edit_frame); |
Amith Yamasani | 79f7430 | 2011-03-08 14:16:35 -0800 | [diff] [blame] | 298 | mSearchPlate = findViewById(R.id.search_plate); |
Amith Yamasani | 9b2e302 | 2011-01-14 11:34:12 -0800 | [diff] [blame] | 299 | mSubmitArea = findViewById(R.id.submit_area); |
Alan Viverette | 5dddb70 | 2014-07-02 15:46:04 -0700 | [diff] [blame] | 300 | mSearchButton = (ImageView) findViewById(R.id.search_button); |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 301 | mGoButton = (ImageView) findViewById(R.id.search_go_btn); |
Amith Yamasani | 4aedb39 | 2010-12-15 16:04:57 -0800 | [diff] [blame] | 302 | mCloseButton = (ImageView) findViewById(R.id.search_close_btn); |
Alan Viverette | 5dddb70 | 2014-07-02 15:46:04 -0700 | [diff] [blame] | 303 | mVoiceButton = (ImageView) findViewById(R.id.search_voice_btn); |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 304 | mCollapsedIcon = (ImageView) findViewById(R.id.search_mag_icon); |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 305 | |
Alan Viverette | 5dddb70 | 2014-07-02 15:46:04 -0700 | [diff] [blame] | 306 | // Set up icons and backgrounds. |
| 307 | mSearchPlate.setBackground(a.getDrawable(R.styleable.SearchView_queryBackground)); |
| 308 | mSubmitArea.setBackground(a.getDrawable(R.styleable.SearchView_submitBackground)); |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 309 | mSearchButton.setImageDrawable(a.getDrawable(R.styleable.SearchView_searchIcon)); |
| 310 | mGoButton.setImageDrawable(a.getDrawable(R.styleable.SearchView_goIcon)); |
Alan Viverette | 5dddb70 | 2014-07-02 15:46:04 -0700 | [diff] [blame] | 311 | mCloseButton.setImageDrawable(a.getDrawable(R.styleable.SearchView_closeIcon)); |
| 312 | mVoiceButton.setImageDrawable(a.getDrawable(R.styleable.SearchView_voiceIcon)); |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 313 | mCollapsedIcon.setImageDrawable(a.getDrawable(R.styleable.SearchView_searchIcon)); |
| 314 | |
| 315 | // Prior to L MR1, the search hint icon defaulted to searchIcon. If the |
| 316 | // style does not have an explicit value set, fall back to that. |
| 317 | if (a.hasValueOrEmpty(R.styleable.SearchView_searchHintIcon)) { |
| 318 | mSearchHintIcon = a.getDrawable(R.styleable.SearchView_searchHintIcon); |
| 319 | } else { |
| 320 | mSearchHintIcon = a.getDrawable(R.styleable.SearchView_searchIcon); |
| 321 | } |
Alan Viverette | 5dddb70 | 2014-07-02 15:46:04 -0700 | [diff] [blame] | 322 | |
| 323 | // Extract dropdown layout resource IDs for later use. |
Alan Viverette | 362f984 | 2014-09-10 16:05:35 -0700 | [diff] [blame] | 324 | mSuggestionRowLayout = a.getResourceId(R.styleable.SearchView_suggestionRowLayout, |
| 325 | R.layout.search_dropdown_item_icons_2line); |
Alan Viverette | 5dddb70 | 2014-07-02 15:46:04 -0700 | [diff] [blame] | 326 | mSuggestionCommitIconResId = a.getResourceId(R.styleable.SearchView_commitIcon, 0); |
| 327 | |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 328 | mSearchButton.setOnClickListener(mOnClickListener); |
| 329 | mCloseButton.setOnClickListener(mOnClickListener); |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 330 | mGoButton.setOnClickListener(mOnClickListener); |
Amith Yamasani | ebcf5a3a | 2010-10-13 11:35:24 -0700 | [diff] [blame] | 331 | mVoiceButton.setOnClickListener(mOnClickListener); |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 332 | mSearchSrcTextView.setOnClickListener(mOnClickListener); |
Amith Yamasani | ebcf5a3a | 2010-10-13 11:35:24 -0700 | [diff] [blame] | 333 | |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 334 | mSearchSrcTextView.addTextChangedListener(mTextWatcher); |
| 335 | mSearchSrcTextView.setOnEditorActionListener(mOnEditorActionListener); |
| 336 | mSearchSrcTextView.setOnItemClickListener(mOnItemClickListener); |
| 337 | mSearchSrcTextView.setOnItemSelectedListener(mOnItemSelectedListener); |
| 338 | mSearchSrcTextView.setOnKeyListener(mTextKeyListener); |
Alan Viverette | 5dddb70 | 2014-07-02 15:46:04 -0700 | [diff] [blame] | 339 | |
Luca Zanolin | 535698c | 2011-10-06 13:36:15 +0100 | [diff] [blame] | 340 | // Inform any listener of focus changes |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 341 | mSearchSrcTextView.setOnFocusChangeListener(new OnFocusChangeListener() { |
Amith Yamasani | 0594476 | 2010-10-08 13:52:38 -0700 | [diff] [blame] | 342 | |
| 343 | public void onFocusChange(View v, boolean hasFocus) { |
| 344 | if (mOnQueryTextFocusChangeListener != null) { |
| 345 | mOnQueryTextFocusChangeListener.onFocusChange(SearchView.this, hasFocus); |
| 346 | } |
| 347 | } |
| 348 | }); |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 349 | setIconifiedByDefault(a.getBoolean(R.styleable.SearchView_iconifiedByDefault, true)); |
Alan Viverette | 5dddb70 | 2014-07-02 15:46:04 -0700 | [diff] [blame] | 350 | |
| 351 | final int maxWidth = a.getDimensionPixelSize(R.styleable.SearchView_maxWidth, -1); |
Amith Yamasani | 5931b1f | 2010-10-18 16:13:14 -0700 | [diff] [blame] | 352 | if (maxWidth != -1) { |
| 353 | setMaxWidth(maxWidth); |
| 354 | } |
Alan Viverette | 5dddb70 | 2014-07-02 15:46:04 -0700 | [diff] [blame] | 355 | |
Alan Viverette | b4004df | 2015-04-29 16:55:42 -0700 | [diff] [blame] | 356 | mDefaultQueryHint = a.getText(R.styleable.SearchView_defaultQueryHint); |
| 357 | mQueryHint = a.getText(R.styleable.SearchView_queryHint); |
Alan Viverette | 5dddb70 | 2014-07-02 15:46:04 -0700 | [diff] [blame] | 358 | |
| 359 | final int imeOptions = a.getInt(R.styleable.SearchView_imeOptions, -1); |
Amith Yamasani | 5607a38 | 2011-08-09 14:16:37 -0700 | [diff] [blame] | 360 | if (imeOptions != -1) { |
| 361 | setImeOptions(imeOptions); |
| 362 | } |
Alan Viverette | 5dddb70 | 2014-07-02 15:46:04 -0700 | [diff] [blame] | 363 | |
| 364 | final int inputType = a.getInt(R.styleable.SearchView_inputType, -1); |
Amith Yamasani | 5607a38 | 2011-08-09 14:16:37 -0700 | [diff] [blame] | 365 | if (inputType != -1) { |
| 366 | setInputType(inputType); |
| 367 | } |
| 368 | |
Evan Rosky | 4c8c963 | 2016-12-16 17:27:55 -0800 | [diff] [blame] | 369 | if (getFocusable() == FOCUSABLE_AUTO) { |
| 370 | setFocusable(FOCUSABLE); |
| 371 | } |
Adam Powell | ea4ecd6 | 2014-09-03 19:35:37 -0700 | [diff] [blame] | 372 | |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 373 | a.recycle(); |
| 374 | |
Amith Yamasani | ebcf5a3a | 2010-10-13 11:35:24 -0700 | [diff] [blame] | 375 | // Save voice intent for later queries/launching |
| 376 | mVoiceWebSearchIntent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH); |
| 377 | mVoiceWebSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); |
| 378 | mVoiceWebSearchIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, |
| 379 | RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH); |
| 380 | |
| 381 | mVoiceAppSearchIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); |
| 382 | mVoiceAppSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); |
| 383 | |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 384 | mDropDownAnchor = findViewById(mSearchSrcTextView.getDropDownAnchor()); |
Amith Yamasani | b4569fb | 2011-07-08 15:25:39 -0700 | [diff] [blame] | 385 | if (mDropDownAnchor != null) { |
| 386 | mDropDownAnchor.addOnLayoutChangeListener(new OnLayoutChangeListener() { |
| 387 | @Override |
| 388 | public void onLayoutChange(View v, int left, int top, int right, int bottom, |
| 389 | int oldLeft, int oldTop, int oldRight, int oldBottom) { |
| 390 | adjustDropDownSizeAndPosition(); |
| 391 | } |
Amith Yamasani | b4569fb | 2011-07-08 15:25:39 -0700 | [diff] [blame] | 392 | }); |
| 393 | } |
| 394 | |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 395 | updateViewsVisibility(mIconifiedByDefault); |
Amith Yamasani | b4569fb | 2011-07-08 15:25:39 -0700 | [diff] [blame] | 396 | updateQueryHint(); |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 397 | } |
| 398 | |
Alan Viverette | 5dddb70 | 2014-07-02 15:46:04 -0700 | [diff] [blame] | 399 | int getSuggestionRowLayout() { |
| 400 | return mSuggestionRowLayout; |
| 401 | } |
| 402 | |
| 403 | int getSuggestionCommitIconResId() { |
| 404 | return mSuggestionCommitIconResId; |
| 405 | } |
| 406 | |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 407 | /** |
| 408 | * Sets the SearchableInfo for this SearchView. Properties in the SearchableInfo are used |
| 409 | * to display labels, hints, suggestions, create intents for launching search results screens |
| 410 | * and controlling other affordances such as a voice button. |
| 411 | * |
| 412 | * @param searchable a SearchableInfo can be retrieved from the SearchManager, for a specific |
| 413 | * activity or a global search provider. |
| 414 | */ |
| 415 | public void setSearchableInfo(SearchableInfo searchable) { |
| 416 | mSearchable = searchable; |
| 417 | if (mSearchable != null) { |
| 418 | updateSearchAutoComplete(); |
Amith Yamasani | 79f7430 | 2011-03-08 14:16:35 -0800 | [diff] [blame] | 419 | updateQueryHint(); |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 420 | } |
Amith Yamasani | 9b2e302 | 2011-01-14 11:34:12 -0800 | [diff] [blame] | 421 | // Cache the voice search capability |
| 422 | mVoiceButtonEnabled = hasVoiceSearch(); |
Luca Zanolin | 535698c | 2011-10-06 13:36:15 +0100 | [diff] [blame] | 423 | |
| 424 | if (mVoiceButtonEnabled) { |
| 425 | // Disable the microphone on the keyboard, as a mic is displayed near the text box |
| 426 | // TODO: use imeOptions to disable voice input when the new API will be available |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 427 | mSearchSrcTextView.setPrivateImeOptions(IME_OPTION_NO_MICROPHONE); |
Luca Zanolin | 535698c | 2011-10-06 13:36:15 +0100 | [diff] [blame] | 428 | } |
Amith Yamasani | b4569fb | 2011-07-08 15:25:39 -0700 | [diff] [blame] | 429 | updateViewsVisibility(isIconified()); |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 430 | } |
| 431 | |
Amith Yamasani | 940ef38 | 2011-03-02 18:43:23 -0800 | [diff] [blame] | 432 | /** |
| 433 | * Sets the APP_DATA for legacy SearchDialog use. |
| 434 | * @param appSearchData bundle provided by the app when launching the search dialog |
| 435 | * @hide |
| 436 | */ |
| 437 | public void setAppSearchData(Bundle appSearchData) { |
| 438 | mAppSearchData = appSearchData; |
| 439 | } |
| 440 | |
Amith Yamasani | 5607a38 | 2011-08-09 14:16:37 -0700 | [diff] [blame] | 441 | /** |
| 442 | * Sets the IME options on the query text field. |
| 443 | * |
| 444 | * @see TextView#setImeOptions(int) |
| 445 | * @param imeOptions the options to set on the query text field |
| 446 | * |
| 447 | * @attr ref android.R.styleable#SearchView_imeOptions |
| 448 | */ |
| 449 | public void setImeOptions(int imeOptions) { |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 450 | mSearchSrcTextView.setImeOptions(imeOptions); |
Amith Yamasani | 5607a38 | 2011-08-09 14:16:37 -0700 | [diff] [blame] | 451 | } |
| 452 | |
| 453 | /** |
Amith Yamasani | eca59d3 | 2012-04-25 18:57:18 -0700 | [diff] [blame] | 454 | * Returns the IME options set on the query text field. |
| 455 | * @return the ime options |
| 456 | * @see TextView#setImeOptions(int) |
| 457 | * |
| 458 | * @attr ref android.R.styleable#SearchView_imeOptions |
| 459 | */ |
| 460 | public int getImeOptions() { |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 461 | return mSearchSrcTextView.getImeOptions(); |
Amith Yamasani | eca59d3 | 2012-04-25 18:57:18 -0700 | [diff] [blame] | 462 | } |
| 463 | |
| 464 | /** |
Amith Yamasani | 5607a38 | 2011-08-09 14:16:37 -0700 | [diff] [blame] | 465 | * Sets the input type on the query text field. |
| 466 | * |
| 467 | * @see TextView#setInputType(int) |
| 468 | * @param inputType the input type to set on the query text field |
| 469 | * |
| 470 | * @attr ref android.R.styleable#SearchView_inputType |
| 471 | */ |
| 472 | public void setInputType(int inputType) { |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 473 | mSearchSrcTextView.setInputType(inputType); |
Amith Yamasani | 5607a38 | 2011-08-09 14:16:37 -0700 | [diff] [blame] | 474 | } |
| 475 | |
Amith Yamasani | eca59d3 | 2012-04-25 18:57:18 -0700 | [diff] [blame] | 476 | /** |
| 477 | * Returns the input type set on the query text field. |
| 478 | * @return the input type |
| 479 | * |
| 480 | * @attr ref android.R.styleable#SearchView_inputType |
| 481 | */ |
| 482 | public int getInputType() { |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 483 | return mSearchSrcTextView.getInputType(); |
Amith Yamasani | eca59d3 | 2012-04-25 18:57:18 -0700 | [diff] [blame] | 484 | } |
| 485 | |
Amith Yamasani | 0594476 | 2010-10-08 13:52:38 -0700 | [diff] [blame] | 486 | /** @hide */ |
| 487 | @Override |
| 488 | public boolean requestFocus(int direction, Rect previouslyFocusedRect) { |
Amith Yamasani | 7f8aef6 | 2011-01-25 11:58:09 -0800 | [diff] [blame] | 489 | // Don't accept focus if in the middle of clearing focus |
| 490 | if (mClearingFocus) return false; |
| 491 | // Check if SearchView is focusable. |
| 492 | if (!isFocusable()) return false; |
| 493 | // If it is not iconified, then give the focus to the text field |
| 494 | if (!isIconified()) { |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 495 | boolean result = mSearchSrcTextView.requestFocus(direction, previouslyFocusedRect); |
Amith Yamasani | f28d187 | 2011-07-26 12:21:03 -0700 | [diff] [blame] | 496 | if (result) { |
| 497 | updateViewsVisibility(false); |
| 498 | } |
Amith Yamasani | 7f8aef6 | 2011-01-25 11:58:09 -0800 | [diff] [blame] | 499 | return result; |
| 500 | } else { |
| 501 | return super.requestFocus(direction, previouslyFocusedRect); |
| 502 | } |
Amith Yamasani | 0594476 | 2010-10-08 13:52:38 -0700 | [diff] [blame] | 503 | } |
| 504 | |
| 505 | /** @hide */ |
| 506 | @Override |
| 507 | public void clearFocus() { |
| 508 | mClearingFocus = true; |
| 509 | super.clearFocus(); |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 510 | mSearchSrcTextView.clearFocus(); |
Yohei Yukawa | 5cfc1b4 | 2017-03-07 00:52:09 -0800 | [diff] [blame] | 511 | mSearchSrcTextView.setImeVisibility(false); |
Amith Yamasani | 0594476 | 2010-10-08 13:52:38 -0700 | [diff] [blame] | 512 | mClearingFocus = false; |
| 513 | } |
| 514 | |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 515 | /** |
| 516 | * Sets a listener for user actions within the SearchView. |
| 517 | * |
| 518 | * @param listener the listener object that receives callbacks when the user performs |
| 519 | * actions in the SearchView such as clicking on buttons or typing a query. |
| 520 | */ |
Adam Powell | 01f2135 | 2011-01-20 18:30:10 -0800 | [diff] [blame] | 521 | public void setOnQueryTextListener(OnQueryTextListener listener) { |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 522 | mOnQueryChangeListener = listener; |
| 523 | } |
| 524 | |
| 525 | /** |
Amith Yamasani | 9322775 | 2010-09-14 10:10:54 -0700 | [diff] [blame] | 526 | * Sets a listener to inform when the user closes the SearchView. |
| 527 | * |
| 528 | * @param listener the listener to call when the user closes the SearchView. |
| 529 | */ |
| 530 | public void setOnCloseListener(OnCloseListener listener) { |
| 531 | mOnCloseListener = listener; |
| 532 | } |
| 533 | |
| 534 | /** |
Amith Yamasani | 0594476 | 2010-10-08 13:52:38 -0700 | [diff] [blame] | 535 | * Sets a listener to inform when the focus of the query text field changes. |
| 536 | * |
| 537 | * @param listener the listener to inform of focus changes. |
| 538 | */ |
| 539 | public void setOnQueryTextFocusChangeListener(OnFocusChangeListener listener) { |
| 540 | mOnQueryTextFocusChangeListener = listener; |
| 541 | } |
| 542 | |
| 543 | /** |
| 544 | * Sets a listener to inform when a suggestion is focused or clicked. |
| 545 | * |
| 546 | * @param listener the listener to inform of suggestion selection events. |
| 547 | */ |
Adam Powell | 01f2135 | 2011-01-20 18:30:10 -0800 | [diff] [blame] | 548 | public void setOnSuggestionListener(OnSuggestionListener listener) { |
Amith Yamasani | 0594476 | 2010-10-08 13:52:38 -0700 | [diff] [blame] | 549 | mOnSuggestionListener = listener; |
| 550 | } |
| 551 | |
| 552 | /** |
Amith Yamasani | 4838548 | 2010-12-03 14:43:52 -0800 | [diff] [blame] | 553 | * Sets a listener to inform when the search button is pressed. This is only |
Scott Main | cccdbe9 | 2011-02-06 15:51:47 -0800 | [diff] [blame] | 554 | * relevant when the text field is not visible by default. Calling {@link #setIconified |
| 555 | * setIconified(false)} can also cause this listener to be informed. |
Amith Yamasani | 4838548 | 2010-12-03 14:43:52 -0800 | [diff] [blame] | 556 | * |
| 557 | * @param listener the listener to inform when the search button is clicked or |
| 558 | * the text field is programmatically de-iconified. |
| 559 | */ |
| 560 | public void setOnSearchClickListener(OnClickListener listener) { |
| 561 | mOnSearchClickListener = listener; |
| 562 | } |
| 563 | |
| 564 | /** |
| 565 | * Returns the query string currently in the text field. |
| 566 | * |
| 567 | * @return the query string |
| 568 | */ |
Ashley Rose | 55f9f92 | 2019-01-28 19:29:36 -0500 | [diff] [blame] | 569 | @InspectableProperty(hasAttributeId = false) |
Amith Yamasani | 4838548 | 2010-12-03 14:43:52 -0800 | [diff] [blame] | 570 | public CharSequence getQuery() { |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 571 | return mSearchSrcTextView.getText(); |
Amith Yamasani | 4838548 | 2010-12-03 14:43:52 -0800 | [diff] [blame] | 572 | } |
| 573 | |
| 574 | /** |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 575 | * Sets a query string in the text field and optionally submits the query as well. |
| 576 | * |
| 577 | * @param query the query string. This replaces any query text already present in the |
| 578 | * text field. |
| 579 | * @param submit whether to submit the query right now or only update the contents of |
| 580 | * text field. |
| 581 | */ |
| 582 | public void setQuery(CharSequence query, boolean submit) { |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 583 | mSearchSrcTextView.setText(query); |
Dmitri Plotnikov | 87c5025 | 2010-10-21 21:16:42 -0700 | [diff] [blame] | 584 | if (query != null) { |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 585 | mSearchSrcTextView.setSelection(mSearchSrcTextView.length()); |
Amith Yamasani | 068d73c | 2011-05-27 15:15:14 -0700 | [diff] [blame] | 586 | mUserQuery = query; |
Dmitri Plotnikov | 87c5025 | 2010-10-21 21:16:42 -0700 | [diff] [blame] | 587 | } |
| 588 | |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 589 | // If the query is not empty and submit is requested, submit the query |
| 590 | if (submit && !TextUtils.isEmpty(query)) { |
| 591 | onSubmitQuery(); |
| 592 | } |
| 593 | } |
| 594 | |
| 595 | /** |
Alan Viverette | b4004df | 2015-04-29 16:55:42 -0700 | [diff] [blame] | 596 | * Sets the hint text to display in the query text field. This overrides |
| 597 | * any hint specified in the {@link SearchableInfo}. |
| 598 | * <p> |
| 599 | * This value may be specified as an empty string to prevent any query hint |
| 600 | * from being displayed. |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 601 | * |
Alan Viverette | b4004df | 2015-04-29 16:55:42 -0700 | [diff] [blame] | 602 | * @param hint the hint text to display or {@code null} to clear |
Scott Main | abdf0d5 | 2011-02-08 10:20:27 -0800 | [diff] [blame] | 603 | * @attr ref android.R.styleable#SearchView_queryHint |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 604 | */ |
Alan Viverette | b4004df | 2015-04-29 16:55:42 -0700 | [diff] [blame] | 605 | public void setQueryHint(@Nullable CharSequence hint) { |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 606 | mQueryHint = hint; |
| 607 | updateQueryHint(); |
| 608 | } |
| 609 | |
| 610 | /** |
Alan Viverette | b4004df | 2015-04-29 16:55:42 -0700 | [diff] [blame] | 611 | * Returns the hint text that will be displayed in the query text field. |
| 612 | * <p> |
| 613 | * The displayed query hint is chosen in the following order: |
| 614 | * <ol> |
| 615 | * <li>Non-null value set with {@link #setQueryHint(CharSequence)} |
| 616 | * <li>Value specified in XML using |
| 617 | * {@link android.R.styleable#SearchView_queryHint android:queryHint} |
| 618 | * <li>Valid string resource ID exposed by the {@link SearchableInfo} via |
| 619 | * {@link SearchableInfo#getHintId()} |
| 620 | * <li>Default hint provided by the theme against which the view was |
| 621 | * inflated |
| 622 | * </ol> |
Amith Yamasani | eca59d3 | 2012-04-25 18:57:18 -0700 | [diff] [blame] | 623 | * |
Alan Viverette | b4004df | 2015-04-29 16:55:42 -0700 | [diff] [blame] | 624 | * @return the displayed query hint text, or {@code null} if none set |
Amith Yamasani | eca59d3 | 2012-04-25 18:57:18 -0700 | [diff] [blame] | 625 | * @attr ref android.R.styleable#SearchView_queryHint |
| 626 | */ |
Ashley Rose | 55f9f92 | 2019-01-28 19:29:36 -0500 | [diff] [blame] | 627 | @InspectableProperty |
Alan Viverette | b4004df | 2015-04-29 16:55:42 -0700 | [diff] [blame] | 628 | @Nullable |
Amith Yamasani | eca59d3 | 2012-04-25 18:57:18 -0700 | [diff] [blame] | 629 | public CharSequence getQueryHint() { |
Alan Viverette | b4004df | 2015-04-29 16:55:42 -0700 | [diff] [blame] | 630 | final CharSequence hint; |
Amith Yamasani | eca59d3 | 2012-04-25 18:57:18 -0700 | [diff] [blame] | 631 | if (mQueryHint != null) { |
Alan Viverette | b4004df | 2015-04-29 16:55:42 -0700 | [diff] [blame] | 632 | hint = mQueryHint; |
| 633 | } else if (mSearchable != null && mSearchable.getHintId() != 0) { |
| 634 | hint = getContext().getText(mSearchable.getHintId()); |
| 635 | } else { |
| 636 | hint = mDefaultQueryHint; |
Amith Yamasani | eca59d3 | 2012-04-25 18:57:18 -0700 | [diff] [blame] | 637 | } |
Alan Viverette | b4004df | 2015-04-29 16:55:42 -0700 | [diff] [blame] | 638 | return hint; |
Amith Yamasani | eca59d3 | 2012-04-25 18:57:18 -0700 | [diff] [blame] | 639 | } |
| 640 | |
| 641 | /** |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 642 | * Sets the default or resting state of the search field. If true, a single search icon is |
| 643 | * shown by default and expands to show the text field and other buttons when pressed. Also, |
| 644 | * if the default state is iconified, then it collapses to that state when the close button |
Amith Yamasani | 9322775 | 2010-09-14 10:10:54 -0700 | [diff] [blame] | 645 | * is pressed. Changes to this property will take effect immediately. |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 646 | * |
Scott Main | cccdbe9 | 2011-02-06 15:51:47 -0800 | [diff] [blame] | 647 | * <p>The default value is true.</p> |
Amith Yamasani | 9322775 | 2010-09-14 10:10:54 -0700 | [diff] [blame] | 648 | * |
| 649 | * @param iconified whether the search field should be iconified by default |
Scott Main | abdf0d5 | 2011-02-08 10:20:27 -0800 | [diff] [blame] | 650 | * |
| 651 | * @attr ref android.R.styleable#SearchView_iconifiedByDefault |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 652 | */ |
| 653 | public void setIconifiedByDefault(boolean iconified) { |
Amith Yamasani | ebcf5a3a | 2010-10-13 11:35:24 -0700 | [diff] [blame] | 654 | if (mIconifiedByDefault == iconified) return; |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 655 | mIconifiedByDefault = iconified; |
| 656 | updateViewsVisibility(iconified); |
Amith Yamasani | b47c4fd | 2011-08-04 14:30:07 -0700 | [diff] [blame] | 657 | updateQueryHint(); |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 658 | } |
| 659 | |
Amith Yamasani | 9322775 | 2010-09-14 10:10:54 -0700 | [diff] [blame] | 660 | /** |
| 661 | * Returns the default iconified state of the search field. |
| 662 | * @return |
Amith Yamasani | 3d3e7a5 | 2012-05-04 14:46:31 -0700 | [diff] [blame] | 663 | * |
Ashley Rose | 55f9f92 | 2019-01-28 19:29:36 -0500 | [diff] [blame] | 664 | * @deprecated use {@link #isIconifiedByDefault()} |
Amith Yamasani | 3d3e7a5 | 2012-05-04 14:46:31 -0700 | [diff] [blame] | 665 | * @attr ref android.R.styleable#SearchView_iconifiedByDefault |
Amith Yamasani | 9322775 | 2010-09-14 10:10:54 -0700 | [diff] [blame] | 666 | */ |
Ashley Rose | 55f9f92 | 2019-01-28 19:29:36 -0500 | [diff] [blame] | 667 | @Deprecated |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 668 | public boolean isIconfiedByDefault() { |
| 669 | return mIconifiedByDefault; |
| 670 | } |
| 671 | |
| 672 | /** |
Ashley Rose | 55f9f92 | 2019-01-28 19:29:36 -0500 | [diff] [blame] | 673 | * Returns the default iconified state of the search field. |
| 674 | * |
| 675 | * @attr ref android.R.styleable#SearchView_iconifiedByDefault |
| 676 | */ |
| 677 | @InspectableProperty |
| 678 | public boolean isIconifiedByDefault() { |
| 679 | return mIconifiedByDefault; |
| 680 | } |
| 681 | |
| 682 | /** |
Amith Yamasani | 9322775 | 2010-09-14 10:10:54 -0700 | [diff] [blame] | 683 | * Iconifies or expands the SearchView. Any query text is cleared when iconified. This is |
| 684 | * a temporary state and does not override the default iconified state set by |
| 685 | * {@link #setIconifiedByDefault(boolean)}. If the default state is iconified, then |
| 686 | * a false here will only be valid until the user closes the field. And if the default |
| 687 | * state is expanded, then a true here will only clear the text field and not close it. |
| 688 | * |
| 689 | * @param iconify a true value will collapse the SearchView to an icon, while a false will |
| 690 | * expand it. |
| 691 | */ |
| 692 | public void setIconified(boolean iconify) { |
| 693 | if (iconify) { |
| 694 | onCloseClicked(); |
| 695 | } else { |
| 696 | onSearchClicked(); |
| 697 | } |
| 698 | } |
| 699 | |
| 700 | /** |
| 701 | * Returns the current iconified state of the SearchView. |
| 702 | * |
| 703 | * @return true if the SearchView is currently iconified, false if the search field is |
| 704 | * fully visible. |
| 705 | */ |
Ashley Rose | 55f9f92 | 2019-01-28 19:29:36 -0500 | [diff] [blame] | 706 | @InspectableProperty(hasAttributeId = false) |
Amith Yamasani | 9322775 | 2010-09-14 10:10:54 -0700 | [diff] [blame] | 707 | public boolean isIconified() { |
| 708 | return mIconified; |
| 709 | } |
| 710 | |
| 711 | /** |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 712 | * Enables showing a submit button when the query is non-empty. In cases where the SearchView |
| 713 | * is being used to filter the contents of the current activity and doesn't launch a separate |
| 714 | * results activity, then the submit button should be disabled. |
| 715 | * |
| 716 | * @param enabled true to show a submit button for submitting queries, false if a submit |
| 717 | * button is not required. |
| 718 | */ |
| 719 | public void setSubmitButtonEnabled(boolean enabled) { |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 720 | mSubmitButtonEnabled = enabled; |
Amith Yamasani | ebcf5a3a | 2010-10-13 11:35:24 -0700 | [diff] [blame] | 721 | updateViewsVisibility(isIconified()); |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 722 | } |
| 723 | |
| 724 | /** |
| 725 | * Returns whether the submit button is enabled when necessary or never displayed. |
| 726 | * |
| 727 | * @return whether the submit button is enabled automatically when necessary |
| 728 | */ |
| 729 | public boolean isSubmitButtonEnabled() { |
| 730 | return mSubmitButtonEnabled; |
| 731 | } |
| 732 | |
Amith Yamasani | e678f46 | 2010-09-15 16:13:43 -0700 | [diff] [blame] | 733 | /** |
| 734 | * Specifies if a query refinement button should be displayed alongside each suggestion |
| 735 | * or if it should depend on the flags set in the individual items retrieved from the |
| 736 | * suggestions provider. Clicking on the query refinement button will replace the text |
| 737 | * in the query text field with the text from the suggestion. This flag only takes effect |
| 738 | * if a SearchableInfo has been specified with {@link #setSearchableInfo(SearchableInfo)} |
| 739 | * and not when using a custom adapter. |
| 740 | * |
| 741 | * @param enable true if all items should have a query refinement button, false if only |
| 742 | * those items that have a query refinement flag set should have the button. |
| 743 | * |
| 744 | * @see SearchManager#SUGGEST_COLUMN_FLAGS |
| 745 | * @see SearchManager#FLAG_QUERY_REFINEMENT |
| 746 | */ |
| 747 | public void setQueryRefinementEnabled(boolean enable) { |
| 748 | mQueryRefinement = enable; |
| 749 | if (mSuggestionsAdapter instanceof SuggestionsAdapter) { |
| 750 | ((SuggestionsAdapter) mSuggestionsAdapter).setQueryRefinement( |
| 751 | enable ? SuggestionsAdapter.REFINE_ALL : SuggestionsAdapter.REFINE_BY_ENTRY); |
| 752 | } |
| 753 | } |
| 754 | |
| 755 | /** |
| 756 | * Returns whether query refinement is enabled for all items or only specific ones. |
| 757 | * @return true if enabled for all items, false otherwise. |
| 758 | */ |
| 759 | public boolean isQueryRefinementEnabled() { |
| 760 | return mQueryRefinement; |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 761 | } |
| 762 | |
| 763 | /** |
| 764 | * You can set a custom adapter if you wish. Otherwise the default adapter is used to |
| 765 | * display the suggestions from the suggestions provider associated with the SearchableInfo. |
| 766 | * |
| 767 | * @see #setSearchableInfo(SearchableInfo) |
| 768 | */ |
| 769 | public void setSuggestionsAdapter(CursorAdapter adapter) { |
| 770 | mSuggestionsAdapter = adapter; |
| 771 | |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 772 | mSearchSrcTextView.setAdapter(mSuggestionsAdapter); |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 773 | } |
| 774 | |
| 775 | /** |
| 776 | * Returns the adapter used for suggestions, if any. |
| 777 | * @return the suggestions adapter |
| 778 | */ |
| 779 | public CursorAdapter getSuggestionsAdapter() { |
| 780 | return mSuggestionsAdapter; |
| 781 | } |
| 782 | |
Amith Yamasani | 5931b1f | 2010-10-18 16:13:14 -0700 | [diff] [blame] | 783 | /** |
| 784 | * Makes the view at most this many pixels wide |
| 785 | * |
| 786 | * @attr ref android.R.styleable#SearchView_maxWidth |
| 787 | */ |
| 788 | public void setMaxWidth(int maxpixels) { |
| 789 | mMaxWidth = maxpixels; |
| 790 | |
| 791 | requestLayout(); |
| 792 | } |
| 793 | |
Amith Yamasani | eca59d3 | 2012-04-25 18:57:18 -0700 | [diff] [blame] | 794 | /** |
| 795 | * Gets the specified maximum width in pixels, if set. Returns zero if |
| 796 | * no maximum width was specified. |
| 797 | * @return the maximum width of the view |
Amith Yamasani | 3d3e7a5 | 2012-05-04 14:46:31 -0700 | [diff] [blame] | 798 | * |
| 799 | * @attr ref android.R.styleable#SearchView_maxWidth |
Amith Yamasani | eca59d3 | 2012-04-25 18:57:18 -0700 | [diff] [blame] | 800 | */ |
Ashley Rose | 55f9f92 | 2019-01-28 19:29:36 -0500 | [diff] [blame] | 801 | @InspectableProperty |
Amith Yamasani | eca59d3 | 2012-04-25 18:57:18 -0700 | [diff] [blame] | 802 | public int getMaxWidth() { |
| 803 | return mMaxWidth; |
| 804 | } |
| 805 | |
Amith Yamasani | 5931b1f | 2010-10-18 16:13:14 -0700 | [diff] [blame] | 806 | @Override |
| 807 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
Amith Yamasani | a95e488 | 2011-08-17 11:41:37 -0700 | [diff] [blame] | 808 | // Let the standard measurements take effect in iconified state. |
| 809 | if (isIconified()) { |
| 810 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); |
| 811 | return; |
| 812 | } |
| 813 | |
Amith Yamasani | 5931b1f | 2010-10-18 16:13:14 -0700 | [diff] [blame] | 814 | int widthMode = MeasureSpec.getMode(widthMeasureSpec); |
| 815 | int width = MeasureSpec.getSize(widthMeasureSpec); |
| 816 | |
Amith Yamasani | 167d69a | 2011-08-12 19:28:37 -0700 | [diff] [blame] | 817 | switch (widthMode) { |
| 818 | case MeasureSpec.AT_MOST: |
| 819 | // If there is an upper limit, don't exceed maximum width (explicit or implicit) |
| 820 | if (mMaxWidth > 0) { |
| 821 | width = Math.min(mMaxWidth, width); |
| 822 | } else { |
| 823 | width = Math.min(getPreferredWidth(), width); |
| 824 | } |
| 825 | break; |
| 826 | case MeasureSpec.EXACTLY: |
| 827 | // If an exact width is specified, still don't exceed any specified maximum width |
| 828 | if (mMaxWidth > 0) { |
| 829 | width = Math.min(mMaxWidth, width); |
| 830 | } |
| 831 | break; |
| 832 | case MeasureSpec.UNSPECIFIED: |
| 833 | // Use maximum width, if specified, else preferred width |
| 834 | width = mMaxWidth > 0 ? mMaxWidth : getPreferredWidth(); |
| 835 | break; |
Amith Yamasani | 5931b1f | 2010-10-18 16:13:14 -0700 | [diff] [blame] | 836 | } |
Amith Yamasani | 167d69a | 2011-08-12 19:28:37 -0700 | [diff] [blame] | 837 | widthMode = MeasureSpec.EXACTLY; |
Aurimas Liutikas | c8fd00a | 2016-04-15 13:55:53 -0700 | [diff] [blame] | 838 | |
| 839 | int heightMode = MeasureSpec.getMode(heightMeasureSpec); |
| 840 | int height = MeasureSpec.getSize(heightMeasureSpec); |
| 841 | |
| 842 | switch (heightMode) { |
| 843 | case MeasureSpec.AT_MOST: |
Aurimas Liutikas | c8fd00a | 2016-04-15 13:55:53 -0700 | [diff] [blame] | 844 | height = Math.min(getPreferredHeight(), height); |
| 845 | break; |
Aurimas Liutikas | f6a50be | 2016-09-09 15:32:55 -0700 | [diff] [blame] | 846 | case MeasureSpec.UNSPECIFIED: |
| 847 | height = getPreferredHeight(); |
| 848 | break; |
Aurimas Liutikas | c8fd00a | 2016-04-15 13:55:53 -0700 | [diff] [blame] | 849 | } |
| 850 | heightMode = MeasureSpec.EXACTLY; |
| 851 | |
| 852 | super.onMeasure(MeasureSpec.makeMeasureSpec(width, widthMode), |
| 853 | MeasureSpec.makeMeasureSpec(height, heightMode)); |
| 854 | } |
| 855 | |
| 856 | @Override |
| 857 | protected void onLayout(boolean changed, int left, int top, int right, int bottom) { |
| 858 | super.onLayout(changed, left, top, right, bottom); |
| 859 | |
| 860 | if (changed) { |
| 861 | // Expand mSearchSrcTextView touch target to be the height of the parent in order to |
| 862 | // allow it to be up to 48dp. |
| 863 | getChildBoundsWithinSearchView(mSearchSrcTextView, mSearchSrcTextViewBounds); |
| 864 | mSearchSrtTextViewBoundsExpanded.set( |
| 865 | mSearchSrcTextViewBounds.left, 0, mSearchSrcTextViewBounds.right, bottom - top); |
| 866 | if (mTouchDelegate == null) { |
| 867 | mTouchDelegate = new UpdatableTouchDelegate(mSearchSrtTextViewBoundsExpanded, |
| 868 | mSearchSrcTextViewBounds, mSearchSrcTextView); |
| 869 | setTouchDelegate(mTouchDelegate); |
| 870 | } else { |
| 871 | mTouchDelegate.setBounds(mSearchSrtTextViewBoundsExpanded, mSearchSrcTextViewBounds); |
| 872 | } |
| 873 | } |
| 874 | } |
| 875 | |
| 876 | private void getChildBoundsWithinSearchView(View view, Rect rect) { |
| 877 | view.getLocationInWindow(mTemp); |
| 878 | getLocationInWindow(mTemp2); |
| 879 | final int top = mTemp[1] - mTemp2[1]; |
| 880 | final int left = mTemp[0] - mTemp2[0]; |
| 881 | rect.set(left , top, left + view.getWidth(), top + view.getHeight()); |
Amith Yamasani | 167d69a | 2011-08-12 19:28:37 -0700 | [diff] [blame] | 882 | } |
| 883 | |
| 884 | private int getPreferredWidth() { |
| 885 | return getContext().getResources() |
| 886 | .getDimensionPixelSize(R.dimen.search_view_preferred_width); |
Amith Yamasani | 5931b1f | 2010-10-18 16:13:14 -0700 | [diff] [blame] | 887 | } |
| 888 | |
Aurimas Liutikas | c8fd00a | 2016-04-15 13:55:53 -0700 | [diff] [blame] | 889 | private int getPreferredHeight() { |
| 890 | return getContext().getResources() |
| 891 | .getDimensionPixelSize(R.dimen.search_view_preferred_height); |
| 892 | } |
| 893 | |
Mathew Inwood | 978c6e2 | 2018-08-21 15:58:55 +0100 | [diff] [blame] | 894 | @UnsupportedAppUsage |
Amith Yamasani | 9322775 | 2010-09-14 10:10:54 -0700 | [diff] [blame] | 895 | private void updateViewsVisibility(final boolean collapsed) { |
| 896 | mIconified = collapsed; |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 897 | // Visibility of views that are visible when collapsed |
Amith Yamasani | 9322775 | 2010-09-14 10:10:54 -0700 | [diff] [blame] | 898 | final int visCollapsed = collapsed ? VISIBLE : GONE; |
Amith Yamasani | 0594476 | 2010-10-08 13:52:38 -0700 | [diff] [blame] | 899 | // Is there text in the query |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 900 | final boolean hasText = !TextUtils.isEmpty(mSearchSrcTextView.getText()); |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 901 | |
| 902 | mSearchButton.setVisibility(visCollapsed); |
Amith Yamasani | 9b2e302 | 2011-01-14 11:34:12 -0800 | [diff] [blame] | 903 | updateSubmitButton(hasText); |
| 904 | mSearchEditFrame.setVisibility(collapsed ? GONE : VISIBLE); |
Alan Viverette | 53b165e | 2015-08-25 12:33:01 -0400 | [diff] [blame] | 905 | |
| 906 | final int iconVisibility; |
| 907 | if (mCollapsedIcon.getDrawable() == null || mIconifiedByDefault) { |
| 908 | iconVisibility = GONE; |
| 909 | } else { |
| 910 | iconVisibility = VISIBLE; |
| 911 | } |
| 912 | mCollapsedIcon.setVisibility(iconVisibility); |
| 913 | |
Amith Yamasani | 4aedb39 | 2010-12-15 16:04:57 -0800 | [diff] [blame] | 914 | updateCloseButton(); |
Amith Yamasani | ebcf5a3a | 2010-10-13 11:35:24 -0700 | [diff] [blame] | 915 | updateVoiceButton(!hasText); |
Amith Yamasani | 9b2e302 | 2011-01-14 11:34:12 -0800 | [diff] [blame] | 916 | updateSubmitArea(); |
Amith Yamasani | 9b2e302 | 2011-01-14 11:34:12 -0800 | [diff] [blame] | 917 | } |
| 918 | |
| 919 | private boolean hasVoiceSearch() { |
| 920 | if (mSearchable != null && mSearchable.getVoiceSearchEnabled()) { |
| 921 | Intent testIntent = null; |
| 922 | if (mSearchable.getVoiceSearchLaunchWebSearch()) { |
| 923 | testIntent = mVoiceWebSearchIntent; |
| 924 | } else if (mSearchable.getVoiceSearchLaunchRecognizer()) { |
| 925 | testIntent = mVoiceAppSearchIntent; |
| 926 | } |
| 927 | if (testIntent != null) { |
| 928 | ResolveInfo ri = getContext().getPackageManager().resolveActivity(testIntent, |
| 929 | PackageManager.MATCH_DEFAULT_ONLY); |
| 930 | return ri != null; |
| 931 | } |
| 932 | } |
| 933 | return false; |
| 934 | } |
| 935 | |
| 936 | private boolean isSubmitAreaEnabled() { |
| 937 | return (mSubmitButtonEnabled || mVoiceButtonEnabled) && !isIconified(); |
| 938 | } |
| 939 | |
Mathew Inwood | 978c6e2 | 2018-08-21 15:58:55 +0100 | [diff] [blame] | 940 | @UnsupportedAppUsage |
Amith Yamasani | 9b2e302 | 2011-01-14 11:34:12 -0800 | [diff] [blame] | 941 | private void updateSubmitButton(boolean hasText) { |
Amith Yamasani | 79f7430 | 2011-03-08 14:16:35 -0800 | [diff] [blame] | 942 | int visibility = GONE; |
Amith Yamasani | cf72ab4 | 2011-11-04 13:49:28 -0700 | [diff] [blame] | 943 | if (mSubmitButtonEnabled && isSubmitAreaEnabled() && hasFocus() |
| 944 | && (hasText || !mVoiceButtonEnabled)) { |
Amith Yamasani | 79f7430 | 2011-03-08 14:16:35 -0800 | [diff] [blame] | 945 | visibility = VISIBLE; |
| 946 | } |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 947 | mGoButton.setVisibility(visibility); |
Amith Yamasani | 9b2e302 | 2011-01-14 11:34:12 -0800 | [diff] [blame] | 948 | } |
| 949 | |
Mathew Inwood | 978c6e2 | 2018-08-21 15:58:55 +0100 | [diff] [blame] | 950 | @UnsupportedAppUsage |
Amith Yamasani | 9b2e302 | 2011-01-14 11:34:12 -0800 | [diff] [blame] | 951 | private void updateSubmitArea() { |
| 952 | int visibility = GONE; |
Amith Yamasani | 79f7430 | 2011-03-08 14:16:35 -0800 | [diff] [blame] | 953 | if (isSubmitAreaEnabled() |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 954 | && (mGoButton.getVisibility() == VISIBLE |
Amith Yamasani | 79f7430 | 2011-03-08 14:16:35 -0800 | [diff] [blame] | 955 | || mVoiceButton.getVisibility() == VISIBLE)) { |
| 956 | visibility = VISIBLE; |
Amith Yamasani | 9b2e302 | 2011-01-14 11:34:12 -0800 | [diff] [blame] | 957 | } |
| 958 | mSubmitArea.setVisibility(visibility); |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 959 | } |
| 960 | |
Amith Yamasani | 4aedb39 | 2010-12-15 16:04:57 -0800 | [diff] [blame] | 961 | private void updateCloseButton() { |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 962 | final boolean hasText = !TextUtils.isEmpty(mSearchSrcTextView.getText()); |
Amith Yamasani | 4aedb39 | 2010-12-15 16:04:57 -0800 | [diff] [blame] | 963 | // Should we show the close button? It is not shown if there's no focus, |
| 964 | // field is not iconified by default and there is no text in it. |
Amith Yamasani | 763bc07 | 2011-07-22 11:53:47 -0700 | [diff] [blame] | 965 | final boolean showClose = hasText || (mIconifiedByDefault && !mExpandedInActionView); |
Amith Yamasani | 167d69a | 2011-08-12 19:28:37 -0700 | [diff] [blame] | 966 | mCloseButton.setVisibility(showClose ? VISIBLE : GONE); |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 967 | final Drawable closeButtonImg = mCloseButton.getDrawable(); |
| 968 | if (closeButtonImg != null){ |
| 969 | closeButtonImg.setState(hasText ? ENABLED_STATE_SET : EMPTY_STATE_SET); |
| 970 | } |
Amith Yamasani | 4aedb39 | 2010-12-15 16:04:57 -0800 | [diff] [blame] | 971 | } |
| 972 | |
Amith Yamasani | a95e488 | 2011-08-17 11:41:37 -0700 | [diff] [blame] | 973 | private void postUpdateFocusedState() { |
| 974 | post(mUpdateDrawableStateRunnable); |
| 975 | } |
| 976 | |
| 977 | private void updateFocusedState() { |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 978 | final boolean focused = mSearchSrcTextView.hasFocus(); |
| 979 | final int[] stateSet = focused ? FOCUSED_STATE_SET : EMPTY_STATE_SET; |
| 980 | final Drawable searchPlateBg = mSearchPlate.getBackground(); |
| 981 | if (searchPlateBg != null) { |
| 982 | searchPlateBg.setState(stateSet); |
| 983 | } |
| 984 | final Drawable submitAreaBg = mSubmitArea.getBackground(); |
| 985 | if (submitAreaBg != null) { |
| 986 | submitAreaBg.setState(stateSet); |
| 987 | } |
Amith Yamasani | a95e488 | 2011-08-17 11:41:37 -0700 | [diff] [blame] | 988 | invalidate(); |
| 989 | } |
| 990 | |
| 991 | @Override |
Amith Yamasani | a465b2d | 2011-08-19 13:01:22 -0700 | [diff] [blame] | 992 | protected void onDetachedFromWindow() { |
Amith Yamasani | a95e488 | 2011-08-17 11:41:37 -0700 | [diff] [blame] | 993 | removeCallbacks(mUpdateDrawableStateRunnable); |
Amith Yamasani | 8790764 | 2011-11-03 11:32:44 -0700 | [diff] [blame] | 994 | post(mReleaseCursorRunnable); |
Amith Yamasani | a95e488 | 2011-08-17 11:41:37 -0700 | [diff] [blame] | 995 | super.onDetachedFromWindow(); |
Amith Yamasani | 79f7430 | 2011-03-08 14:16:35 -0800 | [diff] [blame] | 996 | } |
| 997 | |
Amith Yamasani | e678f46 | 2010-09-15 16:13:43 -0700 | [diff] [blame] | 998 | /** |
| 999 | * Called by the SuggestionsAdapter |
| 1000 | * @hide |
| 1001 | */ |
| 1002 | /* package */void onQueryRefine(CharSequence queryText) { |
| 1003 | setQuery(queryText); |
| 1004 | } |
| 1005 | |
Mathew Inwood | 978c6e2 | 2018-08-21 15:58:55 +0100 | [diff] [blame] | 1006 | @UnsupportedAppUsage |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 1007 | private final OnClickListener mOnClickListener = new OnClickListener() { |
| 1008 | |
| 1009 | public void onClick(View v) { |
| 1010 | if (v == mSearchButton) { |
| 1011 | onSearchClicked(); |
| 1012 | } else if (v == mCloseButton) { |
| 1013 | onCloseClicked(); |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 1014 | } else if (v == mGoButton) { |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 1015 | onSubmitQuery(); |
Amith Yamasani | ebcf5a3a | 2010-10-13 11:35:24 -0700 | [diff] [blame] | 1016 | } else if (v == mVoiceButton) { |
| 1017 | onVoiceClicked(); |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 1018 | } else if (v == mSearchSrcTextView) { |
Amith Yamasani | f28d187 | 2011-07-26 12:21:03 -0700 | [diff] [blame] | 1019 | forceSuggestionQuery(); |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 1020 | } |
| 1021 | } |
| 1022 | }; |
| 1023 | |
| 1024 | /** |
| 1025 | * Handles the key down event for dealing with action keys. |
| 1026 | * |
| 1027 | * @param keyCode This is the keycode of the typed key, and is the same value as |
| 1028 | * found in the KeyEvent parameter. |
| 1029 | * @param event The complete event record for the typed key |
| 1030 | * |
| 1031 | * @return true if the event was handled here, or false if not. |
| 1032 | */ |
| 1033 | @Override |
| 1034 | public boolean onKeyDown(int keyCode, KeyEvent event) { |
| 1035 | if (mSearchable == null) { |
| 1036 | return false; |
| 1037 | } |
| 1038 | |
| 1039 | // if it's an action specified by the searchable activity, launch the |
| 1040 | // entered query with the action key |
| 1041 | SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode); |
| 1042 | if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) { |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 1043 | launchQuerySearch(keyCode, actionKey.getQueryActionMsg(), mSearchSrcTextView.getText() |
Amith Yamasani | 9322775 | 2010-09-14 10:10:54 -0700 | [diff] [blame] | 1044 | .toString()); |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 1045 | return true; |
| 1046 | } |
| 1047 | |
| 1048 | return super.onKeyDown(keyCode, event); |
| 1049 | } |
| 1050 | |
Amith Yamasani | 968ec93 | 2010-12-02 14:00:47 -0800 | [diff] [blame] | 1051 | /** |
| 1052 | * React to the user typing "enter" or other hardwired keys while typing in |
| 1053 | * the search box. This handles these special keys while the edit box has |
| 1054 | * focus. |
| 1055 | */ |
| 1056 | View.OnKeyListener mTextKeyListener = new View.OnKeyListener() { |
| 1057 | public boolean onKey(View v, int keyCode, KeyEvent event) { |
| 1058 | // guard against possible race conditions |
| 1059 | if (mSearchable == null) { |
| 1060 | return false; |
| 1061 | } |
| 1062 | |
| 1063 | if (DBG) { |
| 1064 | Log.d(LOG_TAG, "mTextListener.onKey(" + keyCode + "," + event + "), selection: " |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 1065 | + mSearchSrcTextView.getListSelection()); |
Amith Yamasani | 968ec93 | 2010-12-02 14:00:47 -0800 | [diff] [blame] | 1066 | } |
| 1067 | |
| 1068 | // If a suggestion is selected, handle enter, search key, and action keys |
| 1069 | // as presses on the selected suggestion |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 1070 | if (mSearchSrcTextView.isPopupShowing() |
| 1071 | && mSearchSrcTextView.getListSelection() != ListView.INVALID_POSITION) { |
Amith Yamasani | 968ec93 | 2010-12-02 14:00:47 -0800 | [diff] [blame] | 1072 | return onSuggestionsKey(v, keyCode, event); |
| 1073 | } |
| 1074 | |
| 1075 | // If there is text in the query box, handle enter, and action keys |
| 1076 | // The search key is handled by the dialog's onKeyDown(). |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 1077 | if (!mSearchSrcTextView.isEmpty() && event.hasNoModifiers()) { |
Jeff Brown | 4e6319b | 2010-12-13 10:36:51 -0800 | [diff] [blame] | 1078 | if (event.getAction() == KeyEvent.ACTION_UP) { |
| 1079 | if (keyCode == KeyEvent.KEYCODE_ENTER) { |
| 1080 | v.cancelLongPress(); |
Amith Yamasani | 968ec93 | 2010-12-02 14:00:47 -0800 | [diff] [blame] | 1081 | |
Jeff Brown | 4e6319b | 2010-12-13 10:36:51 -0800 | [diff] [blame] | 1082 | // Launch as a regular search. |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 1083 | launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null, mSearchSrcTextView.getText() |
Jeff Brown | 4e6319b | 2010-12-13 10:36:51 -0800 | [diff] [blame] | 1084 | .toString()); |
| 1085 | return true; |
| 1086 | } |
Amith Yamasani | 968ec93 | 2010-12-02 14:00:47 -0800 | [diff] [blame] | 1087 | } |
| 1088 | if (event.getAction() == KeyEvent.ACTION_DOWN) { |
| 1089 | SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode); |
| 1090 | if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) { |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 1091 | launchQuerySearch(keyCode, actionKey.getQueryActionMsg(), mSearchSrcTextView |
Amith Yamasani | 968ec93 | 2010-12-02 14:00:47 -0800 | [diff] [blame] | 1092 | .getText().toString()); |
| 1093 | return true; |
| 1094 | } |
| 1095 | } |
| 1096 | } |
| 1097 | return false; |
| 1098 | } |
| 1099 | }; |
| 1100 | |
| 1101 | /** |
| 1102 | * React to the user typing while in the suggestions list. First, check for |
| 1103 | * action keys. If not handled, try refocusing regular characters into the |
| 1104 | * EditText. |
| 1105 | */ |
| 1106 | private boolean onSuggestionsKey(View v, int keyCode, KeyEvent event) { |
| 1107 | // guard against possible race conditions (late arrival after dismiss) |
| 1108 | if (mSearchable == null) { |
| 1109 | return false; |
| 1110 | } |
| 1111 | if (mSuggestionsAdapter == null) { |
| 1112 | return false; |
| 1113 | } |
Jeff Brown | 4e6319b | 2010-12-13 10:36:51 -0800 | [diff] [blame] | 1114 | if (event.getAction() == KeyEvent.ACTION_DOWN && event.hasNoModifiers()) { |
Amith Yamasani | 968ec93 | 2010-12-02 14:00:47 -0800 | [diff] [blame] | 1115 | // First, check for enter or search (both of which we'll treat as a |
| 1116 | // "click") |
Jeff Brown | 4e6319b | 2010-12-13 10:36:51 -0800 | [diff] [blame] | 1117 | if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_SEARCH |
| 1118 | || keyCode == KeyEvent.KEYCODE_TAB) { |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 1119 | int position = mSearchSrcTextView.getListSelection(); |
Amith Yamasani | 968ec93 | 2010-12-02 14:00:47 -0800 | [diff] [blame] | 1120 | return onItemClicked(position, KeyEvent.KEYCODE_UNKNOWN, null); |
| 1121 | } |
| 1122 | |
| 1123 | // Next, check for left/right moves, which we use to "return" the |
| 1124 | // user to the edit view |
| 1125 | if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { |
| 1126 | // give "focus" to text editor, with cursor at the beginning if |
| 1127 | // left key, at end if right key |
| 1128 | // TODO: Reverse left/right for right-to-left languages, e.g. |
| 1129 | // Arabic |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 1130 | int selPoint = (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) ? 0 : mSearchSrcTextView |
Amith Yamasani | 968ec93 | 2010-12-02 14:00:47 -0800 | [diff] [blame] | 1131 | .length(); |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 1132 | mSearchSrcTextView.setSelection(selPoint); |
| 1133 | mSearchSrcTextView.setListSelection(0); |
| 1134 | mSearchSrcTextView.clearListSelection(); |
| 1135 | mSearchSrcTextView.ensureImeVisible(true); |
Amith Yamasani | 968ec93 | 2010-12-02 14:00:47 -0800 | [diff] [blame] | 1136 | |
| 1137 | return true; |
| 1138 | } |
| 1139 | |
| 1140 | // Next, check for an "up and out" move |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 1141 | if (keyCode == KeyEvent.KEYCODE_DPAD_UP && 0 == mSearchSrcTextView.getListSelection()) { |
Amith Yamasani | 968ec93 | 2010-12-02 14:00:47 -0800 | [diff] [blame] | 1142 | // TODO: restoreUserQuery(); |
| 1143 | // let ACTV complete the move |
| 1144 | return false; |
| 1145 | } |
| 1146 | |
| 1147 | // Next, check for an "action key" |
| 1148 | SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode); |
| 1149 | if ((actionKey != null) |
| 1150 | && ((actionKey.getSuggestActionMsg() != null) || (actionKey |
| 1151 | .getSuggestActionMsgColumn() != null))) { |
| 1152 | // launch suggestion using action key column |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 1153 | int position = mSearchSrcTextView.getListSelection(); |
Amith Yamasani | 968ec93 | 2010-12-02 14:00:47 -0800 | [diff] [blame] | 1154 | if (position != ListView.INVALID_POSITION) { |
| 1155 | Cursor c = mSuggestionsAdapter.getCursor(); |
| 1156 | if (c.moveToPosition(position)) { |
| 1157 | final String actionMsg = getActionKeyMessage(c, actionKey); |
| 1158 | if (actionMsg != null && (actionMsg.length() > 0)) { |
| 1159 | return onItemClicked(position, keyCode, actionMsg); |
| 1160 | } |
| 1161 | } |
| 1162 | } |
| 1163 | } |
| 1164 | } |
| 1165 | return false; |
| 1166 | } |
| 1167 | |
| 1168 | /** |
| 1169 | * For a given suggestion and a given cursor row, get the action message. If |
| 1170 | * not provided by the specific row/column, also check for a single |
| 1171 | * definition (for the action key). |
| 1172 | * |
| 1173 | * @param c The cursor providing suggestions |
| 1174 | * @param actionKey The actionkey record being examined |
| 1175 | * |
| 1176 | * @return Returns a string, or null if no action key message for this |
| 1177 | * suggestion |
| 1178 | */ |
| 1179 | private static String getActionKeyMessage(Cursor c, SearchableInfo.ActionKeyInfo actionKey) { |
| 1180 | String result = null; |
| 1181 | // check first in the cursor data, for a suggestion-specific message |
| 1182 | final String column = actionKey.getSuggestActionMsgColumn(); |
| 1183 | if (column != null) { |
| 1184 | result = SuggestionsAdapter.getColumnString(c, column); |
| 1185 | } |
| 1186 | // If the cursor didn't give us a message, see if there's a single |
| 1187 | // message defined |
| 1188 | // for the actionkey (for all suggestions) |
| 1189 | if (result == null) { |
| 1190 | result = actionKey.getSuggestActionMsg(); |
| 1191 | } |
| 1192 | return result; |
| 1193 | } |
| 1194 | |
Amith Yamasani | b4569fb | 2011-07-08 15:25:39 -0700 | [diff] [blame] | 1195 | private CharSequence getDecoratedHint(CharSequence hintText) { |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 1196 | // If the field is always expanded or we don't have a search hint icon, |
| 1197 | // then don't add the search icon to the hint. |
| 1198 | if (!mIconifiedByDefault || mSearchHintIcon == null) { |
Alan Viverette | 5dddb70 | 2014-07-02 15:46:04 -0700 | [diff] [blame] | 1199 | return hintText; |
| 1200 | } |
Amith Yamasani | b4569fb | 2011-07-08 15:25:39 -0700 | [diff] [blame] | 1201 | |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 1202 | final int textSize = (int) (mSearchSrcTextView.getTextSize() * 1.25); |
| 1203 | mSearchHintIcon.setBounds(0, 0, textSize, textSize); |
Alan Viverette | 5dddb70 | 2014-07-02 15:46:04 -0700 | [diff] [blame] | 1204 | |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 1205 | final SpannableStringBuilder ssb = new SpannableStringBuilder(" "); |
| 1206 | ssb.setSpan(new ImageSpan(mSearchHintIcon), 1, 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); |
Alan Viverette | 5dddb70 | 2014-07-02 15:46:04 -0700 | [diff] [blame] | 1207 | ssb.append(hintText); |
Amith Yamasani | b4569fb | 2011-07-08 15:25:39 -0700 | [diff] [blame] | 1208 | return ssb; |
| 1209 | } |
| 1210 | |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 1211 | private void updateQueryHint() { |
Alan Viverette | b4004df | 2015-04-29 16:55:42 -0700 | [diff] [blame] | 1212 | final CharSequence hint = getQueryHint(); |
| 1213 | mSearchSrcTextView.setHint(getDecoratedHint(hint == null ? "" : hint)); |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 1214 | } |
| 1215 | |
| 1216 | /** |
| 1217 | * Updates the auto-complete text view. |
| 1218 | */ |
| 1219 | private void updateSearchAutoComplete() { |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 1220 | mSearchSrcTextView.setDropDownAnimationStyle(0); // no animation |
| 1221 | mSearchSrcTextView.setThreshold(mSearchable.getSuggestThreshold()); |
| 1222 | mSearchSrcTextView.setImeOptions(mSearchable.getImeOptions()); |
Amith Yamasani | 5607a38 | 2011-08-09 14:16:37 -0700 | [diff] [blame] | 1223 | int inputType = mSearchable.getInputType(); |
| 1224 | // We only touch this if the input type is set up for text (which it almost certainly |
| 1225 | // should be, in the case of search!) |
| 1226 | if ((inputType & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_TEXT) { |
| 1227 | // The existence of a suggestions authority is the proxy for "suggestions |
| 1228 | // are available here" |
| 1229 | inputType &= ~InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE; |
| 1230 | if (mSearchable.getSuggestAuthority() != null) { |
| 1231 | inputType |= InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE; |
Satoshi Kataoka | 9ce1116 | 2012-06-04 17:12:08 +0900 | [diff] [blame] | 1232 | // TYPE_TEXT_FLAG_AUTO_COMPLETE means that the text editor is performing |
| 1233 | // auto-completion based on its own semantics, which it will present to the user |
| 1234 | // as they type. This generally means that the input method should not show its |
| 1235 | // own candidates, and the spell checker should not be in action. The text editor |
| 1236 | // supplies its candidates by calling InputMethodManager.displayCompletions(), |
| 1237 | // which in turn will call InputMethodSession.displayCompletions(). |
| 1238 | inputType |= InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS; |
Amith Yamasani | 5607a38 | 2011-08-09 14:16:37 -0700 | [diff] [blame] | 1239 | } |
| 1240 | } |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 1241 | mSearchSrcTextView.setInputType(inputType); |
Amith Yamasani | 8790764 | 2011-11-03 11:32:44 -0700 | [diff] [blame] | 1242 | if (mSuggestionsAdapter != null) { |
| 1243 | mSuggestionsAdapter.changeCursor(null); |
| 1244 | } |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 1245 | // attach the suggestions adapter, if suggestions are available |
| 1246 | // The existence of a suggestions authority is the proxy for "suggestions available here" |
| 1247 | if (mSearchable.getSuggestAuthority() != null) { |
| 1248 | mSuggestionsAdapter = new SuggestionsAdapter(getContext(), |
| 1249 | this, mSearchable, mOutsideDrawablesCache); |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 1250 | mSearchSrcTextView.setAdapter(mSuggestionsAdapter); |
Amith Yamasani | e678f46 | 2010-09-15 16:13:43 -0700 | [diff] [blame] | 1251 | ((SuggestionsAdapter) mSuggestionsAdapter).setQueryRefinement( |
| 1252 | mQueryRefinement ? SuggestionsAdapter.REFINE_ALL |
| 1253 | : SuggestionsAdapter.REFINE_BY_ENTRY); |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 1254 | } |
| 1255 | } |
| 1256 | |
Amith Yamasani | ebcf5a3a | 2010-10-13 11:35:24 -0700 | [diff] [blame] | 1257 | /** |
| 1258 | * Update the visibility of the voice button. There are actually two voice search modes, |
| 1259 | * either of which will activate the button. |
| 1260 | * @param empty whether the search query text field is empty. If it is, then the other |
Amith Yamasani | 79f7430 | 2011-03-08 14:16:35 -0800 | [diff] [blame] | 1261 | * criteria apply to make the voice button visible. |
Amith Yamasani | ebcf5a3a | 2010-10-13 11:35:24 -0700 | [diff] [blame] | 1262 | */ |
| 1263 | private void updateVoiceButton(boolean empty) { |
Amith Yamasani | 79f7430 | 2011-03-08 14:16:35 -0800 | [diff] [blame] | 1264 | int visibility = GONE; |
Amith Yamasani | 167d69a | 2011-08-12 19:28:37 -0700 | [diff] [blame] | 1265 | if (mVoiceButtonEnabled && !isIconified() && empty) { |
Amith Yamasani | 9b2e302 | 2011-01-14 11:34:12 -0800 | [diff] [blame] | 1266 | visibility = VISIBLE; |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 1267 | mGoButton.setVisibility(GONE); |
Amith Yamasani | ebcf5a3a | 2010-10-13 11:35:24 -0700 | [diff] [blame] | 1268 | } |
| 1269 | mVoiceButton.setVisibility(visibility); |
| 1270 | } |
| 1271 | |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 1272 | private final OnEditorActionListener mOnEditorActionListener = new OnEditorActionListener() { |
| 1273 | |
| 1274 | /** |
| 1275 | * Called when the input method default action key is pressed. |
| 1276 | */ |
| 1277 | public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { |
| 1278 | onSubmitQuery(); |
| 1279 | return true; |
| 1280 | } |
| 1281 | }; |
| 1282 | |
| 1283 | private void onTextChanged(CharSequence newText) { |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 1284 | CharSequence text = mSearchSrcTextView.getText(); |
Amith Yamasani | 068d73c | 2011-05-27 15:15:14 -0700 | [diff] [blame] | 1285 | mUserQuery = text; |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 1286 | boolean hasText = !TextUtils.isEmpty(text); |
Amith Yamasani | cf72ab4 | 2011-11-04 13:49:28 -0700 | [diff] [blame] | 1287 | updateSubmitButton(hasText); |
Amith Yamasani | ebcf5a3a | 2010-10-13 11:35:24 -0700 | [diff] [blame] | 1288 | updateVoiceButton(!hasText); |
Amith Yamasani | 73e00df | 2010-12-16 16:31:29 -0800 | [diff] [blame] | 1289 | updateCloseButton(); |
Amith Yamasani | 9b2e302 | 2011-01-14 11:34:12 -0800 | [diff] [blame] | 1290 | updateSubmitArea(); |
Amith Yamasani | b47c4fd | 2011-08-04 14:30:07 -0700 | [diff] [blame] | 1291 | if (mOnQueryChangeListener != null && !TextUtils.equals(newText, mOldQueryText)) { |
Adam Powell | 01f2135 | 2011-01-20 18:30:10 -0800 | [diff] [blame] | 1292 | mOnQueryChangeListener.onQueryTextChange(newText.toString()); |
Amith Yamasani | ebcf5a3a | 2010-10-13 11:35:24 -0700 | [diff] [blame] | 1293 | } |
Amith Yamasani | b47c4fd | 2011-08-04 14:30:07 -0700 | [diff] [blame] | 1294 | mOldQueryText = newText.toString(); |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 1295 | } |
| 1296 | |
| 1297 | private void onSubmitQuery() { |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 1298 | CharSequence query = mSearchSrcTextView.getText(); |
Amith Yamasani | 6a7421b | 2011-07-27 11:55:53 -0700 | [diff] [blame] | 1299 | if (query != null && TextUtils.getTrimmedLength(query) > 0) { |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 1300 | if (mOnQueryChangeListener == null |
Adam Powell | 01f2135 | 2011-01-20 18:30:10 -0800 | [diff] [blame] | 1301 | || !mOnQueryChangeListener.onQueryTextSubmit(query.toString())) { |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 1302 | if (mSearchable != null) { |
| 1303 | launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null, query.toString()); |
| 1304 | } |
Yohei Yukawa | 5cfc1b4 | 2017-03-07 00:52:09 -0800 | [diff] [blame] | 1305 | mSearchSrcTextView.setImeVisibility(false); |
Amith Yamasani | ebcf5a3a | 2010-10-13 11:35:24 -0700 | [diff] [blame] | 1306 | dismissSuggestions(); |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 1307 | } |
| 1308 | } |
| 1309 | } |
| 1310 | |
Amith Yamasani | ebcf5a3a | 2010-10-13 11:35:24 -0700 | [diff] [blame] | 1311 | private void dismissSuggestions() { |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 1312 | mSearchSrcTextView.dismissDropDown(); |
Amith Yamasani | ebcf5a3a | 2010-10-13 11:35:24 -0700 | [diff] [blame] | 1313 | } |
| 1314 | |
Mathew Inwood | 8c854f8 | 2018-09-14 12:35:36 +0100 | [diff] [blame] | 1315 | @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 1316 | private void onCloseClicked() { |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 1317 | CharSequence text = mSearchSrcTextView.getText(); |
Amith Yamasani | 2465298 | 2011-06-23 16:16:05 -0700 | [diff] [blame] | 1318 | if (TextUtils.isEmpty(text)) { |
| 1319 | if (mIconifiedByDefault) { |
Amith Yamasani | b4569fb | 2011-07-08 15:25:39 -0700 | [diff] [blame] | 1320 | // If the app doesn't override the close behavior |
| 1321 | if (mOnCloseListener == null || !mOnCloseListener.onClose()) { |
| 1322 | // hide the keyboard and remove focus |
| 1323 | clearFocus(); |
| 1324 | // collapse the search field |
| 1325 | updateViewsVisibility(true); |
| 1326 | } |
Amith Yamasani | 0594476 | 2010-10-08 13:52:38 -0700 | [diff] [blame] | 1327 | } |
Amith Yamasani | 2465298 | 2011-06-23 16:16:05 -0700 | [diff] [blame] | 1328 | } else { |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 1329 | mSearchSrcTextView.setText(""); |
| 1330 | mSearchSrcTextView.requestFocus(); |
Yohei Yukawa | 5cfc1b4 | 2017-03-07 00:52:09 -0800 | [diff] [blame] | 1331 | mSearchSrcTextView.setImeVisibility(true); |
Amith Yamasani | 2465298 | 2011-06-23 16:16:05 -0700 | [diff] [blame] | 1332 | } |
| 1333 | |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 1334 | } |
| 1335 | |
| 1336 | private void onSearchClicked() { |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 1337 | updateViewsVisibility(false); |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 1338 | mSearchSrcTextView.requestFocus(); |
Yohei Yukawa | 5cfc1b4 | 2017-03-07 00:52:09 -0800 | [diff] [blame] | 1339 | mSearchSrcTextView.setImeVisibility(true); |
Amith Yamasani | 4838548 | 2010-12-03 14:43:52 -0800 | [diff] [blame] | 1340 | if (mOnSearchClickListener != null) { |
| 1341 | mOnSearchClickListener.onClick(this); |
| 1342 | } |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 1343 | } |
| 1344 | |
Amith Yamasani | ebcf5a3a | 2010-10-13 11:35:24 -0700 | [diff] [blame] | 1345 | private void onVoiceClicked() { |
| 1346 | // guard against possible race conditions |
| 1347 | if (mSearchable == null) { |
| 1348 | return; |
| 1349 | } |
| 1350 | SearchableInfo searchable = mSearchable; |
| 1351 | try { |
| 1352 | if (searchable.getVoiceSearchLaunchWebSearch()) { |
| 1353 | Intent webSearchIntent = createVoiceWebSearchIntent(mVoiceWebSearchIntent, |
| 1354 | searchable); |
| 1355 | getContext().startActivity(webSearchIntent); |
| 1356 | } else if (searchable.getVoiceSearchLaunchRecognizer()) { |
| 1357 | Intent appSearchIntent = createVoiceAppSearchIntent(mVoiceAppSearchIntent, |
| 1358 | searchable); |
| 1359 | getContext().startActivity(appSearchIntent); |
| 1360 | } |
| 1361 | } catch (ActivityNotFoundException e) { |
| 1362 | // Should not happen, since we check the availability of |
| 1363 | // voice search before showing the button. But just in case... |
| 1364 | Log.w(LOG_TAG, "Could not find voice search activity"); |
| 1365 | } |
| 1366 | } |
| 1367 | |
Amith Yamasani | 4aedb39 | 2010-12-15 16:04:57 -0800 | [diff] [blame] | 1368 | void onTextFocusChanged() { |
Amith Yamasani | 79f7430 | 2011-03-08 14:16:35 -0800 | [diff] [blame] | 1369 | updateViewsVisibility(isIconified()); |
Amith Yamasani | a95e488 | 2011-08-17 11:41:37 -0700 | [diff] [blame] | 1370 | // Delayed update to make sure that the focus has settled down and window focus changes |
| 1371 | // don't affect it. A synchronous update was not working. |
| 1372 | postUpdateFocusedState(); |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 1373 | if (mSearchSrcTextView.hasFocus()) { |
Amith Yamasani | f28d187 | 2011-07-26 12:21:03 -0700 | [diff] [blame] | 1374 | forceSuggestionQuery(); |
| 1375 | } |
Amith Yamasani | 4aedb39 | 2010-12-15 16:04:57 -0800 | [diff] [blame] | 1376 | } |
| 1377 | |
Amith Yamasani | b4569fb | 2011-07-08 15:25:39 -0700 | [diff] [blame] | 1378 | @Override |
Amith Yamasani | a95e488 | 2011-08-17 11:41:37 -0700 | [diff] [blame] | 1379 | public void onWindowFocusChanged(boolean hasWindowFocus) { |
| 1380 | super.onWindowFocusChanged(hasWindowFocus); |
| 1381 | |
| 1382 | postUpdateFocusedState(); |
Amith Yamasani | b4569fb | 2011-07-08 15:25:39 -0700 | [diff] [blame] | 1383 | } |
| 1384 | |
Amith Yamasani | 763bc07 | 2011-07-22 11:53:47 -0700 | [diff] [blame] | 1385 | /** |
| 1386 | * {@inheritDoc} |
| 1387 | */ |
| 1388 | @Override |
| 1389 | public void onActionViewCollapsed() { |
Adam Powell | 99d0ce9 | 2012-12-10 13:11:47 -0800 | [diff] [blame] | 1390 | setQuery("", false); |
Amith Yamasani | 10da590 | 2011-07-26 16:14:26 -0700 | [diff] [blame] | 1391 | clearFocus(); |
| 1392 | updateViewsVisibility(true); |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 1393 | mSearchSrcTextView.setImeOptions(mCollapsedImeOptions); |
Amith Yamasani | 763bc07 | 2011-07-22 11:53:47 -0700 | [diff] [blame] | 1394 | mExpandedInActionView = false; |
| 1395 | } |
| 1396 | |
| 1397 | /** |
| 1398 | * {@inheritDoc} |
| 1399 | */ |
| 1400 | @Override |
| 1401 | public void onActionViewExpanded() { |
Amith Yamasani | 434c73f | 2011-11-01 11:44:50 -0700 | [diff] [blame] | 1402 | if (mExpandedInActionView) return; |
| 1403 | |
Amith Yamasani | 763bc07 | 2011-07-22 11:53:47 -0700 | [diff] [blame] | 1404 | mExpandedInActionView = true; |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 1405 | mCollapsedImeOptions = mSearchSrcTextView.getImeOptions(); |
| 1406 | mSearchSrcTextView.setImeOptions(mCollapsedImeOptions | EditorInfo.IME_FLAG_NO_FULLSCREEN); |
| 1407 | mSearchSrcTextView.setText(""); |
Amith Yamasani | 763bc07 | 2011-07-22 11:53:47 -0700 | [diff] [blame] | 1408 | setIconified(false); |
| 1409 | } |
| 1410 | |
Aurimas Liutikas | 13fdea0 | 2016-02-11 10:10:10 -0800 | [diff] [blame] | 1411 | static class SavedState extends BaseSavedState { |
| 1412 | boolean isIconified; |
| 1413 | |
| 1414 | SavedState(Parcelable superState) { |
| 1415 | super(superState); |
| 1416 | } |
| 1417 | |
| 1418 | public SavedState(Parcel source) { |
| 1419 | super(source); |
| 1420 | isIconified = (Boolean) source.readValue(null); |
| 1421 | } |
| 1422 | |
| 1423 | @Override |
| 1424 | public void writeToParcel(Parcel dest, int flags) { |
| 1425 | super.writeToParcel(dest, flags); |
| 1426 | dest.writeValue(isIconified); |
| 1427 | } |
| 1428 | |
| 1429 | @Override |
| 1430 | public String toString() { |
| 1431 | return "SearchView.SavedState{" |
| 1432 | + Integer.toHexString(System.identityHashCode(this)) |
| 1433 | + " isIconified=" + isIconified + "}"; |
| 1434 | } |
Aurimas Liutikas | 7849a3d | 2016-02-26 15:27:31 -0800 | [diff] [blame] | 1435 | |
Jeff Sharkey | 9e8f83d | 2019-02-28 12:06:45 -0700 | [diff] [blame] | 1436 | public static final @android.annotation.NonNull Parcelable.Creator<SavedState> CREATOR = |
Aurimas Liutikas | 7849a3d | 2016-02-26 15:27:31 -0800 | [diff] [blame] | 1437 | new Parcelable.Creator<SavedState>() { |
| 1438 | public SavedState createFromParcel(Parcel in) { |
| 1439 | return new SavedState(in); |
| 1440 | } |
| 1441 | |
| 1442 | public SavedState[] newArray(int size) { |
| 1443 | return new SavedState[size]; |
| 1444 | } |
| 1445 | }; |
Aurimas Liutikas | 13fdea0 | 2016-02-11 10:10:10 -0800 | [diff] [blame] | 1446 | } |
| 1447 | |
| 1448 | @Override |
| 1449 | protected Parcelable onSaveInstanceState() { |
| 1450 | Parcelable superState = super.onSaveInstanceState(); |
| 1451 | SavedState ss = new SavedState(superState); |
| 1452 | ss.isIconified = isIconified(); |
| 1453 | return ss; |
| 1454 | } |
| 1455 | |
| 1456 | @Override |
| 1457 | protected void onRestoreInstanceState(Parcelable state) { |
| 1458 | SavedState ss = (SavedState) state; |
| 1459 | super.onRestoreInstanceState(ss.getSuperState()); |
| 1460 | updateViewsVisibility(ss.isIconified); |
| 1461 | requestLayout(); |
| 1462 | } |
| 1463 | |
Svetoslav Ganov | 8a78fd4 | 2012-01-17 14:36:46 -0800 | [diff] [blame] | 1464 | @Override |
Dianne Hackborn | a7bb6fb | 2015-02-03 18:13:40 -0800 | [diff] [blame] | 1465 | public CharSequence getAccessibilityClassName() { |
| 1466 | return SearchView.class.getName(); |
Svetoslav Ganov | 8a78fd4 | 2012-01-17 14:36:46 -0800 | [diff] [blame] | 1467 | } |
| 1468 | |
Amith Yamasani | b4569fb | 2011-07-08 15:25:39 -0700 | [diff] [blame] | 1469 | private void adjustDropDownSizeAndPosition() { |
| 1470 | if (mDropDownAnchor.getWidth() > 1) { |
| 1471 | Resources res = getContext().getResources(); |
| 1472 | int anchorPadding = mSearchPlate.getPaddingLeft(); |
| 1473 | Rect dropDownPadding = new Rect(); |
Fabrice Di Meglio | 3a69bea | 2012-07-26 13:50:10 -0700 | [diff] [blame] | 1474 | final boolean isLayoutRtl = isLayoutRtl(); |
Amith Yamasani | b4569fb | 2011-07-08 15:25:39 -0700 | [diff] [blame] | 1475 | int iconOffset = mIconifiedByDefault |
| 1476 | ? res.getDimensionPixelSize(R.dimen.dropdownitem_icon_width) |
| 1477 | + res.getDimensionPixelSize(R.dimen.dropdownitem_text_padding_left) |
| 1478 | : 0; |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 1479 | mSearchSrcTextView.getDropDownBackground().getPadding(dropDownPadding); |
Fabrice Di Meglio | 3a69bea | 2012-07-26 13:50:10 -0700 | [diff] [blame] | 1480 | int offset; |
| 1481 | if (isLayoutRtl) { |
| 1482 | offset = - dropDownPadding.left; |
| 1483 | } else { |
| 1484 | offset = anchorPadding - (dropDownPadding.left + iconOffset); |
| 1485 | } |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 1486 | mSearchSrcTextView.setDropDownHorizontalOffset(offset); |
Fabrice Di Meglio | 3a69bea | 2012-07-26 13:50:10 -0700 | [diff] [blame] | 1487 | final int width = mDropDownAnchor.getWidth() + dropDownPadding.left |
| 1488 | + dropDownPadding.right + iconOffset - anchorPadding; |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 1489 | mSearchSrcTextView.setDropDownWidth(width); |
Amith Yamasani | b4569fb | 2011-07-08 15:25:39 -0700 | [diff] [blame] | 1490 | } |
| 1491 | } |
| 1492 | |
Amith Yamasani | 968ec93 | 2010-12-02 14:00:47 -0800 | [diff] [blame] | 1493 | private boolean onItemClicked(int position, int actionKey, String actionMsg) { |
| 1494 | if (mOnSuggestionListener == null |
Adam Powell | 01f2135 | 2011-01-20 18:30:10 -0800 | [diff] [blame] | 1495 | || !mOnSuggestionListener.onSuggestionClick(position)) { |
Amith Yamasani | 968ec93 | 2010-12-02 14:00:47 -0800 | [diff] [blame] | 1496 | launchSuggestion(position, KeyEvent.KEYCODE_UNKNOWN, null); |
Yohei Yukawa | 5cfc1b4 | 2017-03-07 00:52:09 -0800 | [diff] [blame] | 1497 | mSearchSrcTextView.setImeVisibility(false); |
Amith Yamasani | 968ec93 | 2010-12-02 14:00:47 -0800 | [diff] [blame] | 1498 | dismissSuggestions(); |
| 1499 | return true; |
| 1500 | } |
| 1501 | return false; |
| 1502 | } |
| 1503 | |
| 1504 | private boolean onItemSelected(int position) { |
| 1505 | if (mOnSuggestionListener == null |
Adam Powell | 01f2135 | 2011-01-20 18:30:10 -0800 | [diff] [blame] | 1506 | || !mOnSuggestionListener.onSuggestionSelect(position)) { |
Amith Yamasani | 968ec93 | 2010-12-02 14:00:47 -0800 | [diff] [blame] | 1507 | rewriteQueryFromSuggestion(position); |
| 1508 | return true; |
| 1509 | } |
| 1510 | return false; |
| 1511 | } |
| 1512 | |
Mathew Inwood | 978c6e2 | 2018-08-21 15:58:55 +0100 | [diff] [blame] | 1513 | @UnsupportedAppUsage |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 1514 | private final OnItemClickListener mOnItemClickListener = new OnItemClickListener() { |
| 1515 | |
| 1516 | /** |
| 1517 | * Implements OnItemClickListener |
| 1518 | */ |
| 1519 | public void onItemClick(AdapterView<?> parent, View view, int position, long id) { |
Amith Yamasani | 968ec93 | 2010-12-02 14:00:47 -0800 | [diff] [blame] | 1520 | if (DBG) Log.d(LOG_TAG, "onItemClick() position " + position); |
| 1521 | onItemClicked(position, KeyEvent.KEYCODE_UNKNOWN, null); |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 1522 | } |
| 1523 | }; |
| 1524 | |
| 1525 | private final OnItemSelectedListener mOnItemSelectedListener = new OnItemSelectedListener() { |
| 1526 | |
| 1527 | /** |
| 1528 | * Implements OnItemSelectedListener |
| 1529 | */ |
| 1530 | public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { |
Amith Yamasani | 968ec93 | 2010-12-02 14:00:47 -0800 | [diff] [blame] | 1531 | if (DBG) Log.d(LOG_TAG, "onItemSelected() position " + position); |
| 1532 | SearchView.this.onItemSelected(position); |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 1533 | } |
| 1534 | |
| 1535 | /** |
| 1536 | * Implements OnItemSelectedListener |
| 1537 | */ |
| 1538 | public void onNothingSelected(AdapterView<?> parent) { |
| 1539 | if (DBG) |
| 1540 | Log.d(LOG_TAG, "onNothingSelected()"); |
| 1541 | } |
| 1542 | }; |
| 1543 | |
| 1544 | /** |
| 1545 | * Query rewriting. |
| 1546 | */ |
| 1547 | private void rewriteQueryFromSuggestion(int position) { |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 1548 | CharSequence oldQuery = mSearchSrcTextView.getText(); |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 1549 | Cursor c = mSuggestionsAdapter.getCursor(); |
| 1550 | if (c == null) { |
| 1551 | return; |
| 1552 | } |
| 1553 | if (c.moveToPosition(position)) { |
| 1554 | // Get the new query from the suggestion. |
| 1555 | CharSequence newQuery = mSuggestionsAdapter.convertToString(c); |
| 1556 | if (newQuery != null) { |
| 1557 | // The suggestion rewrites the query. |
| 1558 | // Update the text field, without getting new suggestions. |
| 1559 | setQuery(newQuery); |
| 1560 | } else { |
| 1561 | // The suggestion does not rewrite the query, restore the user's query. |
| 1562 | setQuery(oldQuery); |
| 1563 | } |
| 1564 | } else { |
| 1565 | // We got a bad position, restore the user's query. |
| 1566 | setQuery(oldQuery); |
| 1567 | } |
| 1568 | } |
| 1569 | |
| 1570 | /** |
| 1571 | * Launches an intent based on a suggestion. |
| 1572 | * |
| 1573 | * @param position The index of the suggestion to create the intent from. |
| 1574 | * @param actionKey The key code of the action key that was pressed, |
| 1575 | * or {@link KeyEvent#KEYCODE_UNKNOWN} if none. |
| 1576 | * @param actionMsg The message for the action key that was pressed, |
| 1577 | * or <code>null</code> if none. |
| 1578 | * @return true if a successful launch, false if could not (e.g. bad position). |
| 1579 | */ |
| 1580 | private boolean launchSuggestion(int position, int actionKey, String actionMsg) { |
| 1581 | Cursor c = mSuggestionsAdapter.getCursor(); |
| 1582 | if ((c != null) && c.moveToPosition(position)) { |
| 1583 | |
| 1584 | Intent intent = createIntentFromSuggestion(c, actionKey, actionMsg); |
| 1585 | |
| 1586 | // launch the intent |
| 1587 | launchIntent(intent); |
| 1588 | |
| 1589 | return true; |
| 1590 | } |
| 1591 | return false; |
| 1592 | } |
| 1593 | |
| 1594 | /** |
| 1595 | * Launches an intent, including any special intent handling. |
| 1596 | */ |
| 1597 | private void launchIntent(Intent intent) { |
| 1598 | if (intent == null) { |
| 1599 | return; |
| 1600 | } |
| 1601 | try { |
| 1602 | // If the intent was created from a suggestion, it will always have an explicit |
| 1603 | // component here. |
| 1604 | getContext().startActivity(intent); |
| 1605 | } catch (RuntimeException ex) { |
| 1606 | Log.e(LOG_TAG, "Failed launch activity: " + intent, ex); |
| 1607 | } |
| 1608 | } |
| 1609 | |
| 1610 | /** |
| 1611 | * Sets the text in the query box, without updating the suggestions. |
| 1612 | */ |
Mathew Inwood | 8c854f8 | 2018-09-14 12:35:36 +0100 | [diff] [blame] | 1613 | @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 1614 | private void setQuery(CharSequence query) { |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 1615 | mSearchSrcTextView.setText(query, true); |
Amith Yamasani | e678f46 | 2010-09-15 16:13:43 -0700 | [diff] [blame] | 1616 | // Move the cursor to the end |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 1617 | mSearchSrcTextView.setSelection(TextUtils.isEmpty(query) ? 0 : query.length()); |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 1618 | } |
| 1619 | |
| 1620 | private void launchQuerySearch(int actionKey, String actionMsg, String query) { |
| 1621 | String action = Intent.ACTION_SEARCH; |
Amith Yamasani | e678f46 | 2010-09-15 16:13:43 -0700 | [diff] [blame] | 1622 | Intent intent = createIntent(action, null, null, query, actionKey, actionMsg); |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 1623 | getContext().startActivity(intent); |
| 1624 | } |
| 1625 | |
| 1626 | /** |
| 1627 | * Constructs an intent from the given information and the search dialog state. |
| 1628 | * |
| 1629 | * @param action Intent action. |
| 1630 | * @param data Intent data, or <code>null</code>. |
| 1631 | * @param extraData Data for {@link SearchManager#EXTRA_DATA_KEY} or <code>null</code>. |
| 1632 | * @param query Intent query, or <code>null</code>. |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 1633 | * @param actionKey The key code of the action key that was pressed, |
| 1634 | * or {@link KeyEvent#KEYCODE_UNKNOWN} if none. |
| 1635 | * @param actionMsg The message for the action key that was pressed, |
| 1636 | * or <code>null</code> if none. |
| 1637 | * @param mode The search mode, one of the acceptable values for |
| 1638 | * {@link SearchManager#SEARCH_MODE}, or {@code null}. |
| 1639 | * @return The intent. |
| 1640 | */ |
| 1641 | private Intent createIntent(String action, Uri data, String extraData, String query, |
Amith Yamasani | e678f46 | 2010-09-15 16:13:43 -0700 | [diff] [blame] | 1642 | int actionKey, String actionMsg) { |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 1643 | // Now build the Intent |
| 1644 | Intent intent = new Intent(action); |
| 1645 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); |
| 1646 | // We need CLEAR_TOP to avoid reusing an old task that has other activities |
| 1647 | // on top of the one we want. We don't want to do this in in-app search though, |
| 1648 | // as it can be destructive to the activity stack. |
| 1649 | if (data != null) { |
| 1650 | intent.setData(data); |
| 1651 | } |
Amith Yamasani | 068d73c | 2011-05-27 15:15:14 -0700 | [diff] [blame] | 1652 | intent.putExtra(SearchManager.USER_QUERY, mUserQuery); |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 1653 | if (query != null) { |
| 1654 | intent.putExtra(SearchManager.QUERY, query); |
| 1655 | } |
| 1656 | if (extraData != null) { |
| 1657 | intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData); |
| 1658 | } |
Amith Yamasani | 940ef38 | 2011-03-02 18:43:23 -0800 | [diff] [blame] | 1659 | if (mAppSearchData != null) { |
| 1660 | intent.putExtra(SearchManager.APP_DATA, mAppSearchData); |
| 1661 | } |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 1662 | if (actionKey != KeyEvent.KEYCODE_UNKNOWN) { |
| 1663 | intent.putExtra(SearchManager.ACTION_KEY, actionKey); |
| 1664 | intent.putExtra(SearchManager.ACTION_MSG, actionMsg); |
| 1665 | } |
| 1666 | intent.setComponent(mSearchable.getSearchActivity()); |
| 1667 | return intent; |
| 1668 | } |
| 1669 | |
| 1670 | /** |
Amith Yamasani | ebcf5a3a | 2010-10-13 11:35:24 -0700 | [diff] [blame] | 1671 | * Create and return an Intent that can launch the voice search activity for web search. |
| 1672 | */ |
| 1673 | private Intent createVoiceWebSearchIntent(Intent baseIntent, SearchableInfo searchable) { |
| 1674 | Intent voiceIntent = new Intent(baseIntent); |
| 1675 | ComponentName searchActivity = searchable.getSearchActivity(); |
| 1676 | voiceIntent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, searchActivity == null ? null |
| 1677 | : searchActivity.flattenToShortString()); |
| 1678 | return voiceIntent; |
| 1679 | } |
| 1680 | |
| 1681 | /** |
| 1682 | * Create and return an Intent that can launch the voice search activity, perform a specific |
| 1683 | * voice transcription, and forward the results to the searchable activity. |
| 1684 | * |
| 1685 | * @param baseIntent The voice app search intent to start from |
| 1686 | * @return A completely-configured intent ready to send to the voice search activity |
| 1687 | */ |
| 1688 | private Intent createVoiceAppSearchIntent(Intent baseIntent, SearchableInfo searchable) { |
| 1689 | ComponentName searchActivity = searchable.getSearchActivity(); |
| 1690 | |
| 1691 | // create the necessary intent to set up a search-and-forward operation |
| 1692 | // in the voice search system. We have to keep the bundle separate, |
| 1693 | // because it becomes immutable once it enters the PendingIntent |
| 1694 | Intent queryIntent = new Intent(Intent.ACTION_SEARCH); |
| 1695 | queryIntent.setComponent(searchActivity); |
| 1696 | PendingIntent pending = PendingIntent.getActivity(getContext(), 0, queryIntent, |
| 1697 | PendingIntent.FLAG_ONE_SHOT); |
| 1698 | |
| 1699 | // Now set up the bundle that will be inserted into the pending intent |
| 1700 | // when it's time to do the search. We always build it here (even if empty) |
| 1701 | // because the voice search activity will always need to insert "QUERY" into |
| 1702 | // it anyway. |
| 1703 | Bundle queryExtras = new Bundle(); |
Jorge Ruesga | 1bcfe84 | 2012-09-03 01:26:59 +0200 | [diff] [blame] | 1704 | if (mAppSearchData != null) { |
| 1705 | queryExtras.putParcelable(SearchManager.APP_DATA, mAppSearchData); |
| 1706 | } |
Amith Yamasani | ebcf5a3a | 2010-10-13 11:35:24 -0700 | [diff] [blame] | 1707 | |
| 1708 | // Now build the intent to launch the voice search. Add all necessary |
| 1709 | // extras to launch the voice recognizer, and then all the necessary extras |
| 1710 | // to forward the results to the searchable activity |
| 1711 | Intent voiceIntent = new Intent(baseIntent); |
| 1712 | |
| 1713 | // Add all of the configuration options supplied by the searchable's metadata |
| 1714 | String languageModel = RecognizerIntent.LANGUAGE_MODEL_FREE_FORM; |
| 1715 | String prompt = null; |
| 1716 | String language = null; |
| 1717 | int maxResults = 1; |
| 1718 | |
| 1719 | Resources resources = getResources(); |
| 1720 | if (searchable.getVoiceLanguageModeId() != 0) { |
| 1721 | languageModel = resources.getString(searchable.getVoiceLanguageModeId()); |
| 1722 | } |
| 1723 | if (searchable.getVoicePromptTextId() != 0) { |
| 1724 | prompt = resources.getString(searchable.getVoicePromptTextId()); |
| 1725 | } |
| 1726 | if (searchable.getVoiceLanguageId() != 0) { |
| 1727 | language = resources.getString(searchable.getVoiceLanguageId()); |
| 1728 | } |
| 1729 | if (searchable.getVoiceMaxResults() != 0) { |
| 1730 | maxResults = searchable.getVoiceMaxResults(); |
| 1731 | } |
| 1732 | voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, languageModel); |
| 1733 | voiceIntent.putExtra(RecognizerIntent.EXTRA_PROMPT, prompt); |
| 1734 | voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, language); |
| 1735 | voiceIntent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, maxResults); |
| 1736 | voiceIntent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, searchActivity == null ? null |
| 1737 | : searchActivity.flattenToShortString()); |
| 1738 | |
| 1739 | // Add the values that configure forwarding the results |
| 1740 | voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT, pending); |
| 1741 | voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT_BUNDLE, queryExtras); |
| 1742 | |
| 1743 | return voiceIntent; |
| 1744 | } |
| 1745 | |
| 1746 | /** |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 1747 | * When a particular suggestion has been selected, perform the various lookups required |
| 1748 | * to use the suggestion. This includes checking the cursor for suggestion-specific data, |
| 1749 | * and/or falling back to the XML for defaults; It also creates REST style Uri data when |
| 1750 | * the suggestion includes a data id. |
| 1751 | * |
| 1752 | * @param c The suggestions cursor, moved to the row of the user's selection |
| 1753 | * @param actionKey The key code of the action key that was pressed, |
| 1754 | * or {@link KeyEvent#KEYCODE_UNKNOWN} if none. |
| 1755 | * @param actionMsg The message for the action key that was pressed, |
| 1756 | * or <code>null</code> if none. |
| 1757 | * @return An intent for the suggestion at the cursor's position. |
| 1758 | */ |
| 1759 | private Intent createIntentFromSuggestion(Cursor c, int actionKey, String actionMsg) { |
| 1760 | try { |
| 1761 | // use specific action if supplied, or default action if supplied, or fixed default |
| 1762 | String action = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_ACTION); |
| 1763 | |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 1764 | if (action == null) { |
| 1765 | action = mSearchable.getSuggestIntentAction(); |
| 1766 | } |
| 1767 | if (action == null) { |
| 1768 | action = Intent.ACTION_SEARCH; |
| 1769 | } |
| 1770 | |
| 1771 | // use specific data if supplied, or default data if supplied |
| 1772 | String data = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA); |
| 1773 | if (data == null) { |
| 1774 | data = mSearchable.getSuggestIntentData(); |
| 1775 | } |
| 1776 | // then, if an ID was provided, append it. |
| 1777 | if (data != null) { |
| 1778 | String id = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID); |
| 1779 | if (id != null) { |
| 1780 | data = data + "/" + Uri.encode(id); |
| 1781 | } |
| 1782 | } |
| 1783 | Uri dataUri = (data == null) ? null : Uri.parse(data); |
| 1784 | |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 1785 | String query = getColumnString(c, SearchManager.SUGGEST_COLUMN_QUERY); |
| 1786 | String extraData = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA); |
| 1787 | |
Amith Yamasani | e678f46 | 2010-09-15 16:13:43 -0700 | [diff] [blame] | 1788 | return createIntent(action, dataUri, extraData, query, actionKey, actionMsg); |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 1789 | } catch (RuntimeException e ) { |
| 1790 | int rowNum; |
| 1791 | try { // be really paranoid now |
| 1792 | rowNum = c.getPosition(); |
| 1793 | } catch (RuntimeException e2 ) { |
| 1794 | rowNum = -1; |
| 1795 | } |
Jake Wharton | 73af451 | 2012-07-06 23:15:44 -0700 | [diff] [blame] | 1796 | Log.w(LOG_TAG, "Search suggestions cursor at row " + rowNum + |
| 1797 | " returned exception.", e); |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 1798 | return null; |
| 1799 | } |
| 1800 | } |
| 1801 | |
Amith Yamasani | f28d187 | 2011-07-26 12:21:03 -0700 | [diff] [blame] | 1802 | private void forceSuggestionQuery() { |
Alan Viverette | cb8ed37 | 2014-11-18 17:05:35 -0800 | [diff] [blame] | 1803 | mSearchSrcTextView.doBeforeTextChanged(); |
| 1804 | mSearchSrcTextView.doAfterTextChanged(); |
Amith Yamasani | f28d187 | 2011-07-26 12:21:03 -0700 | [diff] [blame] | 1805 | } |
| 1806 | |
Amith Yamasani | 968ec93 | 2010-12-02 14:00:47 -0800 | [diff] [blame] | 1807 | static boolean isLandscapeMode(Context context) { |
| 1808 | return context.getResources().getConfiguration().orientation |
| 1809 | == Configuration.ORIENTATION_LANDSCAPE; |
| 1810 | } |
| 1811 | |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 1812 | /** |
| 1813 | * Callback to watch the text field for empty/non-empty |
| 1814 | */ |
| 1815 | private TextWatcher mTextWatcher = new TextWatcher() { |
| 1816 | |
| 1817 | public void beforeTextChanged(CharSequence s, int start, int before, int after) { } |
| 1818 | |
| 1819 | public void onTextChanged(CharSequence s, int start, |
| 1820 | int before, int after) { |
| 1821 | SearchView.this.onTextChanged(s); |
| 1822 | } |
| 1823 | |
| 1824 | public void afterTextChanged(Editable s) { |
| 1825 | } |
| 1826 | }; |
Amith Yamasani | 968ec93 | 2010-12-02 14:00:47 -0800 | [diff] [blame] | 1827 | |
Aurimas Liutikas | c8fd00a | 2016-04-15 13:55:53 -0700 | [diff] [blame] | 1828 | private static class UpdatableTouchDelegate extends TouchDelegate { |
| 1829 | /** |
| 1830 | * View that should receive forwarded touch events |
| 1831 | */ |
| 1832 | private final View mDelegateView; |
| 1833 | |
| 1834 | /** |
| 1835 | * Bounds in local coordinates of the containing view that should be mapped to the delegate |
| 1836 | * view. This rect is used for initial hit testing. |
| 1837 | */ |
| 1838 | private final Rect mTargetBounds; |
| 1839 | |
| 1840 | /** |
| 1841 | * Bounds in local coordinates of the containing view that are actual bounds of the delegate |
| 1842 | * view. This rect is used for event coordinate mapping. |
| 1843 | */ |
| 1844 | private final Rect mActualBounds; |
| 1845 | |
| 1846 | /** |
| 1847 | * mTargetBounds inflated to include some slop. This rect is to track whether the motion events |
| 1848 | * should be considered to be be within the delegate view. |
| 1849 | */ |
| 1850 | private final Rect mSlopBounds; |
| 1851 | |
| 1852 | private final int mSlop; |
| 1853 | |
| 1854 | /** |
| 1855 | * True if the delegate had been targeted on a down event (intersected mTargetBounds). |
| 1856 | */ |
| 1857 | private boolean mDelegateTargeted; |
| 1858 | |
| 1859 | public UpdatableTouchDelegate(Rect targetBounds, Rect actualBounds, View delegateView) { |
| 1860 | super(targetBounds, delegateView); |
| 1861 | mSlop = ViewConfiguration.get(delegateView.getContext()).getScaledTouchSlop(); |
| 1862 | mTargetBounds = new Rect(); |
| 1863 | mSlopBounds = new Rect(); |
| 1864 | mActualBounds = new Rect(); |
| 1865 | setBounds(targetBounds, actualBounds); |
| 1866 | mDelegateView = delegateView; |
| 1867 | } |
| 1868 | |
| 1869 | public void setBounds(Rect desiredBounds, Rect actualBounds) { |
| 1870 | mTargetBounds.set(desiredBounds); |
| 1871 | mSlopBounds.set(desiredBounds); |
| 1872 | mSlopBounds.inset(-mSlop, -mSlop); |
| 1873 | mActualBounds.set(actualBounds); |
| 1874 | } |
| 1875 | |
| 1876 | @Override |
| 1877 | public boolean onTouchEvent(MotionEvent event) { |
| 1878 | final int x = (int) event.getX(); |
| 1879 | final int y = (int) event.getY(); |
| 1880 | boolean sendToDelegate = false; |
| 1881 | boolean hit = true; |
| 1882 | boolean handled = false; |
| 1883 | |
| 1884 | switch (event.getAction()) { |
| 1885 | case MotionEvent.ACTION_DOWN: |
| 1886 | if (mTargetBounds.contains(x, y)) { |
| 1887 | mDelegateTargeted = true; |
| 1888 | sendToDelegate = true; |
| 1889 | } |
| 1890 | break; |
| 1891 | case MotionEvent.ACTION_UP: |
| 1892 | case MotionEvent.ACTION_MOVE: |
| 1893 | sendToDelegate = mDelegateTargeted; |
| 1894 | if (sendToDelegate) { |
| 1895 | if (!mSlopBounds.contains(x, y)) { |
| 1896 | hit = false; |
| 1897 | } |
| 1898 | } |
| 1899 | break; |
| 1900 | case MotionEvent.ACTION_CANCEL: |
| 1901 | sendToDelegate = mDelegateTargeted; |
| 1902 | mDelegateTargeted = false; |
| 1903 | break; |
| 1904 | } |
| 1905 | if (sendToDelegate) { |
| 1906 | if (hit && !mActualBounds.contains(x, y)) { |
| 1907 | // Offset event coordinates to be in the center of the target view since we |
| 1908 | // are within the targetBounds, but not inside the actual bounds of |
| 1909 | // mDelegateView |
| 1910 | event.setLocation(mDelegateView.getWidth() / 2, |
| 1911 | mDelegateView.getHeight() / 2); |
| 1912 | } else { |
| 1913 | // Offset event coordinates to the target view coordinates. |
| 1914 | event.setLocation(x - mActualBounds.left, y - mActualBounds.top); |
| 1915 | } |
| 1916 | |
| 1917 | handled = mDelegateView.dispatchTouchEvent(event); |
| 1918 | } |
| 1919 | return handled; |
| 1920 | } |
| 1921 | } |
| 1922 | |
Amith Yamasani | 968ec93 | 2010-12-02 14:00:47 -0800 | [diff] [blame] | 1923 | /** |
| 1924 | * Local subclass for AutoCompleteTextView. |
| 1925 | * @hide |
| 1926 | */ |
| 1927 | public static class SearchAutoComplete extends AutoCompleteTextView { |
| 1928 | |
| 1929 | private int mThreshold; |
| 1930 | private SearchView mSearchView; |
| 1931 | |
Yohei Yukawa | 5cfc1b4 | 2017-03-07 00:52:09 -0800 | [diff] [blame] | 1932 | private boolean mHasPendingShowSoftInputRequest; |
| 1933 | final Runnable mRunShowSoftInputIfNecessary = () -> showSoftInputIfNecessary(); |
| 1934 | |
Amith Yamasani | 968ec93 | 2010-12-02 14:00:47 -0800 | [diff] [blame] | 1935 | public SearchAutoComplete(Context context) { |
| 1936 | super(context); |
| 1937 | mThreshold = getThreshold(); |
| 1938 | } |
| 1939 | |
Mathew Inwood | 978c6e2 | 2018-08-21 15:58:55 +0100 | [diff] [blame] | 1940 | @UnsupportedAppUsage |
Amith Yamasani | 968ec93 | 2010-12-02 14:00:47 -0800 | [diff] [blame] | 1941 | public SearchAutoComplete(Context context, AttributeSet attrs) { |
| 1942 | super(context, attrs); |
| 1943 | mThreshold = getThreshold(); |
| 1944 | } |
| 1945 | |
Alan Viverette | 617feb9 | 2013-09-09 18:09:13 -0700 | [diff] [blame] | 1946 | public SearchAutoComplete(Context context, AttributeSet attrs, int defStyleAttrs) { |
| 1947 | super(context, attrs, defStyleAttrs); |
| 1948 | mThreshold = getThreshold(); |
| 1949 | } |
| 1950 | |
| 1951 | public SearchAutoComplete( |
| 1952 | Context context, AttributeSet attrs, int defStyleAttrs, int defStyleRes) { |
| 1953 | super(context, attrs, defStyleAttrs, defStyleRes); |
Amith Yamasani | 968ec93 | 2010-12-02 14:00:47 -0800 | [diff] [blame] | 1954 | mThreshold = getThreshold(); |
| 1955 | } |
| 1956 | |
Filip Gruszczynski | b635fda | 2015-12-03 18:37:38 -0800 | [diff] [blame] | 1957 | @Override |
| 1958 | protected void onFinishInflate() { |
| 1959 | super.onFinishInflate(); |
| 1960 | DisplayMetrics metrics = getResources().getDisplayMetrics(); |
| 1961 | setMinWidth((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, |
| 1962 | getSearchViewTextMinWidthDp(), metrics)); |
| 1963 | } |
| 1964 | |
Amith Yamasani | 968ec93 | 2010-12-02 14:00:47 -0800 | [diff] [blame] | 1965 | void setSearchView(SearchView searchView) { |
| 1966 | mSearchView = searchView; |
| 1967 | } |
| 1968 | |
| 1969 | @Override |
| 1970 | public void setThreshold(int threshold) { |
| 1971 | super.setThreshold(threshold); |
| 1972 | mThreshold = threshold; |
| 1973 | } |
| 1974 | |
| 1975 | /** |
| 1976 | * Returns true if the text field is empty, or contains only whitespace. |
| 1977 | */ |
| 1978 | private boolean isEmpty() { |
| 1979 | return TextUtils.getTrimmedLength(getText()) == 0; |
| 1980 | } |
| 1981 | |
| 1982 | /** |
| 1983 | * We override this method to avoid replacing the query box text when a |
| 1984 | * suggestion is clicked. |
| 1985 | */ |
| 1986 | @Override |
| 1987 | protected void replaceText(CharSequence text) { |
| 1988 | } |
| 1989 | |
| 1990 | /** |
| 1991 | * We override this method to avoid an extra onItemClick being called on |
| 1992 | * the drop-down's OnItemClickListener by |
| 1993 | * {@link AutoCompleteTextView#onKeyUp(int, KeyEvent)} when an item is |
| 1994 | * clicked with the trackball. |
| 1995 | */ |
| 1996 | @Override |
| 1997 | public void performCompletion() { |
| 1998 | } |
| 1999 | |
| 2000 | /** |
| 2001 | * We override this method to be sure and show the soft keyboard if |
| 2002 | * appropriate when the TextView has focus. |
| 2003 | */ |
| 2004 | @Override |
| 2005 | public void onWindowFocusChanged(boolean hasWindowFocus) { |
| 2006 | super.onWindowFocusChanged(hasWindowFocus); |
| 2007 | |
Amith Yamasani | acd8d2d | 2010-12-06 15:50:23 -0800 | [diff] [blame] | 2008 | if (hasWindowFocus && mSearchView.hasFocus() && getVisibility() == VISIBLE) { |
Yohei Yukawa | 5cfc1b4 | 2017-03-07 00:52:09 -0800 | [diff] [blame] | 2009 | // Since InputMethodManager#onPostWindowFocus() will be called after this callback, |
| 2010 | // it is a bit too early to call InputMethodManager#showSoftInput() here. We still |
| 2011 | // need to wait until the system calls back onCreateInputConnection() to call |
| 2012 | // InputMethodManager#showSoftInput(). |
| 2013 | mHasPendingShowSoftInputRequest = true; |
| 2014 | |
| 2015 | // If in landscape mode, then make sure that the ime is in front of the dropdown. |
Amith Yamasani | 968ec93 | 2010-12-02 14:00:47 -0800 | [diff] [blame] | 2016 | if (isLandscapeMode(getContext())) { |
| 2017 | ensureImeVisible(true); |
| 2018 | } |
| 2019 | } |
| 2020 | } |
| 2021 | |
Amith Yamasani | 4aedb39 | 2010-12-15 16:04:57 -0800 | [diff] [blame] | 2022 | @Override |
| 2023 | protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { |
| 2024 | super.onFocusChanged(focused, direction, previouslyFocusedRect); |
| 2025 | mSearchView.onTextFocusChanged(); |
| 2026 | } |
| 2027 | |
Amith Yamasani | 968ec93 | 2010-12-02 14:00:47 -0800 | [diff] [blame] | 2028 | /** |
| 2029 | * We override this method so that we can allow a threshold of zero, |
| 2030 | * which ACTV does not. |
| 2031 | */ |
| 2032 | @Override |
| 2033 | public boolean enoughToFilter() { |
| 2034 | return mThreshold <= 0 || super.enoughToFilter(); |
| 2035 | } |
Amith Yamasani | b4569fb | 2011-07-08 15:25:39 -0700 | [diff] [blame] | 2036 | |
| 2037 | @Override |
| 2038 | public boolean onKeyPreIme(int keyCode, KeyEvent event) { |
Evan Rosky | 758a652 | 2018-03-06 19:11:12 -0800 | [diff] [blame] | 2039 | final boolean consume = super.onKeyPreIme(keyCode, event); |
| 2040 | if (consume && keyCode == KeyEvent.KEYCODE_BACK |
| 2041 | && event.getAction() == KeyEvent.ACTION_UP) { |
| 2042 | // If AutoCompleteTextView closed its pop-up, it will return true, in which case |
| 2043 | // we should also close the IME. Otherwise, the popup is already closed and we can |
| 2044 | // leave the BACK event alone. |
| 2045 | setImeVisibility(false); |
Amith Yamasani | b4569fb | 2011-07-08 15:25:39 -0700 | [diff] [blame] | 2046 | } |
Evan Rosky | 758a652 | 2018-03-06 19:11:12 -0800 | [diff] [blame] | 2047 | return consume; |
Amith Yamasani | b4569fb | 2011-07-08 15:25:39 -0700 | [diff] [blame] | 2048 | } |
| 2049 | |
Filip Gruszczynski | b635fda | 2015-12-03 18:37:38 -0800 | [diff] [blame] | 2050 | /** |
| 2051 | * Get minimum width of the search view text entry area. |
| 2052 | */ |
| 2053 | private int getSearchViewTextMinWidthDp() { |
| 2054 | final Configuration configuration = getResources().getConfiguration(); |
| 2055 | final int width = configuration.screenWidthDp; |
| 2056 | final int height = configuration.screenHeightDp; |
| 2057 | final int orientation = configuration.orientation; |
| 2058 | if (width >= 960 && height >= 720 |
| 2059 | && orientation == Configuration.ORIENTATION_LANDSCAPE) { |
| 2060 | return 256; |
| 2061 | } else if (width >= 600 || (width >= 640 && height >= 480)) { |
| 2062 | return 192; |
| 2063 | }; |
| 2064 | return 160; |
| 2065 | } |
Yohei Yukawa | 5cfc1b4 | 2017-03-07 00:52:09 -0800 | [diff] [blame] | 2066 | |
| 2067 | /** |
| 2068 | * We override {@link View#onCreateInputConnection(EditorInfo)} as a signal to schedule a |
| 2069 | * pending {@link InputMethodManager#showSoftInput(View, int)} request (if any). |
| 2070 | */ |
| 2071 | @Override |
| 2072 | public InputConnection onCreateInputConnection(EditorInfo editorInfo) { |
| 2073 | final InputConnection ic = super.onCreateInputConnection(editorInfo); |
| 2074 | if (mHasPendingShowSoftInputRequest) { |
| 2075 | removeCallbacks(mRunShowSoftInputIfNecessary); |
| 2076 | post(mRunShowSoftInputIfNecessary); |
| 2077 | } |
| 2078 | return ic; |
| 2079 | } |
| 2080 | |
Tarandeep Singh | af91ab7 | 2019-02-14 10:32:00 -0800 | [diff] [blame] | 2081 | @Override |
| 2082 | public boolean checkInputConnectionProxy(View view) { |
| 2083 | return view == mSearchView; |
| 2084 | } |
| 2085 | |
Yohei Yukawa | 5cfc1b4 | 2017-03-07 00:52:09 -0800 | [diff] [blame] | 2086 | private void showSoftInputIfNecessary() { |
| 2087 | if (mHasPendingShowSoftInputRequest) { |
| 2088 | final InputMethodManager imm = |
| 2089 | getContext().getSystemService(InputMethodManager.class); |
| 2090 | imm.showSoftInput(this, 0); |
| 2091 | mHasPendingShowSoftInputRequest = false; |
| 2092 | } |
| 2093 | } |
| 2094 | |
| 2095 | private void setImeVisibility(final boolean visible) { |
| 2096 | final InputMethodManager imm = getContext().getSystemService(InputMethodManager.class); |
| 2097 | if (!visible) { |
| 2098 | mHasPendingShowSoftInputRequest = false; |
| 2099 | removeCallbacks(mRunShowSoftInputIfNecessary); |
| 2100 | imm.hideSoftInputFromWindow(getWindowToken(), 0); |
| 2101 | return; |
| 2102 | } |
| 2103 | |
| 2104 | if (imm.isActive(this)) { |
| 2105 | // This means that SearchAutoComplete is already connected to the IME. |
| 2106 | // InputMethodManager#showSoftInput() is guaranteed to pass client-side focus check. |
| 2107 | mHasPendingShowSoftInputRequest = false; |
| 2108 | removeCallbacks(mRunShowSoftInputIfNecessary); |
| 2109 | imm.showSoftInput(this, 0); |
| 2110 | return; |
| 2111 | } |
| 2112 | |
| 2113 | // Otherwise, InputMethodManager#showSoftInput() should be deferred after |
| 2114 | // onCreateInputConnection(). |
| 2115 | mHasPendingShowSoftInputRequest = true; |
| 2116 | } |
Amith Yamasani | 968ec93 | 2010-12-02 14:00:47 -0800 | [diff] [blame] | 2117 | } |
Amith Yamasani | 0594476 | 2010-10-08 13:52:38 -0700 | [diff] [blame] | 2118 | } |