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