blob: 89d9e97bddadeec036610f94739526240d08349c [file] [log] [blame]
Amith Yamasani733cbd52010-09-03 12:21:39 -07001/*
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
17package android.widget;
18
19import static android.widget.SuggestionsAdapter.getColumnString;
20
Alan Viveretteb4004df2015-04-29 16:55:42 -070021import android.annotation.Nullable;
Mathew Inwood978c6e22018-08-21 15:58:55 +010022import android.annotation.UnsupportedAppUsage;
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -070023import android.app.PendingIntent;
Amith Yamasani733cbd52010-09-03 12:21:39 -070024import android.app.SearchManager;
25import android.app.SearchableInfo;
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -070026import android.content.ActivityNotFoundException;
27import android.content.ComponentName;
Amith Yamasani733cbd52010-09-03 12:21:39 -070028import android.content.Context;
29import android.content.Intent;
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -070030import android.content.pm.PackageManager;
31import android.content.pm.ResolveInfo;
Amith Yamasani968ec932010-12-02 14:00:47 -080032import android.content.res.Configuration;
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -070033import android.content.res.Resources;
Amith Yamasani733cbd52010-09-03 12:21:39 -070034import android.content.res.TypedArray;
35import android.database.Cursor;
repo sync6a81b822010-09-28 13:00:05 -070036import android.graphics.Rect;
Amith Yamasani733cbd52010-09-03 12:21:39 -070037import android.graphics.drawable.Drawable;
38import android.net.Uri;
Mathew Inwood8c854f82018-09-14 12:35:36 +010039import android.os.Build;
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -070040import android.os.Bundle;
Aurimas Liutikas13fdea02016-02-11 10:10:10 -080041import android.os.Parcel;
42import android.os.Parcelable;
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -070043import android.speech.RecognizerIntent;
Amith Yamasani733cbd52010-09-03 12:21:39 -070044import android.text.Editable;
Amith Yamasani5607a382011-08-09 14:16:37 -070045import android.text.InputType;
Amith Yamasanib4569fb2011-07-08 15:25:39 -070046import android.text.Spannable;
47import android.text.SpannableStringBuilder;
Amith Yamasani733cbd52010-09-03 12:21:39 -070048import android.text.TextUtils;
49import android.text.TextWatcher;
Amith Yamasanib4569fb2011-07-08 15:25:39 -070050import android.text.style.ImageSpan;
Amith Yamasani733cbd52010-09-03 12:21:39 -070051import android.util.AttributeSet;
Filip Gruszczynskib635fda2015-12-03 18:37:38 -080052import android.util.DisplayMetrics;
Amith Yamasani733cbd52010-09-03 12:21:39 -070053import android.util.Log;
Filip Gruszczynskib635fda2015-12-03 18:37:38 -080054import android.util.TypedValue;
Amith Yamasani763bc072011-07-22 11:53:47 -070055import android.view.CollapsibleActionView;
Amith Yamasani733cbd52010-09-03 12:21:39 -070056import android.view.KeyEvent;
57import android.view.LayoutInflater;
Aurimas Liutikasc8fd00a2016-04-15 13:55:53 -070058import android.view.MotionEvent;
59import android.view.TouchDelegate;
Amith Yamasani733cbd52010-09-03 12:21:39 -070060import android.view.View;
Aurimas Liutikasc8fd00a2016-04-15 13:55:53 -070061import android.view.ViewConfiguration;
Amith Yamasani5607a382011-08-09 14:16:37 -070062import android.view.inputmethod.EditorInfo;
Yohei Yukawa5cfc1b42017-03-07 00:52:09 -080063import android.view.inputmethod.InputConnection;
Amith Yamasani733cbd52010-09-03 12:21:39 -070064import android.view.inputmethod.InputMethodManager;
Ashley Rose55f9f922019-01-28 19:29:36 -050065import android.view.inspector.InspectableProperty;
Amith Yamasani733cbd52010-09-03 12:21:39 -070066import android.widget.AdapterView.OnItemClickListener;
67import android.widget.AdapterView.OnItemSelectedListener;
68import android.widget.TextView.OnEditorActionListener;
69
Amith Yamasanib4569fb2011-07-08 15:25:39 -070070import com.android.internal.R;
71
Amith Yamasani733cbd52010-09-03 12:21:39 -070072import java.util.WeakHashMap;
73
74/**
Amith Yamasani763bc072011-07-22 11:53:47 -070075 * A widget that provides a user interface for the user to enter a search query and submit a request
76 * to a search provider. Shows a list of query suggestions or results, if available, and allows the
77 * user to pick a suggestion or result to launch into.
Amith Yamasani5931b1f2010-10-18 16:13:14 -070078 *
Amith Yamasani763bc072011-07-22 11:53:47 -070079 * <p>
80 * When the SearchView is used in an ActionBar as an action view for a collapsible menu item, it
81 * needs to be set to iconified by default using {@link #setIconifiedByDefault(boolean)
82 * setIconifiedByDefault(true)}. This is the default, so nothing needs to be done.
83 * </p>
84 * <p>
85 * If you want the search field to always be visible, then call setIconifiedByDefault(false).
86 * </p>
Amith Yamasani5931b1f2010-10-18 16:13:14 -070087 *
Joe Fernandez3aef8e1d2011-12-20 10:38:34 -080088 * <div class="special reference">
89 * <h3>Developer Guides</h3>
90 * <p>For information about using {@code SearchView}, read the
91 * <a href="{@docRoot}guide/topics/search/index.html">Search</a> developer guide.</p>
92 * </div>
Amith Yamasani763bc072011-07-22 11:53:47 -070093 *
94 * @see android.view.MenuItem#SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW
Amith Yamasani5931b1f2010-10-18 16:13:14 -070095 * @attr ref android.R.styleable#SearchView_iconifiedByDefault
Amith Yamasani5607a382011-08-09 14:16:37 -070096 * @attr ref android.R.styleable#SearchView_imeOptions
97 * @attr ref android.R.styleable#SearchView_inputType
Amith Yamasani5931b1f2010-10-18 16:13:14 -070098 * @attr ref android.R.styleable#SearchView_maxWidth
Scott Mainabdf0d52011-02-08 10:20:27 -080099 * @attr ref android.R.styleable#SearchView_queryHint
Amith Yamasani733cbd52010-09-03 12:21:39 -0700100 */
Amith Yamasani763bc072011-07-22 11:53:47 -0700101public class SearchView extends LinearLayout implements CollapsibleActionView {
Amith Yamasani733cbd52010-09-03 12:21:39 -0700102
103 private static final boolean DBG = false;
104 private static final String LOG_TAG = "SearchView";
105
Luca Zanolin535698c2011-10-06 13:36:15 +0100106 /**
107 * Private constant for removing the microphone in the keyboard.
108 */
109 private static final String IME_OPTION_NO_MICROPHONE = "nm";
110
Mathew Inwood978c6e22018-08-21 15:58:55 +0100111 @UnsupportedAppUsage
Alan Viverettecb8ed372014-11-18 17:05:35 -0800112 private final SearchAutoComplete mSearchSrcTextView;
Mathew Inwood978c6e22018-08-21 15:58:55 +0100113 @UnsupportedAppUsage
Alan Viverette5dddb702014-07-02 15:46:04 -0700114 private final View mSearchEditFrame;
Mathew Inwood978c6e22018-08-21 15:58:55 +0100115 @UnsupportedAppUsage
Alan Viverette5dddb702014-07-02 15:46:04 -0700116 private final View mSearchPlate;
Mathew Inwood978c6e22018-08-21 15:58:55 +0100117 @UnsupportedAppUsage
Alan Viverette5dddb702014-07-02 15:46:04 -0700118 private final View mSubmitArea;
Mathew Inwood978c6e22018-08-21 15:58:55 +0100119 @UnsupportedAppUsage
Alan Viverette5dddb702014-07-02 15:46:04 -0700120 private final ImageView mSearchButton;
Alan Viverettecb8ed372014-11-18 17:05:35 -0800121 private final ImageView mGoButton;
Mathew Inwood978c6e22018-08-21 15:58:55 +0100122 @UnsupportedAppUsage
Alan Viverette5dddb702014-07-02 15:46:04 -0700123 private final ImageView mCloseButton;
Mathew Inwood978c6e22018-08-21 15:58:55 +0100124 @UnsupportedAppUsage
Alan Viverette5dddb702014-07-02 15:46:04 -0700125 private final ImageView mVoiceButton;
Alan Viverette5dddb702014-07-02 15:46:04 -0700126 private final View mDropDownAnchor;
Alan Viverettecb8ed372014-11-18 17:05:35 -0800127
Aurimas Liutikasc8fd00a2016-04-15 13:55:53 -0700128 private UpdatableTouchDelegate mTouchDelegate;
129 private Rect mSearchSrcTextViewBounds = new Rect();
130 private Rect mSearchSrtTextViewBoundsExpanded = new Rect();
131 private int[] mTemp = new int[2];
132 private int[] mTemp2 = new int[2];
133
Alan Viverettecb8ed372014-11-18 17:05:35 -0800134 /** Icon optionally displayed when the SearchView is collapsed. */
135 private final ImageView mCollapsedIcon;
136
137 /** Drawable used as an EditText hint. */
Mathew Inwood978c6e22018-08-21 15:58:55 +0100138 @UnsupportedAppUsage
Alan Viverettecb8ed372014-11-18 17:05:35 -0800139 private final Drawable mSearchHintIcon;
Alan Viverette5dddb702014-07-02 15:46:04 -0700140
141 // Resources used by SuggestionsAdapter to display suggestions.
142 private final int mSuggestionRowLayout;
143 private final int mSuggestionCommitIconResId;
144
145 // Intents used for voice searching.
146 private final Intent mVoiceWebSearchIntent;
147 private final Intent mVoiceAppSearchIntent;
148
Alan Viveretteb4004df2015-04-29 16:55:42 -0700149 private final CharSequence mDefaultQueryHint;
150
Mathew Inwood978c6e22018-08-21 15:58:55 +0100151 @UnsupportedAppUsage
Adam Powell01f21352011-01-20 18:30:10 -0800152 private OnQueryTextListener mOnQueryChangeListener;
Amith Yamasani733cbd52010-09-03 12:21:39 -0700153 private OnCloseListener mOnCloseListener;
Amith Yamasani05944762010-10-08 13:52:38 -0700154 private OnFocusChangeListener mOnQueryTextFocusChangeListener;
Adam Powell01f21352011-01-20 18:30:10 -0800155 private OnSuggestionListener mOnSuggestionListener;
Amith Yamasani48385482010-12-03 14:43:52 -0800156 private OnClickListener mOnSearchClickListener;
Amith Yamasani733cbd52010-09-03 12:21:39 -0700157
Mathew Inwood978c6e22018-08-21 15:58:55 +0100158 @UnsupportedAppUsage
Amith Yamasani733cbd52010-09-03 12:21:39 -0700159 private boolean mIconifiedByDefault;
Mathew Inwood978c6e22018-08-21 15:58:55 +0100160 @UnsupportedAppUsage
Amith Yamasani93227752010-09-14 10:10:54 -0700161 private boolean mIconified;
Mathew Inwood978c6e22018-08-21 15:58:55 +0100162 @UnsupportedAppUsage
Amith Yamasani733cbd52010-09-03 12:21:39 -0700163 private CursorAdapter mSuggestionsAdapter;
Amith Yamasani733cbd52010-09-03 12:21:39 -0700164 private boolean mSubmitButtonEnabled;
165 private CharSequence mQueryHint;
Amith Yamasanie678f462010-09-15 16:13:43 -0700166 private boolean mQueryRefinement;
Mathew Inwood978c6e22018-08-21 15:58:55 +0100167 @UnsupportedAppUsage
Amith Yamasani05944762010-10-08 13:52:38 -0700168 private boolean mClearingFocus;
Amith Yamasani5931b1f2010-10-18 16:13:14 -0700169 private int mMaxWidth;
Mathew Inwood978c6e22018-08-21 15:58:55 +0100170 @UnsupportedAppUsage
Amith Yamasani9b2e3022011-01-14 11:34:12 -0800171 private boolean mVoiceButtonEnabled;
Amith Yamasanib47c4fd2011-08-04 14:30:07 -0700172 private CharSequence mOldQueryText;
Mathew Inwood978c6e22018-08-21 15:58:55 +0100173 @UnsupportedAppUsage
Amith Yamasani068d73c2011-05-27 15:15:14 -0700174 private CharSequence mUserQuery;
Mathew Inwood978c6e22018-08-21 15:58:55 +0100175 @UnsupportedAppUsage
Amith Yamasani763bc072011-07-22 11:53:47 -0700176 private boolean mExpandedInActionView;
Mathew Inwood978c6e22018-08-21 15:58:55 +0100177 @UnsupportedAppUsage
Adam Powell53f56c42011-09-25 13:46:15 -0700178 private int mCollapsedImeOptions;
Amith Yamasani733cbd52010-09-03 12:21:39 -0700179
180 private SearchableInfo mSearchable;
Amith Yamasani940ef382011-03-02 18:43:23 -0800181 private Bundle mAppSearchData;
Amith Yamasani733cbd52010-09-03 12:21:39 -0700182
Amith Yamasania95e4882011-08-17 11:41:37 -0700183 private Runnable mUpdateDrawableStateRunnable = new Runnable() {
184 public void run() {
185 updateFocusedState();
186 }
187 };
188
Amith Yamasani87907642011-11-03 11:32:44 -0700189 private Runnable mReleaseCursorRunnable = new Runnable() {
190 public void run() {
191 if (mSuggestionsAdapter != null && mSuggestionsAdapter instanceof SuggestionsAdapter) {
192 mSuggestionsAdapter.changeCursor(null);
193 }
194 }
195 };
196
Amith Yamasani733cbd52010-09-03 12:21:39 -0700197 // A weak map of drawables we've gotten from other packages, so we don't load them
198 // more than once.
199 private final WeakHashMap<String, Drawable.ConstantState> mOutsideDrawablesCache =
200 new WeakHashMap<String, Drawable.ConstantState>();
201
202 /**
203 * Callbacks for changes to the query text.
204 */
Adam Powell01f21352011-01-20 18:30:10 -0800205 public interface OnQueryTextListener {
Amith Yamasani733cbd52010-09-03 12:21:39 -0700206
207 /**
208 * Called when the user submits the query. This could be due to a key press on the
209 * keyboard or due to pressing a submit button.
210 * The listener can override the standard behavior by returning true
211 * to indicate that it has handled the submit request. Otherwise return false to
212 * let the SearchView handle the submission by launching any associated intent.
213 *
214 * @param query the query text that is to be submitted
215 *
216 * @return true if the query has been handled by the listener, false to let the
217 * SearchView perform the default action.
218 */
Adam Powell01f21352011-01-20 18:30:10 -0800219 boolean onQueryTextSubmit(String query);
Amith Yamasani733cbd52010-09-03 12:21:39 -0700220
221 /**
222 * Called when the query text is changed by the user.
223 *
224 * @param newText the new content of the query text field.
225 *
226 * @return false if the SearchView should perform the default action of showing any
227 * suggestions if available, true if the action was handled by the listener.
228 */
Adam Powell01f21352011-01-20 18:30:10 -0800229 boolean onQueryTextChange(String newText);
Amith Yamasani733cbd52010-09-03 12:21:39 -0700230 }
231
232 public interface OnCloseListener {
233
234 /**
235 * The user is attempting to close the SearchView.
236 *
237 * @return true if the listener wants to override the default behavior of clearing the
238 * text field and dismissing it, false otherwise.
239 */
240 boolean onClose();
241 }
242
Amith Yamasani05944762010-10-08 13:52:38 -0700243 /**
244 * Callback interface for selection events on suggestions. These callbacks
245 * are only relevant when a SearchableInfo has been specified by {@link #setSearchableInfo}.
246 */
Adam Powell01f21352011-01-20 18:30:10 -0800247 public interface OnSuggestionListener {
Amith Yamasani05944762010-10-08 13:52:38 -0700248
249 /**
250 * Called when a suggestion was selected by navigating to it.
251 * @param position the absolute position in the list of suggestions.
252 *
253 * @return true if the listener handles the event and wants to override the default
254 * behavior of possibly rewriting the query based on the selected item, false otherwise.
255 */
Adam Powell01f21352011-01-20 18:30:10 -0800256 boolean onSuggestionSelect(int position);
Amith Yamasani05944762010-10-08 13:52:38 -0700257
258 /**
259 * Called when a suggestion was clicked.
260 * @param position the absolute position of the clicked item in the list of suggestions.
261 *
262 * @return true if the listener handles the event and wants to override the default
263 * behavior of launching any intent or submitting a search query specified on that item.
264 * Return false otherwise.
265 */
Adam Powell01f21352011-01-20 18:30:10 -0800266 boolean onSuggestionClick(int position);
Amith Yamasani05944762010-10-08 13:52:38 -0700267 }
268
Amith Yamasani733cbd52010-09-03 12:21:39 -0700269 public SearchView(Context context) {
270 this(context, null);
271 }
272
273 public SearchView(Context context, AttributeSet attrs) {
Alan Viverette5dddb702014-07-02 15:46:04 -0700274 this(context, attrs, R.attr.searchViewStyle);
Alan Viverette617feb92013-09-09 18:09:13 -0700275 }
276
277 public SearchView(Context context, AttributeSet attrs, int defStyleAttr) {
278 this(context, attrs, defStyleAttr, 0);
279 }
280
281 public SearchView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
282 super(context, attrs, defStyleAttr, defStyleRes);
Amith Yamasani733cbd52010-09-03 12:21:39 -0700283
Alan Viverette5dddb702014-07-02 15:46:04 -0700284 final TypedArray a = context.obtainStyledAttributes(
285 attrs, R.styleable.SearchView, defStyleAttr, defStyleRes);
Aurimas Liutikasab324cf2019-02-07 16:46:38 -0800286 saveAttributeDataForStyleable(context, R.styleable.SearchView,
287 attrs, a, defStyleAttr, defStyleRes);
Alan Viverette5dddb702014-07-02 15:46:04 -0700288 final LayoutInflater inflater = (LayoutInflater) context.getSystemService(
289 Context.LAYOUT_INFLATER_SERVICE);
Alan Viverettecb8ed372014-11-18 17:05:35 -0800290 final int layoutResId = a.getResourceId(
291 R.styleable.SearchView_layout, R.layout.search_view);
Alan Viverette5dddb702014-07-02 15:46:04 -0700292 inflater.inflate(layoutResId, this, true);
Amith Yamasani733cbd52010-09-03 12:21:39 -0700293
Alan Viverettecb8ed372014-11-18 17:05:35 -0800294 mSearchSrcTextView = (SearchAutoComplete) findViewById(R.id.search_src_text);
295 mSearchSrcTextView.setSearchView(this);
Amith Yamasani968ec932010-12-02 14:00:47 -0800296
Amith Yamasani733cbd52010-09-03 12:21:39 -0700297 mSearchEditFrame = findViewById(R.id.search_edit_frame);
Amith Yamasani79f74302011-03-08 14:16:35 -0800298 mSearchPlate = findViewById(R.id.search_plate);
Amith Yamasani9b2e3022011-01-14 11:34:12 -0800299 mSubmitArea = findViewById(R.id.submit_area);
Alan Viverette5dddb702014-07-02 15:46:04 -0700300 mSearchButton = (ImageView) findViewById(R.id.search_button);
Alan Viverettecb8ed372014-11-18 17:05:35 -0800301 mGoButton = (ImageView) findViewById(R.id.search_go_btn);
Amith Yamasani4aedb392010-12-15 16:04:57 -0800302 mCloseButton = (ImageView) findViewById(R.id.search_close_btn);
Alan Viverette5dddb702014-07-02 15:46:04 -0700303 mVoiceButton = (ImageView) findViewById(R.id.search_voice_btn);
Alan Viverettecb8ed372014-11-18 17:05:35 -0800304 mCollapsedIcon = (ImageView) findViewById(R.id.search_mag_icon);
Amith Yamasani733cbd52010-09-03 12:21:39 -0700305
Alan Viverette5dddb702014-07-02 15:46:04 -0700306 // Set up icons and backgrounds.
307 mSearchPlate.setBackground(a.getDrawable(R.styleable.SearchView_queryBackground));
308 mSubmitArea.setBackground(a.getDrawable(R.styleable.SearchView_submitBackground));
Alan Viverettecb8ed372014-11-18 17:05:35 -0800309 mSearchButton.setImageDrawable(a.getDrawable(R.styleable.SearchView_searchIcon));
310 mGoButton.setImageDrawable(a.getDrawable(R.styleable.SearchView_goIcon));
Alan Viverette5dddb702014-07-02 15:46:04 -0700311 mCloseButton.setImageDrawable(a.getDrawable(R.styleable.SearchView_closeIcon));
312 mVoiceButton.setImageDrawable(a.getDrawable(R.styleable.SearchView_voiceIcon));
Alan Viverettecb8ed372014-11-18 17:05:35 -0800313 mCollapsedIcon.setImageDrawable(a.getDrawable(R.styleable.SearchView_searchIcon));
314
315 // Prior to L MR1, the search hint icon defaulted to searchIcon. If the
316 // style does not have an explicit value set, fall back to that.
317 if (a.hasValueOrEmpty(R.styleable.SearchView_searchHintIcon)) {
318 mSearchHintIcon = a.getDrawable(R.styleable.SearchView_searchHintIcon);
319 } else {
320 mSearchHintIcon = a.getDrawable(R.styleable.SearchView_searchIcon);
321 }
Alan Viverette5dddb702014-07-02 15:46:04 -0700322
323 // Extract dropdown layout resource IDs for later use.
Alan Viverette362f9842014-09-10 16:05:35 -0700324 mSuggestionRowLayout = a.getResourceId(R.styleable.SearchView_suggestionRowLayout,
325 R.layout.search_dropdown_item_icons_2line);
Alan Viverette5dddb702014-07-02 15:46:04 -0700326 mSuggestionCommitIconResId = a.getResourceId(R.styleable.SearchView_commitIcon, 0);
327
Amith Yamasani733cbd52010-09-03 12:21:39 -0700328 mSearchButton.setOnClickListener(mOnClickListener);
329 mCloseButton.setOnClickListener(mOnClickListener);
Alan Viverettecb8ed372014-11-18 17:05:35 -0800330 mGoButton.setOnClickListener(mOnClickListener);
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -0700331 mVoiceButton.setOnClickListener(mOnClickListener);
Alan Viverettecb8ed372014-11-18 17:05:35 -0800332 mSearchSrcTextView.setOnClickListener(mOnClickListener);
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -0700333
Alan Viverettecb8ed372014-11-18 17:05:35 -0800334 mSearchSrcTextView.addTextChangedListener(mTextWatcher);
335 mSearchSrcTextView.setOnEditorActionListener(mOnEditorActionListener);
336 mSearchSrcTextView.setOnItemClickListener(mOnItemClickListener);
337 mSearchSrcTextView.setOnItemSelectedListener(mOnItemSelectedListener);
338 mSearchSrcTextView.setOnKeyListener(mTextKeyListener);
Alan Viverette5dddb702014-07-02 15:46:04 -0700339
Luca Zanolin535698c2011-10-06 13:36:15 +0100340 // Inform any listener of focus changes
Alan Viverettecb8ed372014-11-18 17:05:35 -0800341 mSearchSrcTextView.setOnFocusChangeListener(new OnFocusChangeListener() {
Amith Yamasani05944762010-10-08 13:52:38 -0700342
343 public void onFocusChange(View v, boolean hasFocus) {
344 if (mOnQueryTextFocusChangeListener != null) {
345 mOnQueryTextFocusChangeListener.onFocusChange(SearchView.this, hasFocus);
346 }
347 }
348 });
Amith Yamasani733cbd52010-09-03 12:21:39 -0700349 setIconifiedByDefault(a.getBoolean(R.styleable.SearchView_iconifiedByDefault, true));
Alan Viverette5dddb702014-07-02 15:46:04 -0700350
351 final int maxWidth = a.getDimensionPixelSize(R.styleable.SearchView_maxWidth, -1);
Amith Yamasani5931b1f2010-10-18 16:13:14 -0700352 if (maxWidth != -1) {
353 setMaxWidth(maxWidth);
354 }
Alan Viverette5dddb702014-07-02 15:46:04 -0700355
Alan Viveretteb4004df2015-04-29 16:55:42 -0700356 mDefaultQueryHint = a.getText(R.styleable.SearchView_defaultQueryHint);
357 mQueryHint = a.getText(R.styleable.SearchView_queryHint);
Alan Viverette5dddb702014-07-02 15:46:04 -0700358
359 final int imeOptions = a.getInt(R.styleable.SearchView_imeOptions, -1);
Amith Yamasani5607a382011-08-09 14:16:37 -0700360 if (imeOptions != -1) {
361 setImeOptions(imeOptions);
362 }
Alan Viverette5dddb702014-07-02 15:46:04 -0700363
364 final int inputType = a.getInt(R.styleable.SearchView_inputType, -1);
Amith Yamasani5607a382011-08-09 14:16:37 -0700365 if (inputType != -1) {
366 setInputType(inputType);
367 }
368
Evan Rosky4c8c9632016-12-16 17:27:55 -0800369 if (getFocusable() == FOCUSABLE_AUTO) {
370 setFocusable(FOCUSABLE);
371 }
Adam Powellea4ecd62014-09-03 19:35:37 -0700372
Amith Yamasani733cbd52010-09-03 12:21:39 -0700373 a.recycle();
374
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -0700375 // Save voice intent for later queries/launching
376 mVoiceWebSearchIntent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH);
377 mVoiceWebSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
378 mVoiceWebSearchIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
379 RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH);
380
381 mVoiceAppSearchIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
382 mVoiceAppSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
383
Alan Viverettecb8ed372014-11-18 17:05:35 -0800384 mDropDownAnchor = findViewById(mSearchSrcTextView.getDropDownAnchor());
Amith Yamasanib4569fb2011-07-08 15:25:39 -0700385 if (mDropDownAnchor != null) {
386 mDropDownAnchor.addOnLayoutChangeListener(new OnLayoutChangeListener() {
387 @Override
388 public void onLayoutChange(View v, int left, int top, int right, int bottom,
389 int oldLeft, int oldTop, int oldRight, int oldBottom) {
390 adjustDropDownSizeAndPosition();
391 }
Amith Yamasanib4569fb2011-07-08 15:25:39 -0700392 });
393 }
394
Amith Yamasani733cbd52010-09-03 12:21:39 -0700395 updateViewsVisibility(mIconifiedByDefault);
Amith Yamasanib4569fb2011-07-08 15:25:39 -0700396 updateQueryHint();
Amith Yamasani733cbd52010-09-03 12:21:39 -0700397 }
398
Alan Viverette5dddb702014-07-02 15:46:04 -0700399 int getSuggestionRowLayout() {
400 return mSuggestionRowLayout;
401 }
402
403 int getSuggestionCommitIconResId() {
404 return mSuggestionCommitIconResId;
405 }
406
Amith Yamasani733cbd52010-09-03 12:21:39 -0700407 /**
408 * Sets the SearchableInfo for this SearchView. Properties in the SearchableInfo are used
409 * to display labels, hints, suggestions, create intents for launching search results screens
410 * and controlling other affordances such as a voice button.
411 *
412 * @param searchable a SearchableInfo can be retrieved from the SearchManager, for a specific
413 * activity or a global search provider.
414 */
415 public void setSearchableInfo(SearchableInfo searchable) {
416 mSearchable = searchable;
417 if (mSearchable != null) {
418 updateSearchAutoComplete();
Amith Yamasani79f74302011-03-08 14:16:35 -0800419 updateQueryHint();
Amith Yamasani733cbd52010-09-03 12:21:39 -0700420 }
Amith Yamasani9b2e3022011-01-14 11:34:12 -0800421 // Cache the voice search capability
422 mVoiceButtonEnabled = hasVoiceSearch();
Luca Zanolin535698c2011-10-06 13:36:15 +0100423
424 if (mVoiceButtonEnabled) {
425 // Disable the microphone on the keyboard, as a mic is displayed near the text box
426 // TODO: use imeOptions to disable voice input when the new API will be available
Alan Viverettecb8ed372014-11-18 17:05:35 -0800427 mSearchSrcTextView.setPrivateImeOptions(IME_OPTION_NO_MICROPHONE);
Luca Zanolin535698c2011-10-06 13:36:15 +0100428 }
Amith Yamasanib4569fb2011-07-08 15:25:39 -0700429 updateViewsVisibility(isIconified());
Amith Yamasani733cbd52010-09-03 12:21:39 -0700430 }
431
Amith Yamasani940ef382011-03-02 18:43:23 -0800432 /**
433 * Sets the APP_DATA for legacy SearchDialog use.
434 * @param appSearchData bundle provided by the app when launching the search dialog
435 * @hide
436 */
437 public void setAppSearchData(Bundle appSearchData) {
438 mAppSearchData = appSearchData;
439 }
440
Amith Yamasani5607a382011-08-09 14:16:37 -0700441 /**
442 * Sets the IME options on the query text field.
443 *
444 * @see TextView#setImeOptions(int)
445 * @param imeOptions the options to set on the query text field
446 *
447 * @attr ref android.R.styleable#SearchView_imeOptions
448 */
449 public void setImeOptions(int imeOptions) {
Alan Viverettecb8ed372014-11-18 17:05:35 -0800450 mSearchSrcTextView.setImeOptions(imeOptions);
Amith Yamasani5607a382011-08-09 14:16:37 -0700451 }
452
453 /**
Amith Yamasanieca59d32012-04-25 18:57:18 -0700454 * Returns the IME options set on the query text field.
455 * @return the ime options
456 * @see TextView#setImeOptions(int)
457 *
458 * @attr ref android.R.styleable#SearchView_imeOptions
459 */
460 public int getImeOptions() {
Alan Viverettecb8ed372014-11-18 17:05:35 -0800461 return mSearchSrcTextView.getImeOptions();
Amith Yamasanieca59d32012-04-25 18:57:18 -0700462 }
463
464 /**
Amith Yamasani5607a382011-08-09 14:16:37 -0700465 * Sets the input type on the query text field.
466 *
467 * @see TextView#setInputType(int)
468 * @param inputType the input type to set on the query text field
469 *
470 * @attr ref android.R.styleable#SearchView_inputType
471 */
472 public void setInputType(int inputType) {
Alan Viverettecb8ed372014-11-18 17:05:35 -0800473 mSearchSrcTextView.setInputType(inputType);
Amith Yamasani5607a382011-08-09 14:16:37 -0700474 }
475
Amith Yamasanieca59d32012-04-25 18:57:18 -0700476 /**
477 * Returns the input type set on the query text field.
478 * @return the input type
479 *
480 * @attr ref android.R.styleable#SearchView_inputType
481 */
482 public int getInputType() {
Alan Viverettecb8ed372014-11-18 17:05:35 -0800483 return mSearchSrcTextView.getInputType();
Amith Yamasanieca59d32012-04-25 18:57:18 -0700484 }
485
Amith Yamasani05944762010-10-08 13:52:38 -0700486 /** @hide */
487 @Override
488 public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
Amith Yamasani7f8aef62011-01-25 11:58:09 -0800489 // Don't accept focus if in the middle of clearing focus
490 if (mClearingFocus) return false;
491 // Check if SearchView is focusable.
492 if (!isFocusable()) return false;
493 // If it is not iconified, then give the focus to the text field
494 if (!isIconified()) {
Alan Viverettecb8ed372014-11-18 17:05:35 -0800495 boolean result = mSearchSrcTextView.requestFocus(direction, previouslyFocusedRect);
Amith Yamasanif28d1872011-07-26 12:21:03 -0700496 if (result) {
497 updateViewsVisibility(false);
498 }
Amith Yamasani7f8aef62011-01-25 11:58:09 -0800499 return result;
500 } else {
501 return super.requestFocus(direction, previouslyFocusedRect);
502 }
Amith Yamasani05944762010-10-08 13:52:38 -0700503 }
504
505 /** @hide */
506 @Override
507 public void clearFocus() {
508 mClearingFocus = true;
509 super.clearFocus();
Alan Viverettecb8ed372014-11-18 17:05:35 -0800510 mSearchSrcTextView.clearFocus();
Yohei Yukawa5cfc1b42017-03-07 00:52:09 -0800511 mSearchSrcTextView.setImeVisibility(false);
Amith Yamasani05944762010-10-08 13:52:38 -0700512 mClearingFocus = false;
513 }
514
Amith Yamasani733cbd52010-09-03 12:21:39 -0700515 /**
516 * Sets a listener for user actions within the SearchView.
517 *
518 * @param listener the listener object that receives callbacks when the user performs
519 * actions in the SearchView such as clicking on buttons or typing a query.
520 */
Adam Powell01f21352011-01-20 18:30:10 -0800521 public void setOnQueryTextListener(OnQueryTextListener listener) {
Amith Yamasani733cbd52010-09-03 12:21:39 -0700522 mOnQueryChangeListener = listener;
523 }
524
525 /**
Amith Yamasani93227752010-09-14 10:10:54 -0700526 * Sets a listener to inform when the user closes the SearchView.
527 *
528 * @param listener the listener to call when the user closes the SearchView.
529 */
530 public void setOnCloseListener(OnCloseListener listener) {
531 mOnCloseListener = listener;
532 }
533
534 /**
Amith Yamasani05944762010-10-08 13:52:38 -0700535 * Sets a listener to inform when the focus of the query text field changes.
536 *
537 * @param listener the listener to inform of focus changes.
538 */
539 public void setOnQueryTextFocusChangeListener(OnFocusChangeListener listener) {
540 mOnQueryTextFocusChangeListener = listener;
541 }
542
543 /**
544 * Sets a listener to inform when a suggestion is focused or clicked.
545 *
546 * @param listener the listener to inform of suggestion selection events.
547 */
Adam Powell01f21352011-01-20 18:30:10 -0800548 public void setOnSuggestionListener(OnSuggestionListener listener) {
Amith Yamasani05944762010-10-08 13:52:38 -0700549 mOnSuggestionListener = listener;
550 }
551
552 /**
Amith Yamasani48385482010-12-03 14:43:52 -0800553 * Sets a listener to inform when the search button is pressed. This is only
Scott Maincccdbe92011-02-06 15:51:47 -0800554 * relevant when the text field is not visible by default. Calling {@link #setIconified
555 * setIconified(false)} can also cause this listener to be informed.
Amith Yamasani48385482010-12-03 14:43:52 -0800556 *
557 * @param listener the listener to inform when the search button is clicked or
558 * the text field is programmatically de-iconified.
559 */
560 public void setOnSearchClickListener(OnClickListener listener) {
561 mOnSearchClickListener = listener;
562 }
563
564 /**
565 * Returns the query string currently in the text field.
566 *
567 * @return the query string
568 */
Ashley Rose55f9f922019-01-28 19:29:36 -0500569 @InspectableProperty(hasAttributeId = false)
Amith Yamasani48385482010-12-03 14:43:52 -0800570 public CharSequence getQuery() {
Alan Viverettecb8ed372014-11-18 17:05:35 -0800571 return mSearchSrcTextView.getText();
Amith Yamasani48385482010-12-03 14:43:52 -0800572 }
573
574 /**
Amith Yamasani733cbd52010-09-03 12:21:39 -0700575 * Sets a query string in the text field and optionally submits the query as well.
576 *
577 * @param query the query string. This replaces any query text already present in the
578 * text field.
579 * @param submit whether to submit the query right now or only update the contents of
580 * text field.
581 */
582 public void setQuery(CharSequence query, boolean submit) {
Alan Viverettecb8ed372014-11-18 17:05:35 -0800583 mSearchSrcTextView.setText(query);
Dmitri Plotnikov87c50252010-10-21 21:16:42 -0700584 if (query != null) {
Alan Viverettecb8ed372014-11-18 17:05:35 -0800585 mSearchSrcTextView.setSelection(mSearchSrcTextView.length());
Amith Yamasani068d73c2011-05-27 15:15:14 -0700586 mUserQuery = query;
Dmitri Plotnikov87c50252010-10-21 21:16:42 -0700587 }
588
Amith Yamasani733cbd52010-09-03 12:21:39 -0700589 // If the query is not empty and submit is requested, submit the query
590 if (submit && !TextUtils.isEmpty(query)) {
591 onSubmitQuery();
592 }
593 }
594
595 /**
Alan Viveretteb4004df2015-04-29 16:55:42 -0700596 * Sets the hint text to display in the query text field. This overrides
597 * any hint specified in the {@link SearchableInfo}.
598 * <p>
599 * This value may be specified as an empty string to prevent any query hint
600 * from being displayed.
Amith Yamasani733cbd52010-09-03 12:21:39 -0700601 *
Alan Viveretteb4004df2015-04-29 16:55:42 -0700602 * @param hint the hint text to display or {@code null} to clear
Scott Mainabdf0d52011-02-08 10:20:27 -0800603 * @attr ref android.R.styleable#SearchView_queryHint
Amith Yamasani733cbd52010-09-03 12:21:39 -0700604 */
Alan Viveretteb4004df2015-04-29 16:55:42 -0700605 public void setQueryHint(@Nullable CharSequence hint) {
Amith Yamasani733cbd52010-09-03 12:21:39 -0700606 mQueryHint = hint;
607 updateQueryHint();
608 }
609
610 /**
Alan Viveretteb4004df2015-04-29 16:55:42 -0700611 * Returns the hint text that will be displayed in the query text field.
612 * <p>
613 * The displayed query hint is chosen in the following order:
614 * <ol>
615 * <li>Non-null value set with {@link #setQueryHint(CharSequence)}
616 * <li>Value specified in XML using
617 * {@link android.R.styleable#SearchView_queryHint android:queryHint}
618 * <li>Valid string resource ID exposed by the {@link SearchableInfo} via
619 * {@link SearchableInfo#getHintId()}
620 * <li>Default hint provided by the theme against which the view was
621 * inflated
622 * </ol>
Amith Yamasanieca59d32012-04-25 18:57:18 -0700623 *
Alan Viveretteb4004df2015-04-29 16:55:42 -0700624 * @return the displayed query hint text, or {@code null} if none set
Amith Yamasanieca59d32012-04-25 18:57:18 -0700625 * @attr ref android.R.styleable#SearchView_queryHint
626 */
Ashley Rose55f9f922019-01-28 19:29:36 -0500627 @InspectableProperty
Alan Viveretteb4004df2015-04-29 16:55:42 -0700628 @Nullable
Amith Yamasanieca59d32012-04-25 18:57:18 -0700629 public CharSequence getQueryHint() {
Alan Viveretteb4004df2015-04-29 16:55:42 -0700630 final CharSequence hint;
Amith Yamasanieca59d32012-04-25 18:57:18 -0700631 if (mQueryHint != null) {
Alan Viveretteb4004df2015-04-29 16:55:42 -0700632 hint = mQueryHint;
633 } else if (mSearchable != null && mSearchable.getHintId() != 0) {
634 hint = getContext().getText(mSearchable.getHintId());
635 } else {
636 hint = mDefaultQueryHint;
Amith Yamasanieca59d32012-04-25 18:57:18 -0700637 }
Alan Viveretteb4004df2015-04-29 16:55:42 -0700638 return hint;
Amith Yamasanieca59d32012-04-25 18:57:18 -0700639 }
640
641 /**
Amith Yamasani733cbd52010-09-03 12:21:39 -0700642 * Sets the default or resting state of the search field. If true, a single search icon is
643 * shown by default and expands to show the text field and other buttons when pressed. Also,
644 * if the default state is iconified, then it collapses to that state when the close button
Amith Yamasani93227752010-09-14 10:10:54 -0700645 * is pressed. Changes to this property will take effect immediately.
Amith Yamasani733cbd52010-09-03 12:21:39 -0700646 *
Scott Maincccdbe92011-02-06 15:51:47 -0800647 * <p>The default value is true.</p>
Amith Yamasani93227752010-09-14 10:10:54 -0700648 *
649 * @param iconified whether the search field should be iconified by default
Scott Mainabdf0d52011-02-08 10:20:27 -0800650 *
651 * @attr ref android.R.styleable#SearchView_iconifiedByDefault
Amith Yamasani733cbd52010-09-03 12:21:39 -0700652 */
653 public void setIconifiedByDefault(boolean iconified) {
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -0700654 if (mIconifiedByDefault == iconified) return;
Amith Yamasani733cbd52010-09-03 12:21:39 -0700655 mIconifiedByDefault = iconified;
656 updateViewsVisibility(iconified);
Amith Yamasanib47c4fd2011-08-04 14:30:07 -0700657 updateQueryHint();
Amith Yamasani733cbd52010-09-03 12:21:39 -0700658 }
659
Amith Yamasani93227752010-09-14 10:10:54 -0700660 /**
661 * Returns the default iconified state of the search field.
662 * @return
Amith Yamasani3d3e7a52012-05-04 14:46:31 -0700663 *
Ashley Rose55f9f922019-01-28 19:29:36 -0500664 * @deprecated use {@link #isIconifiedByDefault()}
Amith Yamasani3d3e7a52012-05-04 14:46:31 -0700665 * @attr ref android.R.styleable#SearchView_iconifiedByDefault
Amith Yamasani93227752010-09-14 10:10:54 -0700666 */
Ashley Rose55f9f922019-01-28 19:29:36 -0500667 @Deprecated
Amith Yamasani733cbd52010-09-03 12:21:39 -0700668 public boolean isIconfiedByDefault() {
669 return mIconifiedByDefault;
670 }
671
672 /**
Ashley Rose55f9f922019-01-28 19:29:36 -0500673 * Returns the default iconified state of the search field.
674 *
675 * @attr ref android.R.styleable#SearchView_iconifiedByDefault
676 */
677 @InspectableProperty
678 public boolean isIconifiedByDefault() {
679 return mIconifiedByDefault;
680 }
681
682 /**
Amith Yamasani93227752010-09-14 10:10:54 -0700683 * Iconifies or expands the SearchView. Any query text is cleared when iconified. This is
684 * a temporary state and does not override the default iconified state set by
685 * {@link #setIconifiedByDefault(boolean)}. If the default state is iconified, then
686 * a false here will only be valid until the user closes the field. And if the default
687 * state is expanded, then a true here will only clear the text field and not close it.
688 *
689 * @param iconify a true value will collapse the SearchView to an icon, while a false will
690 * expand it.
691 */
692 public void setIconified(boolean iconify) {
693 if (iconify) {
694 onCloseClicked();
695 } else {
696 onSearchClicked();
697 }
698 }
699
700 /**
701 * Returns the current iconified state of the SearchView.
702 *
703 * @return true if the SearchView is currently iconified, false if the search field is
704 * fully visible.
705 */
Ashley Rose55f9f922019-01-28 19:29:36 -0500706 @InspectableProperty(hasAttributeId = false)
Amith Yamasani93227752010-09-14 10:10:54 -0700707 public boolean isIconified() {
708 return mIconified;
709 }
710
711 /**
Amith Yamasani733cbd52010-09-03 12:21:39 -0700712 * Enables showing a submit button when the query is non-empty. In cases where the SearchView
713 * is being used to filter the contents of the current activity and doesn't launch a separate
714 * results activity, then the submit button should be disabled.
715 *
716 * @param enabled true to show a submit button for submitting queries, false if a submit
717 * button is not required.
718 */
719 public void setSubmitButtonEnabled(boolean enabled) {
Amith Yamasani733cbd52010-09-03 12:21:39 -0700720 mSubmitButtonEnabled = enabled;
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -0700721 updateViewsVisibility(isIconified());
Amith Yamasani733cbd52010-09-03 12:21:39 -0700722 }
723
724 /**
725 * Returns whether the submit button is enabled when necessary or never displayed.
726 *
727 * @return whether the submit button is enabled automatically when necessary
728 */
729 public boolean isSubmitButtonEnabled() {
730 return mSubmitButtonEnabled;
731 }
732
Amith Yamasanie678f462010-09-15 16:13:43 -0700733 /**
734 * Specifies if a query refinement button should be displayed alongside each suggestion
735 * or if it should depend on the flags set in the individual items retrieved from the
736 * suggestions provider. Clicking on the query refinement button will replace the text
737 * in the query text field with the text from the suggestion. This flag only takes effect
738 * if a SearchableInfo has been specified with {@link #setSearchableInfo(SearchableInfo)}
739 * and not when using a custom adapter.
740 *
741 * @param enable true if all items should have a query refinement button, false if only
742 * those items that have a query refinement flag set should have the button.
743 *
744 * @see SearchManager#SUGGEST_COLUMN_FLAGS
745 * @see SearchManager#FLAG_QUERY_REFINEMENT
746 */
747 public void setQueryRefinementEnabled(boolean enable) {
748 mQueryRefinement = enable;
749 if (mSuggestionsAdapter instanceof SuggestionsAdapter) {
750 ((SuggestionsAdapter) mSuggestionsAdapter).setQueryRefinement(
751 enable ? SuggestionsAdapter.REFINE_ALL : SuggestionsAdapter.REFINE_BY_ENTRY);
752 }
753 }
754
755 /**
756 * Returns whether query refinement is enabled for all items or only specific ones.
757 * @return true if enabled for all items, false otherwise.
758 */
759 public boolean isQueryRefinementEnabled() {
760 return mQueryRefinement;
Amith Yamasani733cbd52010-09-03 12:21:39 -0700761 }
762
763 /**
764 * You can set a custom adapter if you wish. Otherwise the default adapter is used to
765 * display the suggestions from the suggestions provider associated with the SearchableInfo.
766 *
767 * @see #setSearchableInfo(SearchableInfo)
768 */
769 public void setSuggestionsAdapter(CursorAdapter adapter) {
770 mSuggestionsAdapter = adapter;
771
Alan Viverettecb8ed372014-11-18 17:05:35 -0800772 mSearchSrcTextView.setAdapter(mSuggestionsAdapter);
Amith Yamasani733cbd52010-09-03 12:21:39 -0700773 }
774
775 /**
776 * Returns the adapter used for suggestions, if any.
777 * @return the suggestions adapter
778 */
779 public CursorAdapter getSuggestionsAdapter() {
780 return mSuggestionsAdapter;
781 }
782
Amith Yamasani5931b1f2010-10-18 16:13:14 -0700783 /**
784 * Makes the view at most this many pixels wide
785 *
786 * @attr ref android.R.styleable#SearchView_maxWidth
787 */
788 public void setMaxWidth(int maxpixels) {
789 mMaxWidth = maxpixels;
790
791 requestLayout();
792 }
793
Amith Yamasanieca59d32012-04-25 18:57:18 -0700794 /**
795 * Gets the specified maximum width in pixels, if set. Returns zero if
796 * no maximum width was specified.
797 * @return the maximum width of the view
Amith Yamasani3d3e7a52012-05-04 14:46:31 -0700798 *
799 * @attr ref android.R.styleable#SearchView_maxWidth
Amith Yamasanieca59d32012-04-25 18:57:18 -0700800 */
Ashley Rose55f9f922019-01-28 19:29:36 -0500801 @InspectableProperty
Amith Yamasanieca59d32012-04-25 18:57:18 -0700802 public int getMaxWidth() {
803 return mMaxWidth;
804 }
805
Amith Yamasani5931b1f2010-10-18 16:13:14 -0700806 @Override
807 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Amith Yamasania95e4882011-08-17 11:41:37 -0700808 // Let the standard measurements take effect in iconified state.
809 if (isIconified()) {
810 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
811 return;
812 }
813
Amith Yamasani5931b1f2010-10-18 16:13:14 -0700814 int widthMode = MeasureSpec.getMode(widthMeasureSpec);
815 int width = MeasureSpec.getSize(widthMeasureSpec);
816
Amith Yamasani167d69a2011-08-12 19:28:37 -0700817 switch (widthMode) {
818 case MeasureSpec.AT_MOST:
819 // If there is an upper limit, don't exceed maximum width (explicit or implicit)
820 if (mMaxWidth > 0) {
821 width = Math.min(mMaxWidth, width);
822 } else {
823 width = Math.min(getPreferredWidth(), width);
824 }
825 break;
826 case MeasureSpec.EXACTLY:
827 // If an exact width is specified, still don't exceed any specified maximum width
828 if (mMaxWidth > 0) {
829 width = Math.min(mMaxWidth, width);
830 }
831 break;
832 case MeasureSpec.UNSPECIFIED:
833 // Use maximum width, if specified, else preferred width
834 width = mMaxWidth > 0 ? mMaxWidth : getPreferredWidth();
835 break;
Amith Yamasani5931b1f2010-10-18 16:13:14 -0700836 }
Amith Yamasani167d69a2011-08-12 19:28:37 -0700837 widthMode = MeasureSpec.EXACTLY;
Aurimas Liutikasc8fd00a2016-04-15 13:55:53 -0700838
839 int heightMode = MeasureSpec.getMode(heightMeasureSpec);
840 int height = MeasureSpec.getSize(heightMeasureSpec);
841
842 switch (heightMode) {
843 case MeasureSpec.AT_MOST:
Aurimas Liutikasc8fd00a2016-04-15 13:55:53 -0700844 height = Math.min(getPreferredHeight(), height);
845 break;
Aurimas Liutikasf6a50be2016-09-09 15:32:55 -0700846 case MeasureSpec.UNSPECIFIED:
847 height = getPreferredHeight();
848 break;
Aurimas Liutikasc8fd00a2016-04-15 13:55:53 -0700849 }
850 heightMode = MeasureSpec.EXACTLY;
851
852 super.onMeasure(MeasureSpec.makeMeasureSpec(width, widthMode),
853 MeasureSpec.makeMeasureSpec(height, heightMode));
854 }
855
856 @Override
857 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
858 super.onLayout(changed, left, top, right, bottom);
859
860 if (changed) {
861 // Expand mSearchSrcTextView touch target to be the height of the parent in order to
862 // allow it to be up to 48dp.
863 getChildBoundsWithinSearchView(mSearchSrcTextView, mSearchSrcTextViewBounds);
864 mSearchSrtTextViewBoundsExpanded.set(
865 mSearchSrcTextViewBounds.left, 0, mSearchSrcTextViewBounds.right, bottom - top);
866 if (mTouchDelegate == null) {
867 mTouchDelegate = new UpdatableTouchDelegate(mSearchSrtTextViewBoundsExpanded,
868 mSearchSrcTextViewBounds, mSearchSrcTextView);
869 setTouchDelegate(mTouchDelegate);
870 } else {
871 mTouchDelegate.setBounds(mSearchSrtTextViewBoundsExpanded, mSearchSrcTextViewBounds);
872 }
873 }
874 }
875
876 private void getChildBoundsWithinSearchView(View view, Rect rect) {
877 view.getLocationInWindow(mTemp);
878 getLocationInWindow(mTemp2);
879 final int top = mTemp[1] - mTemp2[1];
880 final int left = mTemp[0] - mTemp2[0];
881 rect.set(left , top, left + view.getWidth(), top + view.getHeight());
Amith Yamasani167d69a2011-08-12 19:28:37 -0700882 }
883
884 private int getPreferredWidth() {
885 return getContext().getResources()
886 .getDimensionPixelSize(R.dimen.search_view_preferred_width);
Amith Yamasani5931b1f2010-10-18 16:13:14 -0700887 }
888
Aurimas Liutikasc8fd00a2016-04-15 13:55:53 -0700889 private int getPreferredHeight() {
890 return getContext().getResources()
891 .getDimensionPixelSize(R.dimen.search_view_preferred_height);
892 }
893
Mathew Inwood978c6e22018-08-21 15:58:55 +0100894 @UnsupportedAppUsage
Amith Yamasani93227752010-09-14 10:10:54 -0700895 private void updateViewsVisibility(final boolean collapsed) {
896 mIconified = collapsed;
Amith Yamasani733cbd52010-09-03 12:21:39 -0700897 // Visibility of views that are visible when collapsed
Amith Yamasani93227752010-09-14 10:10:54 -0700898 final int visCollapsed = collapsed ? VISIBLE : GONE;
Amith Yamasani05944762010-10-08 13:52:38 -0700899 // Is there text in the query
Alan Viverettecb8ed372014-11-18 17:05:35 -0800900 final boolean hasText = !TextUtils.isEmpty(mSearchSrcTextView.getText());
Amith Yamasani733cbd52010-09-03 12:21:39 -0700901
902 mSearchButton.setVisibility(visCollapsed);
Amith Yamasani9b2e3022011-01-14 11:34:12 -0800903 updateSubmitButton(hasText);
904 mSearchEditFrame.setVisibility(collapsed ? GONE : VISIBLE);
Alan Viverette53b165e2015-08-25 12:33:01 -0400905
906 final int iconVisibility;
907 if (mCollapsedIcon.getDrawable() == null || mIconifiedByDefault) {
908 iconVisibility = GONE;
909 } else {
910 iconVisibility = VISIBLE;
911 }
912 mCollapsedIcon.setVisibility(iconVisibility);
913
Amith Yamasani4aedb392010-12-15 16:04:57 -0800914 updateCloseButton();
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -0700915 updateVoiceButton(!hasText);
Amith Yamasani9b2e3022011-01-14 11:34:12 -0800916 updateSubmitArea();
Amith Yamasani9b2e3022011-01-14 11:34:12 -0800917 }
918
919 private boolean hasVoiceSearch() {
920 if (mSearchable != null && mSearchable.getVoiceSearchEnabled()) {
921 Intent testIntent = null;
922 if (mSearchable.getVoiceSearchLaunchWebSearch()) {
923 testIntent = mVoiceWebSearchIntent;
924 } else if (mSearchable.getVoiceSearchLaunchRecognizer()) {
925 testIntent = mVoiceAppSearchIntent;
926 }
927 if (testIntent != null) {
928 ResolveInfo ri = getContext().getPackageManager().resolveActivity(testIntent,
929 PackageManager.MATCH_DEFAULT_ONLY);
930 return ri != null;
931 }
932 }
933 return false;
934 }
935
936 private boolean isSubmitAreaEnabled() {
937 return (mSubmitButtonEnabled || mVoiceButtonEnabled) && !isIconified();
938 }
939
Mathew Inwood978c6e22018-08-21 15:58:55 +0100940 @UnsupportedAppUsage
Amith Yamasani9b2e3022011-01-14 11:34:12 -0800941 private void updateSubmitButton(boolean hasText) {
Amith Yamasani79f74302011-03-08 14:16:35 -0800942 int visibility = GONE;
Amith Yamasanicf72ab42011-11-04 13:49:28 -0700943 if (mSubmitButtonEnabled && isSubmitAreaEnabled() && hasFocus()
944 && (hasText || !mVoiceButtonEnabled)) {
Amith Yamasani79f74302011-03-08 14:16:35 -0800945 visibility = VISIBLE;
946 }
Alan Viverettecb8ed372014-11-18 17:05:35 -0800947 mGoButton.setVisibility(visibility);
Amith Yamasani9b2e3022011-01-14 11:34:12 -0800948 }
949
Mathew Inwood978c6e22018-08-21 15:58:55 +0100950 @UnsupportedAppUsage
Amith Yamasani9b2e3022011-01-14 11:34:12 -0800951 private void updateSubmitArea() {
952 int visibility = GONE;
Amith Yamasani79f74302011-03-08 14:16:35 -0800953 if (isSubmitAreaEnabled()
Alan Viverettecb8ed372014-11-18 17:05:35 -0800954 && (mGoButton.getVisibility() == VISIBLE
Amith Yamasani79f74302011-03-08 14:16:35 -0800955 || mVoiceButton.getVisibility() == VISIBLE)) {
956 visibility = VISIBLE;
Amith Yamasani9b2e3022011-01-14 11:34:12 -0800957 }
958 mSubmitArea.setVisibility(visibility);
Amith Yamasani733cbd52010-09-03 12:21:39 -0700959 }
960
Amith Yamasani4aedb392010-12-15 16:04:57 -0800961 private void updateCloseButton() {
Alan Viverettecb8ed372014-11-18 17:05:35 -0800962 final boolean hasText = !TextUtils.isEmpty(mSearchSrcTextView.getText());
Amith Yamasani4aedb392010-12-15 16:04:57 -0800963 // Should we show the close button? It is not shown if there's no focus,
964 // field is not iconified by default and there is no text in it.
Amith Yamasani763bc072011-07-22 11:53:47 -0700965 final boolean showClose = hasText || (mIconifiedByDefault && !mExpandedInActionView);
Amith Yamasani167d69a2011-08-12 19:28:37 -0700966 mCloseButton.setVisibility(showClose ? VISIBLE : GONE);
Alan Viverettecb8ed372014-11-18 17:05:35 -0800967 final Drawable closeButtonImg = mCloseButton.getDrawable();
968 if (closeButtonImg != null){
969 closeButtonImg.setState(hasText ? ENABLED_STATE_SET : EMPTY_STATE_SET);
970 }
Amith Yamasani4aedb392010-12-15 16:04:57 -0800971 }
972
Amith Yamasania95e4882011-08-17 11:41:37 -0700973 private void postUpdateFocusedState() {
974 post(mUpdateDrawableStateRunnable);
975 }
976
977 private void updateFocusedState() {
Alan Viverettecb8ed372014-11-18 17:05:35 -0800978 final boolean focused = mSearchSrcTextView.hasFocus();
979 final int[] stateSet = focused ? FOCUSED_STATE_SET : EMPTY_STATE_SET;
980 final Drawable searchPlateBg = mSearchPlate.getBackground();
981 if (searchPlateBg != null) {
982 searchPlateBg.setState(stateSet);
983 }
984 final Drawable submitAreaBg = mSubmitArea.getBackground();
985 if (submitAreaBg != null) {
986 submitAreaBg.setState(stateSet);
987 }
Amith Yamasania95e4882011-08-17 11:41:37 -0700988 invalidate();
989 }
990
991 @Override
Amith Yamasania465b2d2011-08-19 13:01:22 -0700992 protected void onDetachedFromWindow() {
Amith Yamasania95e4882011-08-17 11:41:37 -0700993 removeCallbacks(mUpdateDrawableStateRunnable);
Amith Yamasani87907642011-11-03 11:32:44 -0700994 post(mReleaseCursorRunnable);
Amith Yamasania95e4882011-08-17 11:41:37 -0700995 super.onDetachedFromWindow();
Amith Yamasani79f74302011-03-08 14:16:35 -0800996 }
997
Amith Yamasanie678f462010-09-15 16:13:43 -0700998 /**
999 * Called by the SuggestionsAdapter
1000 * @hide
1001 */
1002 /* package */void onQueryRefine(CharSequence queryText) {
1003 setQuery(queryText);
1004 }
1005
Mathew Inwood978c6e22018-08-21 15:58:55 +01001006 @UnsupportedAppUsage
Amith Yamasani733cbd52010-09-03 12:21:39 -07001007 private final OnClickListener mOnClickListener = new OnClickListener() {
1008
1009 public void onClick(View v) {
1010 if (v == mSearchButton) {
1011 onSearchClicked();
1012 } else if (v == mCloseButton) {
1013 onCloseClicked();
Alan Viverettecb8ed372014-11-18 17:05:35 -08001014 } else if (v == mGoButton) {
Amith Yamasani733cbd52010-09-03 12:21:39 -07001015 onSubmitQuery();
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001016 } else if (v == mVoiceButton) {
1017 onVoiceClicked();
Alan Viverettecb8ed372014-11-18 17:05:35 -08001018 } else if (v == mSearchSrcTextView) {
Amith Yamasanif28d1872011-07-26 12:21:03 -07001019 forceSuggestionQuery();
Amith Yamasani733cbd52010-09-03 12:21:39 -07001020 }
1021 }
1022 };
1023
1024 /**
1025 * Handles the key down event for dealing with action keys.
1026 *
1027 * @param keyCode This is the keycode of the typed key, and is the same value as
1028 * found in the KeyEvent parameter.
1029 * @param event The complete event record for the typed key
1030 *
1031 * @return true if the event was handled here, or false if not.
1032 */
1033 @Override
1034 public boolean onKeyDown(int keyCode, KeyEvent event) {
1035 if (mSearchable == null) {
1036 return false;
1037 }
1038
1039 // if it's an action specified by the searchable activity, launch the
1040 // entered query with the action key
1041 SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
1042 if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) {
Alan Viverettecb8ed372014-11-18 17:05:35 -08001043 launchQuerySearch(keyCode, actionKey.getQueryActionMsg(), mSearchSrcTextView.getText()
Amith Yamasani93227752010-09-14 10:10:54 -07001044 .toString());
Amith Yamasani733cbd52010-09-03 12:21:39 -07001045 return true;
1046 }
1047
1048 return super.onKeyDown(keyCode, event);
1049 }
1050
Amith Yamasani968ec932010-12-02 14:00:47 -08001051 /**
1052 * React to the user typing "enter" or other hardwired keys while typing in
1053 * the search box. This handles these special keys while the edit box has
1054 * focus.
1055 */
1056 View.OnKeyListener mTextKeyListener = new View.OnKeyListener() {
1057 public boolean onKey(View v, int keyCode, KeyEvent event) {
1058 // guard against possible race conditions
1059 if (mSearchable == null) {
1060 return false;
1061 }
1062
1063 if (DBG) {
1064 Log.d(LOG_TAG, "mTextListener.onKey(" + keyCode + "," + event + "), selection: "
Alan Viverettecb8ed372014-11-18 17:05:35 -08001065 + mSearchSrcTextView.getListSelection());
Amith Yamasani968ec932010-12-02 14:00:47 -08001066 }
1067
1068 // If a suggestion is selected, handle enter, search key, and action keys
1069 // as presses on the selected suggestion
Alan Viverettecb8ed372014-11-18 17:05:35 -08001070 if (mSearchSrcTextView.isPopupShowing()
1071 && mSearchSrcTextView.getListSelection() != ListView.INVALID_POSITION) {
Amith Yamasani968ec932010-12-02 14:00:47 -08001072 return onSuggestionsKey(v, keyCode, event);
1073 }
1074
1075 // If there is text in the query box, handle enter, and action keys
1076 // The search key is handled by the dialog's onKeyDown().
Alan Viverettecb8ed372014-11-18 17:05:35 -08001077 if (!mSearchSrcTextView.isEmpty() && event.hasNoModifiers()) {
Jeff Brown4e6319b2010-12-13 10:36:51 -08001078 if (event.getAction() == KeyEvent.ACTION_UP) {
1079 if (keyCode == KeyEvent.KEYCODE_ENTER) {
1080 v.cancelLongPress();
Amith Yamasani968ec932010-12-02 14:00:47 -08001081
Jeff Brown4e6319b2010-12-13 10:36:51 -08001082 // Launch as a regular search.
Alan Viverettecb8ed372014-11-18 17:05:35 -08001083 launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null, mSearchSrcTextView.getText()
Jeff Brown4e6319b2010-12-13 10:36:51 -08001084 .toString());
1085 return true;
1086 }
Amith Yamasani968ec932010-12-02 14:00:47 -08001087 }
1088 if (event.getAction() == KeyEvent.ACTION_DOWN) {
1089 SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
1090 if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) {
Alan Viverettecb8ed372014-11-18 17:05:35 -08001091 launchQuerySearch(keyCode, actionKey.getQueryActionMsg(), mSearchSrcTextView
Amith Yamasani968ec932010-12-02 14:00:47 -08001092 .getText().toString());
1093 return true;
1094 }
1095 }
1096 }
1097 return false;
1098 }
1099 };
1100
1101 /**
1102 * React to the user typing while in the suggestions list. First, check for
1103 * action keys. If not handled, try refocusing regular characters into the
1104 * EditText.
1105 */
1106 private boolean onSuggestionsKey(View v, int keyCode, KeyEvent event) {
1107 // guard against possible race conditions (late arrival after dismiss)
1108 if (mSearchable == null) {
1109 return false;
1110 }
1111 if (mSuggestionsAdapter == null) {
1112 return false;
1113 }
Jeff Brown4e6319b2010-12-13 10:36:51 -08001114 if (event.getAction() == KeyEvent.ACTION_DOWN && event.hasNoModifiers()) {
Amith Yamasani968ec932010-12-02 14:00:47 -08001115 // First, check for enter or search (both of which we'll treat as a
1116 // "click")
Jeff Brown4e6319b2010-12-13 10:36:51 -08001117 if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_SEARCH
1118 || keyCode == KeyEvent.KEYCODE_TAB) {
Alan Viverettecb8ed372014-11-18 17:05:35 -08001119 int position = mSearchSrcTextView.getListSelection();
Amith Yamasani968ec932010-12-02 14:00:47 -08001120 return onItemClicked(position, KeyEvent.KEYCODE_UNKNOWN, null);
1121 }
1122
1123 // Next, check for left/right moves, which we use to "return" the
1124 // user to the edit view
1125 if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
1126 // give "focus" to text editor, with cursor at the beginning if
1127 // left key, at end if right key
1128 // TODO: Reverse left/right for right-to-left languages, e.g.
1129 // Arabic
Alan Viverettecb8ed372014-11-18 17:05:35 -08001130 int selPoint = (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) ? 0 : mSearchSrcTextView
Amith Yamasani968ec932010-12-02 14:00:47 -08001131 .length();
Alan Viverettecb8ed372014-11-18 17:05:35 -08001132 mSearchSrcTextView.setSelection(selPoint);
1133 mSearchSrcTextView.setListSelection(0);
1134 mSearchSrcTextView.clearListSelection();
1135 mSearchSrcTextView.ensureImeVisible(true);
Amith Yamasani968ec932010-12-02 14:00:47 -08001136
1137 return true;
1138 }
1139
1140 // Next, check for an "up and out" move
Alan Viverettecb8ed372014-11-18 17:05:35 -08001141 if (keyCode == KeyEvent.KEYCODE_DPAD_UP && 0 == mSearchSrcTextView.getListSelection()) {
Amith Yamasani968ec932010-12-02 14:00:47 -08001142 // TODO: restoreUserQuery();
1143 // let ACTV complete the move
1144 return false;
1145 }
1146
1147 // Next, check for an "action key"
1148 SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
1149 if ((actionKey != null)
1150 && ((actionKey.getSuggestActionMsg() != null) || (actionKey
1151 .getSuggestActionMsgColumn() != null))) {
1152 // launch suggestion using action key column
Alan Viverettecb8ed372014-11-18 17:05:35 -08001153 int position = mSearchSrcTextView.getListSelection();
Amith Yamasani968ec932010-12-02 14:00:47 -08001154 if (position != ListView.INVALID_POSITION) {
1155 Cursor c = mSuggestionsAdapter.getCursor();
1156 if (c.moveToPosition(position)) {
1157 final String actionMsg = getActionKeyMessage(c, actionKey);
1158 if (actionMsg != null && (actionMsg.length() > 0)) {
1159 return onItemClicked(position, keyCode, actionMsg);
1160 }
1161 }
1162 }
1163 }
1164 }
1165 return false;
1166 }
1167
1168 /**
1169 * For a given suggestion and a given cursor row, get the action message. If
1170 * not provided by the specific row/column, also check for a single
1171 * definition (for the action key).
1172 *
1173 * @param c The cursor providing suggestions
1174 * @param actionKey The actionkey record being examined
1175 *
1176 * @return Returns a string, or null if no action key message for this
1177 * suggestion
1178 */
1179 private static String getActionKeyMessage(Cursor c, SearchableInfo.ActionKeyInfo actionKey) {
1180 String result = null;
1181 // check first in the cursor data, for a suggestion-specific message
1182 final String column = actionKey.getSuggestActionMsgColumn();
1183 if (column != null) {
1184 result = SuggestionsAdapter.getColumnString(c, column);
1185 }
1186 // If the cursor didn't give us a message, see if there's a single
1187 // message defined
1188 // for the actionkey (for all suggestions)
1189 if (result == null) {
1190 result = actionKey.getSuggestActionMsg();
1191 }
1192 return result;
1193 }
1194
Amith Yamasanib4569fb2011-07-08 15:25:39 -07001195 private CharSequence getDecoratedHint(CharSequence hintText) {
Alan Viverettecb8ed372014-11-18 17:05:35 -08001196 // If the field is always expanded or we don't have a search hint icon,
1197 // then don't add the search icon to the hint.
1198 if (!mIconifiedByDefault || mSearchHintIcon == null) {
Alan Viverette5dddb702014-07-02 15:46:04 -07001199 return hintText;
1200 }
Amith Yamasanib4569fb2011-07-08 15:25:39 -07001201
Alan Viverettecb8ed372014-11-18 17:05:35 -08001202 final int textSize = (int) (mSearchSrcTextView.getTextSize() * 1.25);
1203 mSearchHintIcon.setBounds(0, 0, textSize, textSize);
Alan Viverette5dddb702014-07-02 15:46:04 -07001204
Alan Viverettecb8ed372014-11-18 17:05:35 -08001205 final SpannableStringBuilder ssb = new SpannableStringBuilder(" ");
1206 ssb.setSpan(new ImageSpan(mSearchHintIcon), 1, 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
Alan Viverette5dddb702014-07-02 15:46:04 -07001207 ssb.append(hintText);
Amith Yamasanib4569fb2011-07-08 15:25:39 -07001208 return ssb;
1209 }
1210
Amith Yamasani733cbd52010-09-03 12:21:39 -07001211 private void updateQueryHint() {
Alan Viveretteb4004df2015-04-29 16:55:42 -07001212 final CharSequence hint = getQueryHint();
1213 mSearchSrcTextView.setHint(getDecoratedHint(hint == null ? "" : hint));
Amith Yamasani733cbd52010-09-03 12:21:39 -07001214 }
1215
1216 /**
1217 * Updates the auto-complete text view.
1218 */
1219 private void updateSearchAutoComplete() {
Alan Viverettecb8ed372014-11-18 17:05:35 -08001220 mSearchSrcTextView.setDropDownAnimationStyle(0); // no animation
1221 mSearchSrcTextView.setThreshold(mSearchable.getSuggestThreshold());
1222 mSearchSrcTextView.setImeOptions(mSearchable.getImeOptions());
Amith Yamasani5607a382011-08-09 14:16:37 -07001223 int inputType = mSearchable.getInputType();
1224 // We only touch this if the input type is set up for text (which it almost certainly
1225 // should be, in the case of search!)
1226 if ((inputType & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_TEXT) {
1227 // The existence of a suggestions authority is the proxy for "suggestions
1228 // are available here"
1229 inputType &= ~InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;
1230 if (mSearchable.getSuggestAuthority() != null) {
1231 inputType |= InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;
Satoshi Kataoka9ce11162012-06-04 17:12:08 +09001232 // TYPE_TEXT_FLAG_AUTO_COMPLETE means that the text editor is performing
1233 // auto-completion based on its own semantics, which it will present to the user
1234 // as they type. This generally means that the input method should not show its
1235 // own candidates, and the spell checker should not be in action. The text editor
1236 // supplies its candidates by calling InputMethodManager.displayCompletions(),
1237 // which in turn will call InputMethodSession.displayCompletions().
1238 inputType |= InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
Amith Yamasani5607a382011-08-09 14:16:37 -07001239 }
1240 }
Alan Viverettecb8ed372014-11-18 17:05:35 -08001241 mSearchSrcTextView.setInputType(inputType);
Amith Yamasani87907642011-11-03 11:32:44 -07001242 if (mSuggestionsAdapter != null) {
1243 mSuggestionsAdapter.changeCursor(null);
1244 }
Amith Yamasani733cbd52010-09-03 12:21:39 -07001245 // attach the suggestions adapter, if suggestions are available
1246 // The existence of a suggestions authority is the proxy for "suggestions available here"
1247 if (mSearchable.getSuggestAuthority() != null) {
1248 mSuggestionsAdapter = new SuggestionsAdapter(getContext(),
1249 this, mSearchable, mOutsideDrawablesCache);
Alan Viverettecb8ed372014-11-18 17:05:35 -08001250 mSearchSrcTextView.setAdapter(mSuggestionsAdapter);
Amith Yamasanie678f462010-09-15 16:13:43 -07001251 ((SuggestionsAdapter) mSuggestionsAdapter).setQueryRefinement(
1252 mQueryRefinement ? SuggestionsAdapter.REFINE_ALL
1253 : SuggestionsAdapter.REFINE_BY_ENTRY);
Amith Yamasani733cbd52010-09-03 12:21:39 -07001254 }
1255 }
1256
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001257 /**
1258 * Update the visibility of the voice button. There are actually two voice search modes,
1259 * either of which will activate the button.
1260 * @param empty whether the search query text field is empty. If it is, then the other
Amith Yamasani79f74302011-03-08 14:16:35 -08001261 * criteria apply to make the voice button visible.
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001262 */
1263 private void updateVoiceButton(boolean empty) {
Amith Yamasani79f74302011-03-08 14:16:35 -08001264 int visibility = GONE;
Amith Yamasani167d69a2011-08-12 19:28:37 -07001265 if (mVoiceButtonEnabled && !isIconified() && empty) {
Amith Yamasani9b2e3022011-01-14 11:34:12 -08001266 visibility = VISIBLE;
Alan Viverettecb8ed372014-11-18 17:05:35 -08001267 mGoButton.setVisibility(GONE);
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001268 }
1269 mVoiceButton.setVisibility(visibility);
1270 }
1271
Amith Yamasani733cbd52010-09-03 12:21:39 -07001272 private final OnEditorActionListener mOnEditorActionListener = new OnEditorActionListener() {
1273
1274 /**
1275 * Called when the input method default action key is pressed.
1276 */
1277 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
1278 onSubmitQuery();
1279 return true;
1280 }
1281 };
1282
1283 private void onTextChanged(CharSequence newText) {
Alan Viverettecb8ed372014-11-18 17:05:35 -08001284 CharSequence text = mSearchSrcTextView.getText();
Amith Yamasani068d73c2011-05-27 15:15:14 -07001285 mUserQuery = text;
Amith Yamasani733cbd52010-09-03 12:21:39 -07001286 boolean hasText = !TextUtils.isEmpty(text);
Amith Yamasanicf72ab42011-11-04 13:49:28 -07001287 updateSubmitButton(hasText);
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001288 updateVoiceButton(!hasText);
Amith Yamasani73e00df2010-12-16 16:31:29 -08001289 updateCloseButton();
Amith Yamasani9b2e3022011-01-14 11:34:12 -08001290 updateSubmitArea();
Amith Yamasanib47c4fd2011-08-04 14:30:07 -07001291 if (mOnQueryChangeListener != null && !TextUtils.equals(newText, mOldQueryText)) {
Adam Powell01f21352011-01-20 18:30:10 -08001292 mOnQueryChangeListener.onQueryTextChange(newText.toString());
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001293 }
Amith Yamasanib47c4fd2011-08-04 14:30:07 -07001294 mOldQueryText = newText.toString();
Amith Yamasani733cbd52010-09-03 12:21:39 -07001295 }
1296
1297 private void onSubmitQuery() {
Alan Viverettecb8ed372014-11-18 17:05:35 -08001298 CharSequence query = mSearchSrcTextView.getText();
Amith Yamasani6a7421b2011-07-27 11:55:53 -07001299 if (query != null && TextUtils.getTrimmedLength(query) > 0) {
Amith Yamasani733cbd52010-09-03 12:21:39 -07001300 if (mOnQueryChangeListener == null
Adam Powell01f21352011-01-20 18:30:10 -08001301 || !mOnQueryChangeListener.onQueryTextSubmit(query.toString())) {
Amith Yamasani733cbd52010-09-03 12:21:39 -07001302 if (mSearchable != null) {
1303 launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null, query.toString());
1304 }
Yohei Yukawa5cfc1b42017-03-07 00:52:09 -08001305 mSearchSrcTextView.setImeVisibility(false);
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001306 dismissSuggestions();
Amith Yamasani733cbd52010-09-03 12:21:39 -07001307 }
1308 }
1309 }
1310
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001311 private void dismissSuggestions() {
Alan Viverettecb8ed372014-11-18 17:05:35 -08001312 mSearchSrcTextView.dismissDropDown();
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001313 }
1314
Mathew Inwood8c854f82018-09-14 12:35:36 +01001315 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
Amith Yamasani733cbd52010-09-03 12:21:39 -07001316 private void onCloseClicked() {
Alan Viverettecb8ed372014-11-18 17:05:35 -08001317 CharSequence text = mSearchSrcTextView.getText();
Amith Yamasani24652982011-06-23 16:16:05 -07001318 if (TextUtils.isEmpty(text)) {
1319 if (mIconifiedByDefault) {
Amith Yamasanib4569fb2011-07-08 15:25:39 -07001320 // If the app doesn't override the close behavior
1321 if (mOnCloseListener == null || !mOnCloseListener.onClose()) {
1322 // hide the keyboard and remove focus
1323 clearFocus();
1324 // collapse the search field
1325 updateViewsVisibility(true);
1326 }
Amith Yamasani05944762010-10-08 13:52:38 -07001327 }
Amith Yamasani24652982011-06-23 16:16:05 -07001328 } else {
Alan Viverettecb8ed372014-11-18 17:05:35 -08001329 mSearchSrcTextView.setText("");
1330 mSearchSrcTextView.requestFocus();
Yohei Yukawa5cfc1b42017-03-07 00:52:09 -08001331 mSearchSrcTextView.setImeVisibility(true);
Amith Yamasani24652982011-06-23 16:16:05 -07001332 }
1333
Amith Yamasani733cbd52010-09-03 12:21:39 -07001334 }
1335
1336 private void onSearchClicked() {
Amith Yamasani733cbd52010-09-03 12:21:39 -07001337 updateViewsVisibility(false);
Alan Viverettecb8ed372014-11-18 17:05:35 -08001338 mSearchSrcTextView.requestFocus();
Yohei Yukawa5cfc1b42017-03-07 00:52:09 -08001339 mSearchSrcTextView.setImeVisibility(true);
Amith Yamasani48385482010-12-03 14:43:52 -08001340 if (mOnSearchClickListener != null) {
1341 mOnSearchClickListener.onClick(this);
1342 }
Amith Yamasani733cbd52010-09-03 12:21:39 -07001343 }
1344
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001345 private void onVoiceClicked() {
1346 // guard against possible race conditions
1347 if (mSearchable == null) {
1348 return;
1349 }
1350 SearchableInfo searchable = mSearchable;
1351 try {
1352 if (searchable.getVoiceSearchLaunchWebSearch()) {
1353 Intent webSearchIntent = createVoiceWebSearchIntent(mVoiceWebSearchIntent,
1354 searchable);
1355 getContext().startActivity(webSearchIntent);
1356 } else if (searchable.getVoiceSearchLaunchRecognizer()) {
1357 Intent appSearchIntent = createVoiceAppSearchIntent(mVoiceAppSearchIntent,
1358 searchable);
1359 getContext().startActivity(appSearchIntent);
1360 }
1361 } catch (ActivityNotFoundException e) {
1362 // Should not happen, since we check the availability of
1363 // voice search before showing the button. But just in case...
1364 Log.w(LOG_TAG, "Could not find voice search activity");
1365 }
1366 }
1367
Amith Yamasani4aedb392010-12-15 16:04:57 -08001368 void onTextFocusChanged() {
Amith Yamasani79f74302011-03-08 14:16:35 -08001369 updateViewsVisibility(isIconified());
Amith Yamasania95e4882011-08-17 11:41:37 -07001370 // Delayed update to make sure that the focus has settled down and window focus changes
1371 // don't affect it. A synchronous update was not working.
1372 postUpdateFocusedState();
Alan Viverettecb8ed372014-11-18 17:05:35 -08001373 if (mSearchSrcTextView.hasFocus()) {
Amith Yamasanif28d1872011-07-26 12:21:03 -07001374 forceSuggestionQuery();
1375 }
Amith Yamasani4aedb392010-12-15 16:04:57 -08001376 }
1377
Amith Yamasanib4569fb2011-07-08 15:25:39 -07001378 @Override
Amith Yamasania95e4882011-08-17 11:41:37 -07001379 public void onWindowFocusChanged(boolean hasWindowFocus) {
1380 super.onWindowFocusChanged(hasWindowFocus);
1381
1382 postUpdateFocusedState();
Amith Yamasanib4569fb2011-07-08 15:25:39 -07001383 }
1384
Amith Yamasani763bc072011-07-22 11:53:47 -07001385 /**
1386 * {@inheritDoc}
1387 */
1388 @Override
1389 public void onActionViewCollapsed() {
Adam Powell99d0ce92012-12-10 13:11:47 -08001390 setQuery("", false);
Amith Yamasani10da5902011-07-26 16:14:26 -07001391 clearFocus();
1392 updateViewsVisibility(true);
Alan Viverettecb8ed372014-11-18 17:05:35 -08001393 mSearchSrcTextView.setImeOptions(mCollapsedImeOptions);
Amith Yamasani763bc072011-07-22 11:53:47 -07001394 mExpandedInActionView = false;
1395 }
1396
1397 /**
1398 * {@inheritDoc}
1399 */
1400 @Override
1401 public void onActionViewExpanded() {
Amith Yamasani434c73f2011-11-01 11:44:50 -07001402 if (mExpandedInActionView) return;
1403
Amith Yamasani763bc072011-07-22 11:53:47 -07001404 mExpandedInActionView = true;
Alan Viverettecb8ed372014-11-18 17:05:35 -08001405 mCollapsedImeOptions = mSearchSrcTextView.getImeOptions();
1406 mSearchSrcTextView.setImeOptions(mCollapsedImeOptions | EditorInfo.IME_FLAG_NO_FULLSCREEN);
1407 mSearchSrcTextView.setText("");
Amith Yamasani763bc072011-07-22 11:53:47 -07001408 setIconified(false);
1409 }
1410
Aurimas Liutikas13fdea02016-02-11 10:10:10 -08001411 static class SavedState extends BaseSavedState {
1412 boolean isIconified;
1413
1414 SavedState(Parcelable superState) {
1415 super(superState);
1416 }
1417
1418 public SavedState(Parcel source) {
1419 super(source);
1420 isIconified = (Boolean) source.readValue(null);
1421 }
1422
1423 @Override
1424 public void writeToParcel(Parcel dest, int flags) {
1425 super.writeToParcel(dest, flags);
1426 dest.writeValue(isIconified);
1427 }
1428
1429 @Override
1430 public String toString() {
1431 return "SearchView.SavedState{"
1432 + Integer.toHexString(System.identityHashCode(this))
1433 + " isIconified=" + isIconified + "}";
1434 }
Aurimas Liutikas7849a3d2016-02-26 15:27:31 -08001435
Jeff Sharkey9e8f83d2019-02-28 12:06:45 -07001436 public static final @android.annotation.NonNull Parcelable.Creator<SavedState> CREATOR =
Aurimas Liutikas7849a3d2016-02-26 15:27:31 -08001437 new Parcelable.Creator<SavedState>() {
1438 public SavedState createFromParcel(Parcel in) {
1439 return new SavedState(in);
1440 }
1441
1442 public SavedState[] newArray(int size) {
1443 return new SavedState[size];
1444 }
1445 };
Aurimas Liutikas13fdea02016-02-11 10:10:10 -08001446 }
1447
1448 @Override
1449 protected Parcelable onSaveInstanceState() {
1450 Parcelable superState = super.onSaveInstanceState();
1451 SavedState ss = new SavedState(superState);
1452 ss.isIconified = isIconified();
1453 return ss;
1454 }
1455
1456 @Override
1457 protected void onRestoreInstanceState(Parcelable state) {
1458 SavedState ss = (SavedState) state;
1459 super.onRestoreInstanceState(ss.getSuperState());
1460 updateViewsVisibility(ss.isIconified);
1461 requestLayout();
1462 }
1463
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001464 @Override
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -08001465 public CharSequence getAccessibilityClassName() {
1466 return SearchView.class.getName();
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001467 }
1468
Amith Yamasanib4569fb2011-07-08 15:25:39 -07001469 private void adjustDropDownSizeAndPosition() {
1470 if (mDropDownAnchor.getWidth() > 1) {
1471 Resources res = getContext().getResources();
1472 int anchorPadding = mSearchPlate.getPaddingLeft();
1473 Rect dropDownPadding = new Rect();
Fabrice Di Meglio3a69bea2012-07-26 13:50:10 -07001474 final boolean isLayoutRtl = isLayoutRtl();
Amith Yamasanib4569fb2011-07-08 15:25:39 -07001475 int iconOffset = mIconifiedByDefault
1476 ? res.getDimensionPixelSize(R.dimen.dropdownitem_icon_width)
1477 + res.getDimensionPixelSize(R.dimen.dropdownitem_text_padding_left)
1478 : 0;
Alan Viverettecb8ed372014-11-18 17:05:35 -08001479 mSearchSrcTextView.getDropDownBackground().getPadding(dropDownPadding);
Fabrice Di Meglio3a69bea2012-07-26 13:50:10 -07001480 int offset;
1481 if (isLayoutRtl) {
1482 offset = - dropDownPadding.left;
1483 } else {
1484 offset = anchorPadding - (dropDownPadding.left + iconOffset);
1485 }
Alan Viverettecb8ed372014-11-18 17:05:35 -08001486 mSearchSrcTextView.setDropDownHorizontalOffset(offset);
Fabrice Di Meglio3a69bea2012-07-26 13:50:10 -07001487 final int width = mDropDownAnchor.getWidth() + dropDownPadding.left
1488 + dropDownPadding.right + iconOffset - anchorPadding;
Alan Viverettecb8ed372014-11-18 17:05:35 -08001489 mSearchSrcTextView.setDropDownWidth(width);
Amith Yamasanib4569fb2011-07-08 15:25:39 -07001490 }
1491 }
1492
Amith Yamasani968ec932010-12-02 14:00:47 -08001493 private boolean onItemClicked(int position, int actionKey, String actionMsg) {
1494 if (mOnSuggestionListener == null
Adam Powell01f21352011-01-20 18:30:10 -08001495 || !mOnSuggestionListener.onSuggestionClick(position)) {
Amith Yamasani968ec932010-12-02 14:00:47 -08001496 launchSuggestion(position, KeyEvent.KEYCODE_UNKNOWN, null);
Yohei Yukawa5cfc1b42017-03-07 00:52:09 -08001497 mSearchSrcTextView.setImeVisibility(false);
Amith Yamasani968ec932010-12-02 14:00:47 -08001498 dismissSuggestions();
1499 return true;
1500 }
1501 return false;
1502 }
1503
1504 private boolean onItemSelected(int position) {
1505 if (mOnSuggestionListener == null
Adam Powell01f21352011-01-20 18:30:10 -08001506 || !mOnSuggestionListener.onSuggestionSelect(position)) {
Amith Yamasani968ec932010-12-02 14:00:47 -08001507 rewriteQueryFromSuggestion(position);
1508 return true;
1509 }
1510 return false;
1511 }
1512
Mathew Inwood978c6e22018-08-21 15:58:55 +01001513 @UnsupportedAppUsage
Amith Yamasani733cbd52010-09-03 12:21:39 -07001514 private final OnItemClickListener mOnItemClickListener = new OnItemClickListener() {
1515
1516 /**
1517 * Implements OnItemClickListener
1518 */
1519 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Amith Yamasani968ec932010-12-02 14:00:47 -08001520 if (DBG) Log.d(LOG_TAG, "onItemClick() position " + position);
1521 onItemClicked(position, KeyEvent.KEYCODE_UNKNOWN, null);
Amith Yamasani733cbd52010-09-03 12:21:39 -07001522 }
1523 };
1524
1525 private final OnItemSelectedListener mOnItemSelectedListener = new OnItemSelectedListener() {
1526
1527 /**
1528 * Implements OnItemSelectedListener
1529 */
1530 public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
Amith Yamasani968ec932010-12-02 14:00:47 -08001531 if (DBG) Log.d(LOG_TAG, "onItemSelected() position " + position);
1532 SearchView.this.onItemSelected(position);
Amith Yamasani733cbd52010-09-03 12:21:39 -07001533 }
1534
1535 /**
1536 * Implements OnItemSelectedListener
1537 */
1538 public void onNothingSelected(AdapterView<?> parent) {
1539 if (DBG)
1540 Log.d(LOG_TAG, "onNothingSelected()");
1541 }
1542 };
1543
1544 /**
1545 * Query rewriting.
1546 */
1547 private void rewriteQueryFromSuggestion(int position) {
Alan Viverettecb8ed372014-11-18 17:05:35 -08001548 CharSequence oldQuery = mSearchSrcTextView.getText();
Amith Yamasani733cbd52010-09-03 12:21:39 -07001549 Cursor c = mSuggestionsAdapter.getCursor();
1550 if (c == null) {
1551 return;
1552 }
1553 if (c.moveToPosition(position)) {
1554 // Get the new query from the suggestion.
1555 CharSequence newQuery = mSuggestionsAdapter.convertToString(c);
1556 if (newQuery != null) {
1557 // The suggestion rewrites the query.
1558 // Update the text field, without getting new suggestions.
1559 setQuery(newQuery);
1560 } else {
1561 // The suggestion does not rewrite the query, restore the user's query.
1562 setQuery(oldQuery);
1563 }
1564 } else {
1565 // We got a bad position, restore the user's query.
1566 setQuery(oldQuery);
1567 }
1568 }
1569
1570 /**
1571 * Launches an intent based on a suggestion.
1572 *
1573 * @param position The index of the suggestion to create the intent from.
1574 * @param actionKey The key code of the action key that was pressed,
1575 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
1576 * @param actionMsg The message for the action key that was pressed,
1577 * or <code>null</code> if none.
1578 * @return true if a successful launch, false if could not (e.g. bad position).
1579 */
1580 private boolean launchSuggestion(int position, int actionKey, String actionMsg) {
1581 Cursor c = mSuggestionsAdapter.getCursor();
1582 if ((c != null) && c.moveToPosition(position)) {
1583
1584 Intent intent = createIntentFromSuggestion(c, actionKey, actionMsg);
1585
1586 // launch the intent
1587 launchIntent(intent);
1588
1589 return true;
1590 }
1591 return false;
1592 }
1593
1594 /**
1595 * Launches an intent, including any special intent handling.
1596 */
1597 private void launchIntent(Intent intent) {
1598 if (intent == null) {
1599 return;
1600 }
1601 try {
1602 // If the intent was created from a suggestion, it will always have an explicit
1603 // component here.
1604 getContext().startActivity(intent);
1605 } catch (RuntimeException ex) {
1606 Log.e(LOG_TAG, "Failed launch activity: " + intent, ex);
1607 }
1608 }
1609
1610 /**
1611 * Sets the text in the query box, without updating the suggestions.
1612 */
Mathew Inwood8c854f82018-09-14 12:35:36 +01001613 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
Amith Yamasani733cbd52010-09-03 12:21:39 -07001614 private void setQuery(CharSequence query) {
Alan Viverettecb8ed372014-11-18 17:05:35 -08001615 mSearchSrcTextView.setText(query, true);
Amith Yamasanie678f462010-09-15 16:13:43 -07001616 // Move the cursor to the end
Alan Viverettecb8ed372014-11-18 17:05:35 -08001617 mSearchSrcTextView.setSelection(TextUtils.isEmpty(query) ? 0 : query.length());
Amith Yamasani733cbd52010-09-03 12:21:39 -07001618 }
1619
1620 private void launchQuerySearch(int actionKey, String actionMsg, String query) {
1621 String action = Intent.ACTION_SEARCH;
Amith Yamasanie678f462010-09-15 16:13:43 -07001622 Intent intent = createIntent(action, null, null, query, actionKey, actionMsg);
Amith Yamasani733cbd52010-09-03 12:21:39 -07001623 getContext().startActivity(intent);
1624 }
1625
1626 /**
1627 * Constructs an intent from the given information and the search dialog state.
1628 *
1629 * @param action Intent action.
1630 * @param data Intent data, or <code>null</code>.
1631 * @param extraData Data for {@link SearchManager#EXTRA_DATA_KEY} or <code>null</code>.
1632 * @param query Intent query, or <code>null</code>.
Amith Yamasani733cbd52010-09-03 12:21:39 -07001633 * @param actionKey The key code of the action key that was pressed,
1634 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
1635 * @param actionMsg The message for the action key that was pressed,
1636 * or <code>null</code> if none.
1637 * @param mode The search mode, one of the acceptable values for
1638 * {@link SearchManager#SEARCH_MODE}, or {@code null}.
1639 * @return The intent.
1640 */
1641 private Intent createIntent(String action, Uri data, String extraData, String query,
Amith Yamasanie678f462010-09-15 16:13:43 -07001642 int actionKey, String actionMsg) {
Amith Yamasani733cbd52010-09-03 12:21:39 -07001643 // Now build the Intent
1644 Intent intent = new Intent(action);
1645 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
1646 // We need CLEAR_TOP to avoid reusing an old task that has other activities
1647 // on top of the one we want. We don't want to do this in in-app search though,
1648 // as it can be destructive to the activity stack.
1649 if (data != null) {
1650 intent.setData(data);
1651 }
Amith Yamasani068d73c2011-05-27 15:15:14 -07001652 intent.putExtra(SearchManager.USER_QUERY, mUserQuery);
Amith Yamasani733cbd52010-09-03 12:21:39 -07001653 if (query != null) {
1654 intent.putExtra(SearchManager.QUERY, query);
1655 }
1656 if (extraData != null) {
1657 intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData);
1658 }
Amith Yamasani940ef382011-03-02 18:43:23 -08001659 if (mAppSearchData != null) {
1660 intent.putExtra(SearchManager.APP_DATA, mAppSearchData);
1661 }
Amith Yamasani733cbd52010-09-03 12:21:39 -07001662 if (actionKey != KeyEvent.KEYCODE_UNKNOWN) {
1663 intent.putExtra(SearchManager.ACTION_KEY, actionKey);
1664 intent.putExtra(SearchManager.ACTION_MSG, actionMsg);
1665 }
1666 intent.setComponent(mSearchable.getSearchActivity());
1667 return intent;
1668 }
1669
1670 /**
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001671 * Create and return an Intent that can launch the voice search activity for web search.
1672 */
1673 private Intent createVoiceWebSearchIntent(Intent baseIntent, SearchableInfo searchable) {
1674 Intent voiceIntent = new Intent(baseIntent);
1675 ComponentName searchActivity = searchable.getSearchActivity();
1676 voiceIntent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, searchActivity == null ? null
1677 : searchActivity.flattenToShortString());
1678 return voiceIntent;
1679 }
1680
1681 /**
1682 * Create and return an Intent that can launch the voice search activity, perform a specific
1683 * voice transcription, and forward the results to the searchable activity.
1684 *
1685 * @param baseIntent The voice app search intent to start from
1686 * @return A completely-configured intent ready to send to the voice search activity
1687 */
1688 private Intent createVoiceAppSearchIntent(Intent baseIntent, SearchableInfo searchable) {
1689 ComponentName searchActivity = searchable.getSearchActivity();
1690
1691 // create the necessary intent to set up a search-and-forward operation
1692 // in the voice search system. We have to keep the bundle separate,
1693 // because it becomes immutable once it enters the PendingIntent
1694 Intent queryIntent = new Intent(Intent.ACTION_SEARCH);
1695 queryIntent.setComponent(searchActivity);
1696 PendingIntent pending = PendingIntent.getActivity(getContext(), 0, queryIntent,
1697 PendingIntent.FLAG_ONE_SHOT);
1698
1699 // Now set up the bundle that will be inserted into the pending intent
1700 // when it's time to do the search. We always build it here (even if empty)
1701 // because the voice search activity will always need to insert "QUERY" into
1702 // it anyway.
1703 Bundle queryExtras = new Bundle();
Jorge Ruesga1bcfe842012-09-03 01:26:59 +02001704 if (mAppSearchData != null) {
1705 queryExtras.putParcelable(SearchManager.APP_DATA, mAppSearchData);
1706 }
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001707
1708 // Now build the intent to launch the voice search. Add all necessary
1709 // extras to launch the voice recognizer, and then all the necessary extras
1710 // to forward the results to the searchable activity
1711 Intent voiceIntent = new Intent(baseIntent);
1712
1713 // Add all of the configuration options supplied by the searchable's metadata
1714 String languageModel = RecognizerIntent.LANGUAGE_MODEL_FREE_FORM;
1715 String prompt = null;
1716 String language = null;
1717 int maxResults = 1;
1718
1719 Resources resources = getResources();
1720 if (searchable.getVoiceLanguageModeId() != 0) {
1721 languageModel = resources.getString(searchable.getVoiceLanguageModeId());
1722 }
1723 if (searchable.getVoicePromptTextId() != 0) {
1724 prompt = resources.getString(searchable.getVoicePromptTextId());
1725 }
1726 if (searchable.getVoiceLanguageId() != 0) {
1727 language = resources.getString(searchable.getVoiceLanguageId());
1728 }
1729 if (searchable.getVoiceMaxResults() != 0) {
1730 maxResults = searchable.getVoiceMaxResults();
1731 }
1732 voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, languageModel);
1733 voiceIntent.putExtra(RecognizerIntent.EXTRA_PROMPT, prompt);
1734 voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, language);
1735 voiceIntent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, maxResults);
1736 voiceIntent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, searchActivity == null ? null
1737 : searchActivity.flattenToShortString());
1738
1739 // Add the values that configure forwarding the results
1740 voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT, pending);
1741 voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT_BUNDLE, queryExtras);
1742
1743 return voiceIntent;
1744 }
1745
1746 /**
Amith Yamasani733cbd52010-09-03 12:21:39 -07001747 * When a particular suggestion has been selected, perform the various lookups required
1748 * to use the suggestion. This includes checking the cursor for suggestion-specific data,
1749 * and/or falling back to the XML for defaults; It also creates REST style Uri data when
1750 * the suggestion includes a data id.
1751 *
1752 * @param c The suggestions cursor, moved to the row of the user's selection
1753 * @param actionKey The key code of the action key that was pressed,
1754 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
1755 * @param actionMsg The message for the action key that was pressed,
1756 * or <code>null</code> if none.
1757 * @return An intent for the suggestion at the cursor's position.
1758 */
1759 private Intent createIntentFromSuggestion(Cursor c, int actionKey, String actionMsg) {
1760 try {
1761 // use specific action if supplied, or default action if supplied, or fixed default
1762 String action = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_ACTION);
1763
Amith Yamasani733cbd52010-09-03 12:21:39 -07001764 if (action == null) {
1765 action = mSearchable.getSuggestIntentAction();
1766 }
1767 if (action == null) {
1768 action = Intent.ACTION_SEARCH;
1769 }
1770
1771 // use specific data if supplied, or default data if supplied
1772 String data = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA);
1773 if (data == null) {
1774 data = mSearchable.getSuggestIntentData();
1775 }
1776 // then, if an ID was provided, append it.
1777 if (data != null) {
1778 String id = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID);
1779 if (id != null) {
1780 data = data + "/" + Uri.encode(id);
1781 }
1782 }
1783 Uri dataUri = (data == null) ? null : Uri.parse(data);
1784
Amith Yamasani733cbd52010-09-03 12:21:39 -07001785 String query = getColumnString(c, SearchManager.SUGGEST_COLUMN_QUERY);
1786 String extraData = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA);
1787
Amith Yamasanie678f462010-09-15 16:13:43 -07001788 return createIntent(action, dataUri, extraData, query, actionKey, actionMsg);
Amith Yamasani733cbd52010-09-03 12:21:39 -07001789 } catch (RuntimeException e ) {
1790 int rowNum;
1791 try { // be really paranoid now
1792 rowNum = c.getPosition();
1793 } catch (RuntimeException e2 ) {
1794 rowNum = -1;
1795 }
Jake Wharton73af4512012-07-06 23:15:44 -07001796 Log.w(LOG_TAG, "Search suggestions cursor at row " + rowNum +
1797 " returned exception.", e);
Amith Yamasani733cbd52010-09-03 12:21:39 -07001798 return null;
1799 }
1800 }
1801
Amith Yamasanif28d1872011-07-26 12:21:03 -07001802 private void forceSuggestionQuery() {
Alan Viverettecb8ed372014-11-18 17:05:35 -08001803 mSearchSrcTextView.doBeforeTextChanged();
1804 mSearchSrcTextView.doAfterTextChanged();
Amith Yamasanif28d1872011-07-26 12:21:03 -07001805 }
1806
Amith Yamasani968ec932010-12-02 14:00:47 -08001807 static boolean isLandscapeMode(Context context) {
1808 return context.getResources().getConfiguration().orientation
1809 == Configuration.ORIENTATION_LANDSCAPE;
1810 }
1811
Amith Yamasani733cbd52010-09-03 12:21:39 -07001812 /**
1813 * Callback to watch the text field for empty/non-empty
1814 */
1815 private TextWatcher mTextWatcher = new TextWatcher() {
1816
1817 public void beforeTextChanged(CharSequence s, int start, int before, int after) { }
1818
1819 public void onTextChanged(CharSequence s, int start,
1820 int before, int after) {
1821 SearchView.this.onTextChanged(s);
1822 }
1823
1824 public void afterTextChanged(Editable s) {
1825 }
1826 };
Amith Yamasani968ec932010-12-02 14:00:47 -08001827
Aurimas Liutikasc8fd00a2016-04-15 13:55:53 -07001828 private static class UpdatableTouchDelegate extends TouchDelegate {
1829 /**
1830 * View that should receive forwarded touch events
1831 */
1832 private final View mDelegateView;
1833
1834 /**
1835 * Bounds in local coordinates of the containing view that should be mapped to the delegate
1836 * view. This rect is used for initial hit testing.
1837 */
1838 private final Rect mTargetBounds;
1839
1840 /**
1841 * Bounds in local coordinates of the containing view that are actual bounds of the delegate
1842 * view. This rect is used for event coordinate mapping.
1843 */
1844 private final Rect mActualBounds;
1845
1846 /**
1847 * mTargetBounds inflated to include some slop. This rect is to track whether the motion events
1848 * should be considered to be be within the delegate view.
1849 */
1850 private final Rect mSlopBounds;
1851
1852 private final int mSlop;
1853
1854 /**
1855 * True if the delegate had been targeted on a down event (intersected mTargetBounds).
1856 */
1857 private boolean mDelegateTargeted;
1858
1859 public UpdatableTouchDelegate(Rect targetBounds, Rect actualBounds, View delegateView) {
1860 super(targetBounds, delegateView);
1861 mSlop = ViewConfiguration.get(delegateView.getContext()).getScaledTouchSlop();
1862 mTargetBounds = new Rect();
1863 mSlopBounds = new Rect();
1864 mActualBounds = new Rect();
1865 setBounds(targetBounds, actualBounds);
1866 mDelegateView = delegateView;
1867 }
1868
1869 public void setBounds(Rect desiredBounds, Rect actualBounds) {
1870 mTargetBounds.set(desiredBounds);
1871 mSlopBounds.set(desiredBounds);
1872 mSlopBounds.inset(-mSlop, -mSlop);
1873 mActualBounds.set(actualBounds);
1874 }
1875
1876 @Override
1877 public boolean onTouchEvent(MotionEvent event) {
1878 final int x = (int) event.getX();
1879 final int y = (int) event.getY();
1880 boolean sendToDelegate = false;
1881 boolean hit = true;
1882 boolean handled = false;
1883
1884 switch (event.getAction()) {
1885 case MotionEvent.ACTION_DOWN:
1886 if (mTargetBounds.contains(x, y)) {
1887 mDelegateTargeted = true;
1888 sendToDelegate = true;
1889 }
1890 break;
1891 case MotionEvent.ACTION_UP:
1892 case MotionEvent.ACTION_MOVE:
1893 sendToDelegate = mDelegateTargeted;
1894 if (sendToDelegate) {
1895 if (!mSlopBounds.contains(x, y)) {
1896 hit = false;
1897 }
1898 }
1899 break;
1900 case MotionEvent.ACTION_CANCEL:
1901 sendToDelegate = mDelegateTargeted;
1902 mDelegateTargeted = false;
1903 break;
1904 }
1905 if (sendToDelegate) {
1906 if (hit && !mActualBounds.contains(x, y)) {
1907 // Offset event coordinates to be in the center of the target view since we
1908 // are within the targetBounds, but not inside the actual bounds of
1909 // mDelegateView
1910 event.setLocation(mDelegateView.getWidth() / 2,
1911 mDelegateView.getHeight() / 2);
1912 } else {
1913 // Offset event coordinates to the target view coordinates.
1914 event.setLocation(x - mActualBounds.left, y - mActualBounds.top);
1915 }
1916
1917 handled = mDelegateView.dispatchTouchEvent(event);
1918 }
1919 return handled;
1920 }
1921 }
1922
Amith Yamasani968ec932010-12-02 14:00:47 -08001923 /**
1924 * Local subclass for AutoCompleteTextView.
1925 * @hide
1926 */
1927 public static class SearchAutoComplete extends AutoCompleteTextView {
1928
1929 private int mThreshold;
1930 private SearchView mSearchView;
1931
Yohei Yukawa5cfc1b42017-03-07 00:52:09 -08001932 private boolean mHasPendingShowSoftInputRequest;
1933 final Runnable mRunShowSoftInputIfNecessary = () -> showSoftInputIfNecessary();
1934
Amith Yamasani968ec932010-12-02 14:00:47 -08001935 public SearchAutoComplete(Context context) {
1936 super(context);
1937 mThreshold = getThreshold();
1938 }
1939
Mathew Inwood978c6e22018-08-21 15:58:55 +01001940 @UnsupportedAppUsage
Amith Yamasani968ec932010-12-02 14:00:47 -08001941 public SearchAutoComplete(Context context, AttributeSet attrs) {
1942 super(context, attrs);
1943 mThreshold = getThreshold();
1944 }
1945
Alan Viverette617feb92013-09-09 18:09:13 -07001946 public SearchAutoComplete(Context context, AttributeSet attrs, int defStyleAttrs) {
1947 super(context, attrs, defStyleAttrs);
1948 mThreshold = getThreshold();
1949 }
1950
1951 public SearchAutoComplete(
1952 Context context, AttributeSet attrs, int defStyleAttrs, int defStyleRes) {
1953 super(context, attrs, defStyleAttrs, defStyleRes);
Amith Yamasani968ec932010-12-02 14:00:47 -08001954 mThreshold = getThreshold();
1955 }
1956
Filip Gruszczynskib635fda2015-12-03 18:37:38 -08001957 @Override
1958 protected void onFinishInflate() {
1959 super.onFinishInflate();
1960 DisplayMetrics metrics = getResources().getDisplayMetrics();
1961 setMinWidth((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
1962 getSearchViewTextMinWidthDp(), metrics));
1963 }
1964
Amith Yamasani968ec932010-12-02 14:00:47 -08001965 void setSearchView(SearchView searchView) {
1966 mSearchView = searchView;
1967 }
1968
1969 @Override
1970 public void setThreshold(int threshold) {
1971 super.setThreshold(threshold);
1972 mThreshold = threshold;
1973 }
1974
1975 /**
1976 * Returns true if the text field is empty, or contains only whitespace.
1977 */
1978 private boolean isEmpty() {
1979 return TextUtils.getTrimmedLength(getText()) == 0;
1980 }
1981
1982 /**
1983 * We override this method to avoid replacing the query box text when a
1984 * suggestion is clicked.
1985 */
1986 @Override
1987 protected void replaceText(CharSequence text) {
1988 }
1989
1990 /**
1991 * We override this method to avoid an extra onItemClick being called on
1992 * the drop-down's OnItemClickListener by
1993 * {@link AutoCompleteTextView#onKeyUp(int, KeyEvent)} when an item is
1994 * clicked with the trackball.
1995 */
1996 @Override
1997 public void performCompletion() {
1998 }
1999
2000 /**
2001 * We override this method to be sure and show the soft keyboard if
2002 * appropriate when the TextView has focus.
2003 */
2004 @Override
2005 public void onWindowFocusChanged(boolean hasWindowFocus) {
2006 super.onWindowFocusChanged(hasWindowFocus);
2007
Amith Yamasaniacd8d2d2010-12-06 15:50:23 -08002008 if (hasWindowFocus && mSearchView.hasFocus() && getVisibility() == VISIBLE) {
Yohei Yukawa5cfc1b42017-03-07 00:52:09 -08002009 // Since InputMethodManager#onPostWindowFocus() will be called after this callback,
2010 // it is a bit too early to call InputMethodManager#showSoftInput() here. We still
2011 // need to wait until the system calls back onCreateInputConnection() to call
2012 // InputMethodManager#showSoftInput().
2013 mHasPendingShowSoftInputRequest = true;
2014
2015 // If in landscape mode, then make sure that the ime is in front of the dropdown.
Amith Yamasani968ec932010-12-02 14:00:47 -08002016 if (isLandscapeMode(getContext())) {
2017 ensureImeVisible(true);
2018 }
2019 }
2020 }
2021
Amith Yamasani4aedb392010-12-15 16:04:57 -08002022 @Override
2023 protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
2024 super.onFocusChanged(focused, direction, previouslyFocusedRect);
2025 mSearchView.onTextFocusChanged();
2026 }
2027
Amith Yamasani968ec932010-12-02 14:00:47 -08002028 /**
2029 * We override this method so that we can allow a threshold of zero,
2030 * which ACTV does not.
2031 */
2032 @Override
2033 public boolean enoughToFilter() {
2034 return mThreshold <= 0 || super.enoughToFilter();
2035 }
Amith Yamasanib4569fb2011-07-08 15:25:39 -07002036
2037 @Override
2038 public boolean onKeyPreIme(int keyCode, KeyEvent event) {
Evan Rosky758a6522018-03-06 19:11:12 -08002039 final boolean consume = super.onKeyPreIme(keyCode, event);
2040 if (consume && keyCode == KeyEvent.KEYCODE_BACK
2041 && event.getAction() == KeyEvent.ACTION_UP) {
2042 // If AutoCompleteTextView closed its pop-up, it will return true, in which case
2043 // we should also close the IME. Otherwise, the popup is already closed and we can
2044 // leave the BACK event alone.
2045 setImeVisibility(false);
Amith Yamasanib4569fb2011-07-08 15:25:39 -07002046 }
Evan Rosky758a6522018-03-06 19:11:12 -08002047 return consume;
Amith Yamasanib4569fb2011-07-08 15:25:39 -07002048 }
2049
Filip Gruszczynskib635fda2015-12-03 18:37:38 -08002050 /**
2051 * Get minimum width of the search view text entry area.
2052 */
2053 private int getSearchViewTextMinWidthDp() {
2054 final Configuration configuration = getResources().getConfiguration();
2055 final int width = configuration.screenWidthDp;
2056 final int height = configuration.screenHeightDp;
2057 final int orientation = configuration.orientation;
2058 if (width >= 960 && height >= 720
2059 && orientation == Configuration.ORIENTATION_LANDSCAPE) {
2060 return 256;
2061 } else if (width >= 600 || (width >= 640 && height >= 480)) {
2062 return 192;
2063 };
2064 return 160;
2065 }
Yohei Yukawa5cfc1b42017-03-07 00:52:09 -08002066
2067 /**
2068 * We override {@link View#onCreateInputConnection(EditorInfo)} as a signal to schedule a
2069 * pending {@link InputMethodManager#showSoftInput(View, int)} request (if any).
2070 */
2071 @Override
2072 public InputConnection onCreateInputConnection(EditorInfo editorInfo) {
2073 final InputConnection ic = super.onCreateInputConnection(editorInfo);
2074 if (mHasPendingShowSoftInputRequest) {
2075 removeCallbacks(mRunShowSoftInputIfNecessary);
2076 post(mRunShowSoftInputIfNecessary);
2077 }
2078 return ic;
2079 }
2080
Tarandeep Singhaf91ab72019-02-14 10:32:00 -08002081 @Override
2082 public boolean checkInputConnectionProxy(View view) {
2083 return view == mSearchView;
2084 }
2085
Yohei Yukawa5cfc1b42017-03-07 00:52:09 -08002086 private void showSoftInputIfNecessary() {
2087 if (mHasPendingShowSoftInputRequest) {
2088 final InputMethodManager imm =
2089 getContext().getSystemService(InputMethodManager.class);
2090 imm.showSoftInput(this, 0);
2091 mHasPendingShowSoftInputRequest = false;
2092 }
2093 }
2094
2095 private void setImeVisibility(final boolean visible) {
2096 final InputMethodManager imm = getContext().getSystemService(InputMethodManager.class);
2097 if (!visible) {
2098 mHasPendingShowSoftInputRequest = false;
2099 removeCallbacks(mRunShowSoftInputIfNecessary);
2100 imm.hideSoftInputFromWindow(getWindowToken(), 0);
2101 return;
2102 }
2103
2104 if (imm.isActive(this)) {
2105 // This means that SearchAutoComplete is already connected to the IME.
2106 // InputMethodManager#showSoftInput() is guaranteed to pass client-side focus check.
2107 mHasPendingShowSoftInputRequest = false;
2108 removeCallbacks(mRunShowSoftInputIfNecessary);
2109 imm.showSoftInput(this, 0);
2110 return;
2111 }
2112
2113 // Otherwise, InputMethodManager#showSoftInput() should be deferred after
2114 // onCreateInputConnection().
2115 mHasPendingShowSoftInputRequest = true;
2116 }
Amith Yamasani968ec932010-12-02 14:00:47 -08002117 }
Amith Yamasani05944762010-10-08 13:52:38 -07002118}