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