blob: 38221383df3ff98dfd4923284e75cc984ffaa6bb [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;
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -070022import android.app.PendingIntent;
Amith Yamasani733cbd52010-09-03 12:21:39 -070023import android.app.SearchManager;
24import android.app.SearchableInfo;
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -070025import android.content.ActivityNotFoundException;
26import android.content.ComponentName;
Amith Yamasani733cbd52010-09-03 12:21:39 -070027import android.content.Context;
28import android.content.Intent;
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -070029import android.content.pm.PackageManager;
30import android.content.pm.ResolveInfo;
Amith Yamasani968ec932010-12-02 14:00:47 -080031import android.content.res.Configuration;
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -070032import android.content.res.Resources;
Amith Yamasani733cbd52010-09-03 12:21:39 -070033import android.content.res.TypedArray;
34import android.database.Cursor;
repo sync6a81b822010-09-28 13:00:05 -070035import android.graphics.Rect;
Amith Yamasani733cbd52010-09-03 12:21:39 -070036import android.graphics.drawable.Drawable;
37import android.net.Uri;
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -070038import android.os.Bundle;
Aurimas Liutikas13fdea02016-02-11 10:10:10 -080039import android.os.Parcel;
40import android.os.Parcelable;
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -070041import android.speech.RecognizerIntent;
Amith Yamasani733cbd52010-09-03 12:21:39 -070042import android.text.Editable;
Amith Yamasani5607a382011-08-09 14:16:37 -070043import android.text.InputType;
Amith Yamasanib4569fb2011-07-08 15:25:39 -070044import android.text.Spannable;
45import android.text.SpannableStringBuilder;
Amith Yamasani733cbd52010-09-03 12:21:39 -070046import android.text.TextUtils;
47import android.text.TextWatcher;
Amith Yamasanib4569fb2011-07-08 15:25:39 -070048import android.text.style.ImageSpan;
Amith Yamasani733cbd52010-09-03 12:21:39 -070049import android.util.AttributeSet;
Filip Gruszczynskib635fda2015-12-03 18:37:38 -080050import android.util.DisplayMetrics;
Amith Yamasani733cbd52010-09-03 12:21:39 -070051import android.util.Log;
Filip Gruszczynskib635fda2015-12-03 18:37:38 -080052import android.util.TypedValue;
Amith Yamasani763bc072011-07-22 11:53:47 -070053import android.view.CollapsibleActionView;
Amith Yamasani733cbd52010-09-03 12:21:39 -070054import android.view.KeyEvent;
55import android.view.LayoutInflater;
Aurimas Liutikasc8fd00a2016-04-15 13:55:53 -070056import android.view.MotionEvent;
57import android.view.TouchDelegate;
Amith Yamasani733cbd52010-09-03 12:21:39 -070058import android.view.View;
Aurimas Liutikasc8fd00a2016-04-15 13:55:53 -070059import android.view.ViewConfiguration;
Amith Yamasani5607a382011-08-09 14:16:37 -070060import android.view.inputmethod.EditorInfo;
Amith Yamasani733cbd52010-09-03 12:21:39 -070061import android.view.inputmethod.InputMethodManager;
62import android.widget.AdapterView.OnItemClickListener;
63import android.widget.AdapterView.OnItemSelectedListener;
64import android.widget.TextView.OnEditorActionListener;
65
Amith Yamasanib4569fb2011-07-08 15:25:39 -070066import com.android.internal.R;
67
Amith Yamasani733cbd52010-09-03 12:21:39 -070068import java.util.WeakHashMap;
69
70/**
Amith Yamasani763bc072011-07-22 11:53:47 -070071 * A widget that provides a user interface for the user to enter a search query and submit a request
72 * to a search provider. Shows a list of query suggestions or results, if available, and allows the
73 * user to pick a suggestion or result to launch into.
Amith Yamasani5931b1f2010-10-18 16:13:14 -070074 *
Amith Yamasani763bc072011-07-22 11:53:47 -070075 * <p>
76 * When the SearchView is used in an ActionBar as an action view for a collapsible menu item, it
77 * needs to be set to iconified by default using {@link #setIconifiedByDefault(boolean)
78 * setIconifiedByDefault(true)}. This is the default, so nothing needs to be done.
79 * </p>
80 * <p>
81 * If you want the search field to always be visible, then call setIconifiedByDefault(false).
82 * </p>
Amith Yamasani5931b1f2010-10-18 16:13:14 -070083 *
Joe Fernandez3aef8e1d2011-12-20 10:38:34 -080084 * <div class="special reference">
85 * <h3>Developer Guides</h3>
86 * <p>For information about using {@code SearchView}, read the
87 * <a href="{@docRoot}guide/topics/search/index.html">Search</a> developer guide.</p>
88 * </div>
Amith Yamasani763bc072011-07-22 11:53:47 -070089 *
90 * @see android.view.MenuItem#SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW
Amith Yamasani5931b1f2010-10-18 16:13:14 -070091 * @attr ref android.R.styleable#SearchView_iconifiedByDefault
Amith Yamasani5607a382011-08-09 14:16:37 -070092 * @attr ref android.R.styleable#SearchView_imeOptions
93 * @attr ref android.R.styleable#SearchView_inputType
Amith Yamasani5931b1f2010-10-18 16:13:14 -070094 * @attr ref android.R.styleable#SearchView_maxWidth
Scott Mainabdf0d52011-02-08 10:20:27 -080095 * @attr ref android.R.styleable#SearchView_queryHint
Amith Yamasani733cbd52010-09-03 12:21:39 -070096 */
Amith Yamasani763bc072011-07-22 11:53:47 -070097public class SearchView extends LinearLayout implements CollapsibleActionView {
Amith Yamasani733cbd52010-09-03 12:21:39 -070098
99 private static final boolean DBG = false;
100 private static final String LOG_TAG = "SearchView";
101
Luca Zanolin535698c2011-10-06 13:36:15 +0100102 /**
103 * Private constant for removing the microphone in the keyboard.
104 */
105 private static final String IME_OPTION_NO_MICROPHONE = "nm";
106
Alan Viverettecb8ed372014-11-18 17:05:35 -0800107 private final SearchAutoComplete mSearchSrcTextView;
Alan Viverette5dddb702014-07-02 15:46:04 -0700108 private final View mSearchEditFrame;
109 private final View mSearchPlate;
110 private final View mSubmitArea;
111 private final ImageView mSearchButton;
Alan Viverettecb8ed372014-11-18 17:05:35 -0800112 private final ImageView mGoButton;
Alan Viverette5dddb702014-07-02 15:46:04 -0700113 private final ImageView mCloseButton;
114 private final ImageView mVoiceButton;
Alan Viverette5dddb702014-07-02 15:46:04 -0700115 private final View mDropDownAnchor;
Alan Viverettecb8ed372014-11-18 17:05:35 -0800116
Aurimas Liutikasc8fd00a2016-04-15 13:55:53 -0700117 private UpdatableTouchDelegate mTouchDelegate;
118 private Rect mSearchSrcTextViewBounds = new Rect();
119 private Rect mSearchSrtTextViewBoundsExpanded = new Rect();
120 private int[] mTemp = new int[2];
121 private int[] mTemp2 = new int[2];
122
Alan Viverettecb8ed372014-11-18 17:05:35 -0800123 /** Icon optionally displayed when the SearchView is collapsed. */
124 private final ImageView mCollapsedIcon;
125
126 /** Drawable used as an EditText hint. */
127 private final Drawable mSearchHintIcon;
Alan Viverette5dddb702014-07-02 15:46:04 -0700128
129 // Resources used by SuggestionsAdapter to display suggestions.
130 private final int mSuggestionRowLayout;
131 private final int mSuggestionCommitIconResId;
132
133 // Intents used for voice searching.
134 private final Intent mVoiceWebSearchIntent;
135 private final Intent mVoiceAppSearchIntent;
136
Alan Viveretteb4004df2015-04-29 16:55:42 -0700137 private final CharSequence mDefaultQueryHint;
138
Adam Powell01f21352011-01-20 18:30:10 -0800139 private OnQueryTextListener mOnQueryChangeListener;
Amith Yamasani733cbd52010-09-03 12:21:39 -0700140 private OnCloseListener mOnCloseListener;
Amith Yamasani05944762010-10-08 13:52:38 -0700141 private OnFocusChangeListener mOnQueryTextFocusChangeListener;
Adam Powell01f21352011-01-20 18:30:10 -0800142 private OnSuggestionListener mOnSuggestionListener;
Amith Yamasani48385482010-12-03 14:43:52 -0800143 private OnClickListener mOnSearchClickListener;
Amith Yamasani733cbd52010-09-03 12:21:39 -0700144
145 private boolean mIconifiedByDefault;
Amith Yamasani93227752010-09-14 10:10:54 -0700146 private boolean mIconified;
Amith Yamasani733cbd52010-09-03 12:21:39 -0700147 private CursorAdapter mSuggestionsAdapter;
Amith Yamasani733cbd52010-09-03 12:21:39 -0700148 private boolean mSubmitButtonEnabled;
149 private CharSequence mQueryHint;
Amith Yamasanie678f462010-09-15 16:13:43 -0700150 private boolean mQueryRefinement;
Amith Yamasani05944762010-10-08 13:52:38 -0700151 private boolean mClearingFocus;
Amith Yamasani5931b1f2010-10-18 16:13:14 -0700152 private int mMaxWidth;
Amith Yamasani9b2e3022011-01-14 11:34:12 -0800153 private boolean mVoiceButtonEnabled;
Amith Yamasanib47c4fd2011-08-04 14:30:07 -0700154 private CharSequence mOldQueryText;
Amith Yamasani068d73c2011-05-27 15:15:14 -0700155 private CharSequence mUserQuery;
Amith Yamasani763bc072011-07-22 11:53:47 -0700156 private boolean mExpandedInActionView;
Adam Powell53f56c42011-09-25 13:46:15 -0700157 private int mCollapsedImeOptions;
Amith Yamasani733cbd52010-09-03 12:21:39 -0700158
159 private SearchableInfo mSearchable;
Amith Yamasani940ef382011-03-02 18:43:23 -0800160 private Bundle mAppSearchData;
Amith Yamasani733cbd52010-09-03 12:21:39 -0700161
Adam Powellccdd4ee2011-07-27 20:05:14 -0700162 /*
163 * SearchView can be set expanded before the IME is ready to be shown during
164 * initial UI setup. The show operation is asynchronous to account for this.
165 */
166 private Runnable mShowImeRunnable = new Runnable() {
167 public void run() {
Yohei Yukawa777ef952015-11-25 20:32:24 -0800168 InputMethodManager imm = getContext().getSystemService(InputMethodManager.class);
Adam Powellccdd4ee2011-07-27 20:05:14 -0700169
170 if (imm != null) {
171 imm.showSoftInputUnchecked(0, null);
172 }
173 }
174 };
175
Amith Yamasania95e4882011-08-17 11:41:37 -0700176 private Runnable mUpdateDrawableStateRunnable = new Runnable() {
177 public void run() {
178 updateFocusedState();
179 }
180 };
181
Amith Yamasani87907642011-11-03 11:32:44 -0700182 private Runnable mReleaseCursorRunnable = new Runnable() {
183 public void run() {
184 if (mSuggestionsAdapter != null && mSuggestionsAdapter instanceof SuggestionsAdapter) {
185 mSuggestionsAdapter.changeCursor(null);
186 }
187 }
188 };
189
Amith Yamasani733cbd52010-09-03 12:21:39 -0700190 // A weak map of drawables we've gotten from other packages, so we don't load them
191 // more than once.
192 private final WeakHashMap<String, Drawable.ConstantState> mOutsideDrawablesCache =
193 new WeakHashMap<String, Drawable.ConstantState>();
194
195 /**
196 * Callbacks for changes to the query text.
197 */
Adam Powell01f21352011-01-20 18:30:10 -0800198 public interface OnQueryTextListener {
Amith Yamasani733cbd52010-09-03 12:21:39 -0700199
200 /**
201 * Called when the user submits the query. This could be due to a key press on the
202 * keyboard or due to pressing a submit button.
203 * The listener can override the standard behavior by returning true
204 * to indicate that it has handled the submit request. Otherwise return false to
205 * let the SearchView handle the submission by launching any associated intent.
206 *
207 * @param query the query text that is to be submitted
208 *
209 * @return true if the query has been handled by the listener, false to let the
210 * SearchView perform the default action.
211 */
Adam Powell01f21352011-01-20 18:30:10 -0800212 boolean onQueryTextSubmit(String query);
Amith Yamasani733cbd52010-09-03 12:21:39 -0700213
214 /**
215 * Called when the query text is changed by the user.
216 *
217 * @param newText the new content of the query text field.
218 *
219 * @return false if the SearchView should perform the default action of showing any
220 * suggestions if available, true if the action was handled by the listener.
221 */
Adam Powell01f21352011-01-20 18:30:10 -0800222 boolean onQueryTextChange(String newText);
Amith Yamasani733cbd52010-09-03 12:21:39 -0700223 }
224
225 public interface OnCloseListener {
226
227 /**
228 * The user is attempting to close the SearchView.
229 *
230 * @return true if the listener wants to override the default behavior of clearing the
231 * text field and dismissing it, false otherwise.
232 */
233 boolean onClose();
234 }
235
Amith Yamasani05944762010-10-08 13:52:38 -0700236 /**
237 * Callback interface for selection events on suggestions. These callbacks
238 * are only relevant when a SearchableInfo has been specified by {@link #setSearchableInfo}.
239 */
Adam Powell01f21352011-01-20 18:30:10 -0800240 public interface OnSuggestionListener {
Amith Yamasani05944762010-10-08 13:52:38 -0700241
242 /**
243 * Called when a suggestion was selected by navigating to it.
244 * @param position the absolute position in the list of suggestions.
245 *
246 * @return true if the listener handles the event and wants to override the default
247 * behavior of possibly rewriting the query based on the selected item, false otherwise.
248 */
Adam Powell01f21352011-01-20 18:30:10 -0800249 boolean onSuggestionSelect(int position);
Amith Yamasani05944762010-10-08 13:52:38 -0700250
251 /**
252 * Called when a suggestion was clicked.
253 * @param position the absolute position of the clicked item in the list of suggestions.
254 *
255 * @return true if the listener handles the event and wants to override the default
256 * behavior of launching any intent or submitting a search query specified on that item.
257 * Return false otherwise.
258 */
Adam Powell01f21352011-01-20 18:30:10 -0800259 boolean onSuggestionClick(int position);
Amith Yamasani05944762010-10-08 13:52:38 -0700260 }
261
Amith Yamasani733cbd52010-09-03 12:21:39 -0700262 public SearchView(Context context) {
263 this(context, null);
264 }
265
266 public SearchView(Context context, AttributeSet attrs) {
Alan Viverette5dddb702014-07-02 15:46:04 -0700267 this(context, attrs, R.attr.searchViewStyle);
Alan Viverette617feb92013-09-09 18:09:13 -0700268 }
269
270 public SearchView(Context context, AttributeSet attrs, int defStyleAttr) {
271 this(context, attrs, defStyleAttr, 0);
272 }
273
274 public SearchView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
275 super(context, attrs, defStyleAttr, defStyleRes);
Amith Yamasani733cbd52010-09-03 12:21:39 -0700276
Alan Viverette5dddb702014-07-02 15:46:04 -0700277 final TypedArray a = context.obtainStyledAttributes(
278 attrs, R.styleable.SearchView, defStyleAttr, defStyleRes);
279 final LayoutInflater inflater = (LayoutInflater) context.getSystemService(
280 Context.LAYOUT_INFLATER_SERVICE);
Alan Viverettecb8ed372014-11-18 17:05:35 -0800281 final int layoutResId = a.getResourceId(
282 R.styleable.SearchView_layout, R.layout.search_view);
Alan Viverette5dddb702014-07-02 15:46:04 -0700283 inflater.inflate(layoutResId, this, true);
Amith Yamasani733cbd52010-09-03 12:21:39 -0700284
Alan Viverettecb8ed372014-11-18 17:05:35 -0800285 mSearchSrcTextView = (SearchAutoComplete) findViewById(R.id.search_src_text);
286 mSearchSrcTextView.setSearchView(this);
Amith Yamasani968ec932010-12-02 14:00:47 -0800287
Amith Yamasani733cbd52010-09-03 12:21:39 -0700288 mSearchEditFrame = findViewById(R.id.search_edit_frame);
Amith Yamasani79f74302011-03-08 14:16:35 -0800289 mSearchPlate = findViewById(R.id.search_plate);
Amith Yamasani9b2e3022011-01-14 11:34:12 -0800290 mSubmitArea = findViewById(R.id.submit_area);
Alan Viverette5dddb702014-07-02 15:46:04 -0700291 mSearchButton = (ImageView) findViewById(R.id.search_button);
Alan Viverettecb8ed372014-11-18 17:05:35 -0800292 mGoButton = (ImageView) findViewById(R.id.search_go_btn);
Amith Yamasani4aedb392010-12-15 16:04:57 -0800293 mCloseButton = (ImageView) findViewById(R.id.search_close_btn);
Alan Viverette5dddb702014-07-02 15:46:04 -0700294 mVoiceButton = (ImageView) findViewById(R.id.search_voice_btn);
Alan Viverettecb8ed372014-11-18 17:05:35 -0800295 mCollapsedIcon = (ImageView) findViewById(R.id.search_mag_icon);
Amith Yamasani733cbd52010-09-03 12:21:39 -0700296
Alan Viverette5dddb702014-07-02 15:46:04 -0700297 // Set up icons and backgrounds.
298 mSearchPlate.setBackground(a.getDrawable(R.styleable.SearchView_queryBackground));
299 mSubmitArea.setBackground(a.getDrawable(R.styleable.SearchView_submitBackground));
Alan Viverettecb8ed372014-11-18 17:05:35 -0800300 mSearchButton.setImageDrawable(a.getDrawable(R.styleable.SearchView_searchIcon));
301 mGoButton.setImageDrawable(a.getDrawable(R.styleable.SearchView_goIcon));
Alan Viverette5dddb702014-07-02 15:46:04 -0700302 mCloseButton.setImageDrawable(a.getDrawable(R.styleable.SearchView_closeIcon));
303 mVoiceButton.setImageDrawable(a.getDrawable(R.styleable.SearchView_voiceIcon));
Alan Viverettecb8ed372014-11-18 17:05:35 -0800304 mCollapsedIcon.setImageDrawable(a.getDrawable(R.styleable.SearchView_searchIcon));
305
306 // Prior to L MR1, the search hint icon defaulted to searchIcon. If the
307 // style does not have an explicit value set, fall back to that.
308 if (a.hasValueOrEmpty(R.styleable.SearchView_searchHintIcon)) {
309 mSearchHintIcon = a.getDrawable(R.styleable.SearchView_searchHintIcon);
310 } else {
311 mSearchHintIcon = a.getDrawable(R.styleable.SearchView_searchIcon);
312 }
Alan Viverette5dddb702014-07-02 15:46:04 -0700313
314 // Extract dropdown layout resource IDs for later use.
Alan Viverette362f9842014-09-10 16:05:35 -0700315 mSuggestionRowLayout = a.getResourceId(R.styleable.SearchView_suggestionRowLayout,
316 R.layout.search_dropdown_item_icons_2line);
Alan Viverette5dddb702014-07-02 15:46:04 -0700317 mSuggestionCommitIconResId = a.getResourceId(R.styleable.SearchView_commitIcon, 0);
318
Amith Yamasani733cbd52010-09-03 12:21:39 -0700319 mSearchButton.setOnClickListener(mOnClickListener);
320 mCloseButton.setOnClickListener(mOnClickListener);
Alan Viverettecb8ed372014-11-18 17:05:35 -0800321 mGoButton.setOnClickListener(mOnClickListener);
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -0700322 mVoiceButton.setOnClickListener(mOnClickListener);
Alan Viverettecb8ed372014-11-18 17:05:35 -0800323 mSearchSrcTextView.setOnClickListener(mOnClickListener);
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -0700324
Alan Viverettecb8ed372014-11-18 17:05:35 -0800325 mSearchSrcTextView.addTextChangedListener(mTextWatcher);
326 mSearchSrcTextView.setOnEditorActionListener(mOnEditorActionListener);
327 mSearchSrcTextView.setOnItemClickListener(mOnItemClickListener);
328 mSearchSrcTextView.setOnItemSelectedListener(mOnItemSelectedListener);
329 mSearchSrcTextView.setOnKeyListener(mTextKeyListener);
Alan Viverette5dddb702014-07-02 15:46:04 -0700330
Luca Zanolin535698c2011-10-06 13:36:15 +0100331 // Inform any listener of focus changes
Alan Viverettecb8ed372014-11-18 17:05:35 -0800332 mSearchSrcTextView.setOnFocusChangeListener(new OnFocusChangeListener() {
Amith Yamasani05944762010-10-08 13:52:38 -0700333
334 public void onFocusChange(View v, boolean hasFocus) {
335 if (mOnQueryTextFocusChangeListener != null) {
336 mOnQueryTextFocusChangeListener.onFocusChange(SearchView.this, hasFocus);
337 }
338 }
339 });
Amith Yamasani733cbd52010-09-03 12:21:39 -0700340 setIconifiedByDefault(a.getBoolean(R.styleable.SearchView_iconifiedByDefault, true));
Alan Viverette5dddb702014-07-02 15:46:04 -0700341
342 final int maxWidth = a.getDimensionPixelSize(R.styleable.SearchView_maxWidth, -1);
Amith Yamasani5931b1f2010-10-18 16:13:14 -0700343 if (maxWidth != -1) {
344 setMaxWidth(maxWidth);
345 }
Alan Viverette5dddb702014-07-02 15:46:04 -0700346
Alan Viveretteb4004df2015-04-29 16:55:42 -0700347 mDefaultQueryHint = a.getText(R.styleable.SearchView_defaultQueryHint);
348 mQueryHint = a.getText(R.styleable.SearchView_queryHint);
Alan Viverette5dddb702014-07-02 15:46:04 -0700349
350 final int imeOptions = a.getInt(R.styleable.SearchView_imeOptions, -1);
Amith Yamasani5607a382011-08-09 14:16:37 -0700351 if (imeOptions != -1) {
352 setImeOptions(imeOptions);
353 }
Alan Viverette5dddb702014-07-02 15:46:04 -0700354
355 final int inputType = a.getInt(R.styleable.SearchView_inputType, -1);
Amith Yamasani5607a382011-08-09 14:16:37 -0700356 if (inputType != -1) {
357 setInputType(inputType);
358 }
359
Evan Rosky4c8c9632016-12-16 17:27:55 -0800360 if (getFocusable() == FOCUSABLE_AUTO) {
361 setFocusable(FOCUSABLE);
362 }
Adam Powellea4ecd62014-09-03 19:35:37 -0700363
Amith Yamasani733cbd52010-09-03 12:21:39 -0700364 a.recycle();
365
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -0700366 // Save voice intent for later queries/launching
367 mVoiceWebSearchIntent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH);
368 mVoiceWebSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
369 mVoiceWebSearchIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
370 RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH);
371
372 mVoiceAppSearchIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
373 mVoiceAppSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
374
Alan Viverettecb8ed372014-11-18 17:05:35 -0800375 mDropDownAnchor = findViewById(mSearchSrcTextView.getDropDownAnchor());
Amith Yamasanib4569fb2011-07-08 15:25:39 -0700376 if (mDropDownAnchor != null) {
377 mDropDownAnchor.addOnLayoutChangeListener(new OnLayoutChangeListener() {
378 @Override
379 public void onLayoutChange(View v, int left, int top, int right, int bottom,
380 int oldLeft, int oldTop, int oldRight, int oldBottom) {
381 adjustDropDownSizeAndPosition();
382 }
Amith Yamasanib4569fb2011-07-08 15:25:39 -0700383 });
384 }
385
Amith Yamasani733cbd52010-09-03 12:21:39 -0700386 updateViewsVisibility(mIconifiedByDefault);
Amith Yamasanib4569fb2011-07-08 15:25:39 -0700387 updateQueryHint();
Amith Yamasani733cbd52010-09-03 12:21:39 -0700388 }
389
Alan Viverette5dddb702014-07-02 15:46:04 -0700390 int getSuggestionRowLayout() {
391 return mSuggestionRowLayout;
392 }
393
394 int getSuggestionCommitIconResId() {
395 return mSuggestionCommitIconResId;
396 }
397
Amith Yamasani733cbd52010-09-03 12:21:39 -0700398 /**
399 * Sets the SearchableInfo for this SearchView. Properties in the SearchableInfo are used
400 * to display labels, hints, suggestions, create intents for launching search results screens
401 * and controlling other affordances such as a voice button.
402 *
403 * @param searchable a SearchableInfo can be retrieved from the SearchManager, for a specific
404 * activity or a global search provider.
405 */
406 public void setSearchableInfo(SearchableInfo searchable) {
407 mSearchable = searchable;
408 if (mSearchable != null) {
409 updateSearchAutoComplete();
Amith Yamasani79f74302011-03-08 14:16:35 -0800410 updateQueryHint();
Amith Yamasani733cbd52010-09-03 12:21:39 -0700411 }
Amith Yamasani9b2e3022011-01-14 11:34:12 -0800412 // Cache the voice search capability
413 mVoiceButtonEnabled = hasVoiceSearch();
Luca Zanolin535698c2011-10-06 13:36:15 +0100414
415 if (mVoiceButtonEnabled) {
416 // Disable the microphone on the keyboard, as a mic is displayed near the text box
417 // TODO: use imeOptions to disable voice input when the new API will be available
Alan Viverettecb8ed372014-11-18 17:05:35 -0800418 mSearchSrcTextView.setPrivateImeOptions(IME_OPTION_NO_MICROPHONE);
Luca Zanolin535698c2011-10-06 13:36:15 +0100419 }
Amith Yamasanib4569fb2011-07-08 15:25:39 -0700420 updateViewsVisibility(isIconified());
Amith Yamasani733cbd52010-09-03 12:21:39 -0700421 }
422
Amith Yamasani940ef382011-03-02 18:43:23 -0800423 /**
424 * Sets the APP_DATA for legacy SearchDialog use.
425 * @param appSearchData bundle provided by the app when launching the search dialog
426 * @hide
427 */
428 public void setAppSearchData(Bundle appSearchData) {
429 mAppSearchData = appSearchData;
430 }
431
Amith Yamasani5607a382011-08-09 14:16:37 -0700432 /**
433 * Sets the IME options on the query text field.
434 *
435 * @see TextView#setImeOptions(int)
436 * @param imeOptions the options to set on the query text field
437 *
438 * @attr ref android.R.styleable#SearchView_imeOptions
439 */
440 public void setImeOptions(int imeOptions) {
Alan Viverettecb8ed372014-11-18 17:05:35 -0800441 mSearchSrcTextView.setImeOptions(imeOptions);
Amith Yamasani5607a382011-08-09 14:16:37 -0700442 }
443
444 /**
Amith Yamasanieca59d32012-04-25 18:57:18 -0700445 * Returns the IME options set on the query text field.
446 * @return the ime options
447 * @see TextView#setImeOptions(int)
448 *
449 * @attr ref android.R.styleable#SearchView_imeOptions
450 */
451 public int getImeOptions() {
Alan Viverettecb8ed372014-11-18 17:05:35 -0800452 return mSearchSrcTextView.getImeOptions();
Amith Yamasanieca59d32012-04-25 18:57:18 -0700453 }
454
455 /**
Amith Yamasani5607a382011-08-09 14:16:37 -0700456 * Sets the input type on the query text field.
457 *
458 * @see TextView#setInputType(int)
459 * @param inputType the input type to set on the query text field
460 *
461 * @attr ref android.R.styleable#SearchView_inputType
462 */
463 public void setInputType(int inputType) {
Alan Viverettecb8ed372014-11-18 17:05:35 -0800464 mSearchSrcTextView.setInputType(inputType);
Amith Yamasani5607a382011-08-09 14:16:37 -0700465 }
466
Amith Yamasanieca59d32012-04-25 18:57:18 -0700467 /**
468 * Returns the input type set on the query text field.
469 * @return the input type
470 *
471 * @attr ref android.R.styleable#SearchView_inputType
472 */
473 public int getInputType() {
Alan Viverettecb8ed372014-11-18 17:05:35 -0800474 return mSearchSrcTextView.getInputType();
Amith Yamasanieca59d32012-04-25 18:57:18 -0700475 }
476
Amith Yamasani05944762010-10-08 13:52:38 -0700477 /** @hide */
478 @Override
479 public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
Amith Yamasani7f8aef62011-01-25 11:58:09 -0800480 // Don't accept focus if in the middle of clearing focus
481 if (mClearingFocus) return false;
482 // Check if SearchView is focusable.
483 if (!isFocusable()) return false;
484 // If it is not iconified, then give the focus to the text field
485 if (!isIconified()) {
Alan Viverettecb8ed372014-11-18 17:05:35 -0800486 boolean result = mSearchSrcTextView.requestFocus(direction, previouslyFocusedRect);
Amith Yamasanif28d1872011-07-26 12:21:03 -0700487 if (result) {
488 updateViewsVisibility(false);
489 }
Amith Yamasani7f8aef62011-01-25 11:58:09 -0800490 return result;
491 } else {
492 return super.requestFocus(direction, previouslyFocusedRect);
493 }
Amith Yamasani05944762010-10-08 13:52:38 -0700494 }
495
496 /** @hide */
497 @Override
498 public void clearFocus() {
499 mClearingFocus = true;
Amith Yamasani10da5902011-07-26 16:14:26 -0700500 setImeVisibility(false);
Amith Yamasani05944762010-10-08 13:52:38 -0700501 super.clearFocus();
Alan Viverettecb8ed372014-11-18 17:05:35 -0800502 mSearchSrcTextView.clearFocus();
Amith Yamasani05944762010-10-08 13:52:38 -0700503 mClearingFocus = false;
504 }
505
Amith Yamasani733cbd52010-09-03 12:21:39 -0700506 /**
507 * Sets a listener for user actions within the SearchView.
508 *
509 * @param listener the listener object that receives callbacks when the user performs
510 * actions in the SearchView such as clicking on buttons or typing a query.
511 */
Adam Powell01f21352011-01-20 18:30:10 -0800512 public void setOnQueryTextListener(OnQueryTextListener listener) {
Amith Yamasani733cbd52010-09-03 12:21:39 -0700513 mOnQueryChangeListener = listener;
514 }
515
516 /**
Amith Yamasani93227752010-09-14 10:10:54 -0700517 * Sets a listener to inform when the user closes the SearchView.
518 *
519 * @param listener the listener to call when the user closes the SearchView.
520 */
521 public void setOnCloseListener(OnCloseListener listener) {
522 mOnCloseListener = listener;
523 }
524
525 /**
Amith Yamasani05944762010-10-08 13:52:38 -0700526 * Sets a listener to inform when the focus of the query text field changes.
527 *
528 * @param listener the listener to inform of focus changes.
529 */
530 public void setOnQueryTextFocusChangeListener(OnFocusChangeListener listener) {
531 mOnQueryTextFocusChangeListener = listener;
532 }
533
534 /**
535 * Sets a listener to inform when a suggestion is focused or clicked.
536 *
537 * @param listener the listener to inform of suggestion selection events.
538 */
Adam Powell01f21352011-01-20 18:30:10 -0800539 public void setOnSuggestionListener(OnSuggestionListener listener) {
Amith Yamasani05944762010-10-08 13:52:38 -0700540 mOnSuggestionListener = listener;
541 }
542
543 /**
Amith Yamasani48385482010-12-03 14:43:52 -0800544 * Sets a listener to inform when the search button is pressed. This is only
Scott Maincccdbe92011-02-06 15:51:47 -0800545 * relevant when the text field is not visible by default. Calling {@link #setIconified
546 * setIconified(false)} can also cause this listener to be informed.
Amith Yamasani48385482010-12-03 14:43:52 -0800547 *
548 * @param listener the listener to inform when the search button is clicked or
549 * the text field is programmatically de-iconified.
550 */
551 public void setOnSearchClickListener(OnClickListener listener) {
552 mOnSearchClickListener = listener;
553 }
554
555 /**
556 * Returns the query string currently in the text field.
557 *
558 * @return the query string
559 */
560 public CharSequence getQuery() {
Alan Viverettecb8ed372014-11-18 17:05:35 -0800561 return mSearchSrcTextView.getText();
Amith Yamasani48385482010-12-03 14:43:52 -0800562 }
563
564 /**
Amith Yamasani733cbd52010-09-03 12:21:39 -0700565 * Sets a query string in the text field and optionally submits the query as well.
566 *
567 * @param query the query string. This replaces any query text already present in the
568 * text field.
569 * @param submit whether to submit the query right now or only update the contents of
570 * text field.
571 */
572 public void setQuery(CharSequence query, boolean submit) {
Alan Viverettecb8ed372014-11-18 17:05:35 -0800573 mSearchSrcTextView.setText(query);
Dmitri Plotnikov87c50252010-10-21 21:16:42 -0700574 if (query != null) {
Alan Viverettecb8ed372014-11-18 17:05:35 -0800575 mSearchSrcTextView.setSelection(mSearchSrcTextView.length());
Amith Yamasani068d73c2011-05-27 15:15:14 -0700576 mUserQuery = query;
Dmitri Plotnikov87c50252010-10-21 21:16:42 -0700577 }
578
Amith Yamasani733cbd52010-09-03 12:21:39 -0700579 // If the query is not empty and submit is requested, submit the query
580 if (submit && !TextUtils.isEmpty(query)) {
581 onSubmitQuery();
582 }
583 }
584
585 /**
Alan Viveretteb4004df2015-04-29 16:55:42 -0700586 * Sets the hint text to display in the query text field. This overrides
587 * any hint specified in the {@link SearchableInfo}.
588 * <p>
589 * This value may be specified as an empty string to prevent any query hint
590 * from being displayed.
Amith Yamasani733cbd52010-09-03 12:21:39 -0700591 *
Alan Viveretteb4004df2015-04-29 16:55:42 -0700592 * @param hint the hint text to display or {@code null} to clear
Scott Mainabdf0d52011-02-08 10:20:27 -0800593 * @attr ref android.R.styleable#SearchView_queryHint
Amith Yamasani733cbd52010-09-03 12:21:39 -0700594 */
Alan Viveretteb4004df2015-04-29 16:55:42 -0700595 public void setQueryHint(@Nullable CharSequence hint) {
Amith Yamasani733cbd52010-09-03 12:21:39 -0700596 mQueryHint = hint;
597 updateQueryHint();
598 }
599
600 /**
Alan Viveretteb4004df2015-04-29 16:55:42 -0700601 * Returns the hint text that will be displayed in the query text field.
602 * <p>
603 * The displayed query hint is chosen in the following order:
604 * <ol>
605 * <li>Non-null value set with {@link #setQueryHint(CharSequence)}
606 * <li>Value specified in XML using
607 * {@link android.R.styleable#SearchView_queryHint android:queryHint}
608 * <li>Valid string resource ID exposed by the {@link SearchableInfo} via
609 * {@link SearchableInfo#getHintId()}
610 * <li>Default hint provided by the theme against which the view was
611 * inflated
612 * </ol>
Amith Yamasanieca59d32012-04-25 18:57:18 -0700613 *
Alan Viveretteb4004df2015-04-29 16:55:42 -0700614 * @return the displayed query hint text, or {@code null} if none set
Amith Yamasanieca59d32012-04-25 18:57:18 -0700615 * @attr ref android.R.styleable#SearchView_queryHint
616 */
Alan Viveretteb4004df2015-04-29 16:55:42 -0700617 @Nullable
Amith Yamasanieca59d32012-04-25 18:57:18 -0700618 public CharSequence getQueryHint() {
Alan Viveretteb4004df2015-04-29 16:55:42 -0700619 final CharSequence hint;
Amith Yamasanieca59d32012-04-25 18:57:18 -0700620 if (mQueryHint != null) {
Alan Viveretteb4004df2015-04-29 16:55:42 -0700621 hint = mQueryHint;
622 } else if (mSearchable != null && mSearchable.getHintId() != 0) {
623 hint = getContext().getText(mSearchable.getHintId());
624 } else {
625 hint = mDefaultQueryHint;
Amith Yamasanieca59d32012-04-25 18:57:18 -0700626 }
Alan Viveretteb4004df2015-04-29 16:55:42 -0700627 return hint;
Amith Yamasanieca59d32012-04-25 18:57:18 -0700628 }
629
630 /**
Amith Yamasani733cbd52010-09-03 12:21:39 -0700631 * Sets the default or resting state of the search field. If true, a single search icon is
632 * shown by default and expands to show the text field and other buttons when pressed. Also,
633 * if the default state is iconified, then it collapses to that state when the close button
Amith Yamasani93227752010-09-14 10:10:54 -0700634 * is pressed. Changes to this property will take effect immediately.
Amith Yamasani733cbd52010-09-03 12:21:39 -0700635 *
Scott Maincccdbe92011-02-06 15:51:47 -0800636 * <p>The default value is true.</p>
Amith Yamasani93227752010-09-14 10:10:54 -0700637 *
638 * @param iconified whether the search field should be iconified by default
Scott Mainabdf0d52011-02-08 10:20:27 -0800639 *
640 * @attr ref android.R.styleable#SearchView_iconifiedByDefault
Amith Yamasani733cbd52010-09-03 12:21:39 -0700641 */
642 public void setIconifiedByDefault(boolean iconified) {
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -0700643 if (mIconifiedByDefault == iconified) return;
Amith Yamasani733cbd52010-09-03 12:21:39 -0700644 mIconifiedByDefault = iconified;
645 updateViewsVisibility(iconified);
Amith Yamasanib47c4fd2011-08-04 14:30:07 -0700646 updateQueryHint();
Amith Yamasani733cbd52010-09-03 12:21:39 -0700647 }
648
Amith Yamasani93227752010-09-14 10:10:54 -0700649 /**
650 * Returns the default iconified state of the search field.
651 * @return
Amith Yamasani3d3e7a52012-05-04 14:46:31 -0700652 *
653 * @attr ref android.R.styleable#SearchView_iconifiedByDefault
Amith Yamasani93227752010-09-14 10:10:54 -0700654 */
Amith Yamasani733cbd52010-09-03 12:21:39 -0700655 public boolean isIconfiedByDefault() {
656 return mIconifiedByDefault;
657 }
658
659 /**
Amith Yamasani93227752010-09-14 10:10:54 -0700660 * Iconifies or expands the SearchView. Any query text is cleared when iconified. This is
661 * a temporary state and does not override the default iconified state set by
662 * {@link #setIconifiedByDefault(boolean)}. If the default state is iconified, then
663 * a false here will only be valid until the user closes the field. And if the default
664 * state is expanded, then a true here will only clear the text field and not close it.
665 *
666 * @param iconify a true value will collapse the SearchView to an icon, while a false will
667 * expand it.
668 */
669 public void setIconified(boolean iconify) {
670 if (iconify) {
671 onCloseClicked();
672 } else {
673 onSearchClicked();
674 }
675 }
676
677 /**
678 * Returns the current iconified state of the SearchView.
679 *
680 * @return true if the SearchView is currently iconified, false if the search field is
681 * fully visible.
682 */
683 public boolean isIconified() {
684 return mIconified;
685 }
686
687 /**
Amith Yamasani733cbd52010-09-03 12:21:39 -0700688 * Enables showing a submit button when the query is non-empty. In cases where the SearchView
689 * is being used to filter the contents of the current activity and doesn't launch a separate
690 * results activity, then the submit button should be disabled.
691 *
692 * @param enabled true to show a submit button for submitting queries, false if a submit
693 * button is not required.
694 */
695 public void setSubmitButtonEnabled(boolean enabled) {
Amith Yamasani733cbd52010-09-03 12:21:39 -0700696 mSubmitButtonEnabled = enabled;
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -0700697 updateViewsVisibility(isIconified());
Amith Yamasani733cbd52010-09-03 12:21:39 -0700698 }
699
700 /**
701 * Returns whether the submit button is enabled when necessary or never displayed.
702 *
703 * @return whether the submit button is enabled automatically when necessary
704 */
705 public boolean isSubmitButtonEnabled() {
706 return mSubmitButtonEnabled;
707 }
708
Amith Yamasanie678f462010-09-15 16:13:43 -0700709 /**
710 * Specifies if a query refinement button should be displayed alongside each suggestion
711 * or if it should depend on the flags set in the individual items retrieved from the
712 * suggestions provider. Clicking on the query refinement button will replace the text
713 * in the query text field with the text from the suggestion. This flag only takes effect
714 * if a SearchableInfo has been specified with {@link #setSearchableInfo(SearchableInfo)}
715 * and not when using a custom adapter.
716 *
717 * @param enable true if all items should have a query refinement button, false if only
718 * those items that have a query refinement flag set should have the button.
719 *
720 * @see SearchManager#SUGGEST_COLUMN_FLAGS
721 * @see SearchManager#FLAG_QUERY_REFINEMENT
722 */
723 public void setQueryRefinementEnabled(boolean enable) {
724 mQueryRefinement = enable;
725 if (mSuggestionsAdapter instanceof SuggestionsAdapter) {
726 ((SuggestionsAdapter) mSuggestionsAdapter).setQueryRefinement(
727 enable ? SuggestionsAdapter.REFINE_ALL : SuggestionsAdapter.REFINE_BY_ENTRY);
728 }
729 }
730
731 /**
732 * Returns whether query refinement is enabled for all items or only specific ones.
733 * @return true if enabled for all items, false otherwise.
734 */
735 public boolean isQueryRefinementEnabled() {
736 return mQueryRefinement;
Amith Yamasani733cbd52010-09-03 12:21:39 -0700737 }
738
739 /**
740 * You can set a custom adapter if you wish. Otherwise the default adapter is used to
741 * display the suggestions from the suggestions provider associated with the SearchableInfo.
742 *
743 * @see #setSearchableInfo(SearchableInfo)
744 */
745 public void setSuggestionsAdapter(CursorAdapter adapter) {
746 mSuggestionsAdapter = adapter;
747
Alan Viverettecb8ed372014-11-18 17:05:35 -0800748 mSearchSrcTextView.setAdapter(mSuggestionsAdapter);
Amith Yamasani733cbd52010-09-03 12:21:39 -0700749 }
750
751 /**
752 * Returns the adapter used for suggestions, if any.
753 * @return the suggestions adapter
754 */
755 public CursorAdapter getSuggestionsAdapter() {
756 return mSuggestionsAdapter;
757 }
758
Amith Yamasani5931b1f2010-10-18 16:13:14 -0700759 /**
760 * Makes the view at most this many pixels wide
761 *
762 * @attr ref android.R.styleable#SearchView_maxWidth
763 */
764 public void setMaxWidth(int maxpixels) {
765 mMaxWidth = maxpixels;
766
767 requestLayout();
768 }
769
Amith Yamasanieca59d32012-04-25 18:57:18 -0700770 /**
771 * Gets the specified maximum width in pixels, if set. Returns zero if
772 * no maximum width was specified.
773 * @return the maximum width of the view
Amith Yamasani3d3e7a52012-05-04 14:46:31 -0700774 *
775 * @attr ref android.R.styleable#SearchView_maxWidth
Amith Yamasanieca59d32012-04-25 18:57:18 -0700776 */
777 public int getMaxWidth() {
778 return mMaxWidth;
779 }
780
Amith Yamasani5931b1f2010-10-18 16:13:14 -0700781 @Override
782 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Amith Yamasania95e4882011-08-17 11:41:37 -0700783 // Let the standard measurements take effect in iconified state.
784 if (isIconified()) {
785 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
786 return;
787 }
788
Amith Yamasani5931b1f2010-10-18 16:13:14 -0700789 int widthMode = MeasureSpec.getMode(widthMeasureSpec);
790 int width = MeasureSpec.getSize(widthMeasureSpec);
791
Amith Yamasani167d69a2011-08-12 19:28:37 -0700792 switch (widthMode) {
793 case MeasureSpec.AT_MOST:
794 // If there is an upper limit, don't exceed maximum width (explicit or implicit)
795 if (mMaxWidth > 0) {
796 width = Math.min(mMaxWidth, width);
797 } else {
798 width = Math.min(getPreferredWidth(), width);
799 }
800 break;
801 case MeasureSpec.EXACTLY:
802 // If an exact width is specified, still don't exceed any specified maximum width
803 if (mMaxWidth > 0) {
804 width = Math.min(mMaxWidth, width);
805 }
806 break;
807 case MeasureSpec.UNSPECIFIED:
808 // Use maximum width, if specified, else preferred width
809 width = mMaxWidth > 0 ? mMaxWidth : getPreferredWidth();
810 break;
Amith Yamasani5931b1f2010-10-18 16:13:14 -0700811 }
Amith Yamasani167d69a2011-08-12 19:28:37 -0700812 widthMode = MeasureSpec.EXACTLY;
Aurimas Liutikasc8fd00a2016-04-15 13:55:53 -0700813
814 int heightMode = MeasureSpec.getMode(heightMeasureSpec);
815 int height = MeasureSpec.getSize(heightMeasureSpec);
816
817 switch (heightMode) {
818 case MeasureSpec.AT_MOST:
Aurimas Liutikasc8fd00a2016-04-15 13:55:53 -0700819 height = Math.min(getPreferredHeight(), height);
820 break;
Aurimas Liutikasf6a50be2016-09-09 15:32:55 -0700821 case MeasureSpec.UNSPECIFIED:
822 height = getPreferredHeight();
823 break;
Aurimas Liutikasc8fd00a2016-04-15 13:55:53 -0700824 }
825 heightMode = MeasureSpec.EXACTLY;
826
827 super.onMeasure(MeasureSpec.makeMeasureSpec(width, widthMode),
828 MeasureSpec.makeMeasureSpec(height, heightMode));
829 }
830
831 @Override
832 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
833 super.onLayout(changed, left, top, right, bottom);
834
835 if (changed) {
836 // Expand mSearchSrcTextView touch target to be the height of the parent in order to
837 // allow it to be up to 48dp.
838 getChildBoundsWithinSearchView(mSearchSrcTextView, mSearchSrcTextViewBounds);
839 mSearchSrtTextViewBoundsExpanded.set(
840 mSearchSrcTextViewBounds.left, 0, mSearchSrcTextViewBounds.right, bottom - top);
841 if (mTouchDelegate == null) {
842 mTouchDelegate = new UpdatableTouchDelegate(mSearchSrtTextViewBoundsExpanded,
843 mSearchSrcTextViewBounds, mSearchSrcTextView);
844 setTouchDelegate(mTouchDelegate);
845 } else {
846 mTouchDelegate.setBounds(mSearchSrtTextViewBoundsExpanded, mSearchSrcTextViewBounds);
847 }
848 }
849 }
850
851 private void getChildBoundsWithinSearchView(View view, Rect rect) {
852 view.getLocationInWindow(mTemp);
853 getLocationInWindow(mTemp2);
854 final int top = mTemp[1] - mTemp2[1];
855 final int left = mTemp[0] - mTemp2[0];
856 rect.set(left , top, left + view.getWidth(), top + view.getHeight());
Amith Yamasani167d69a2011-08-12 19:28:37 -0700857 }
858
859 private int getPreferredWidth() {
860 return getContext().getResources()
861 .getDimensionPixelSize(R.dimen.search_view_preferred_width);
Amith Yamasani5931b1f2010-10-18 16:13:14 -0700862 }
863
Aurimas Liutikasc8fd00a2016-04-15 13:55:53 -0700864 private int getPreferredHeight() {
865 return getContext().getResources()
866 .getDimensionPixelSize(R.dimen.search_view_preferred_height);
867 }
868
Amith Yamasani93227752010-09-14 10:10:54 -0700869 private void updateViewsVisibility(final boolean collapsed) {
870 mIconified = collapsed;
Amith Yamasani733cbd52010-09-03 12:21:39 -0700871 // Visibility of views that are visible when collapsed
Amith Yamasani93227752010-09-14 10:10:54 -0700872 final int visCollapsed = collapsed ? VISIBLE : GONE;
Amith Yamasani05944762010-10-08 13:52:38 -0700873 // Is there text in the query
Alan Viverettecb8ed372014-11-18 17:05:35 -0800874 final boolean hasText = !TextUtils.isEmpty(mSearchSrcTextView.getText());
Amith Yamasani733cbd52010-09-03 12:21:39 -0700875
876 mSearchButton.setVisibility(visCollapsed);
Amith Yamasani9b2e3022011-01-14 11:34:12 -0800877 updateSubmitButton(hasText);
878 mSearchEditFrame.setVisibility(collapsed ? GONE : VISIBLE);
Alan Viverette53b165e2015-08-25 12:33:01 -0400879
880 final int iconVisibility;
881 if (mCollapsedIcon.getDrawable() == null || mIconifiedByDefault) {
882 iconVisibility = GONE;
883 } else {
884 iconVisibility = VISIBLE;
885 }
886 mCollapsedIcon.setVisibility(iconVisibility);
887
Amith Yamasani4aedb392010-12-15 16:04:57 -0800888 updateCloseButton();
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -0700889 updateVoiceButton(!hasText);
Amith Yamasani9b2e3022011-01-14 11:34:12 -0800890 updateSubmitArea();
Amith Yamasani9b2e3022011-01-14 11:34:12 -0800891 }
892
893 private boolean hasVoiceSearch() {
894 if (mSearchable != null && mSearchable.getVoiceSearchEnabled()) {
895 Intent testIntent = null;
896 if (mSearchable.getVoiceSearchLaunchWebSearch()) {
897 testIntent = mVoiceWebSearchIntent;
898 } else if (mSearchable.getVoiceSearchLaunchRecognizer()) {
899 testIntent = mVoiceAppSearchIntent;
900 }
901 if (testIntent != null) {
902 ResolveInfo ri = getContext().getPackageManager().resolveActivity(testIntent,
903 PackageManager.MATCH_DEFAULT_ONLY);
904 return ri != null;
905 }
906 }
907 return false;
908 }
909
910 private boolean isSubmitAreaEnabled() {
911 return (mSubmitButtonEnabled || mVoiceButtonEnabled) && !isIconified();
912 }
913
914 private void updateSubmitButton(boolean hasText) {
Amith Yamasani79f74302011-03-08 14:16:35 -0800915 int visibility = GONE;
Amith Yamasanicf72ab42011-11-04 13:49:28 -0700916 if (mSubmitButtonEnabled && isSubmitAreaEnabled() && hasFocus()
917 && (hasText || !mVoiceButtonEnabled)) {
Amith Yamasani79f74302011-03-08 14:16:35 -0800918 visibility = VISIBLE;
919 }
Alan Viverettecb8ed372014-11-18 17:05:35 -0800920 mGoButton.setVisibility(visibility);
Amith Yamasani9b2e3022011-01-14 11:34:12 -0800921 }
922
923 private void updateSubmitArea() {
924 int visibility = GONE;
Amith Yamasani79f74302011-03-08 14:16:35 -0800925 if (isSubmitAreaEnabled()
Alan Viverettecb8ed372014-11-18 17:05:35 -0800926 && (mGoButton.getVisibility() == VISIBLE
Amith Yamasani79f74302011-03-08 14:16:35 -0800927 || mVoiceButton.getVisibility() == VISIBLE)) {
928 visibility = VISIBLE;
Amith Yamasani9b2e3022011-01-14 11:34:12 -0800929 }
930 mSubmitArea.setVisibility(visibility);
Amith Yamasani733cbd52010-09-03 12:21:39 -0700931 }
932
Amith Yamasani4aedb392010-12-15 16:04:57 -0800933 private void updateCloseButton() {
Alan Viverettecb8ed372014-11-18 17:05:35 -0800934 final boolean hasText = !TextUtils.isEmpty(mSearchSrcTextView.getText());
Amith Yamasani4aedb392010-12-15 16:04:57 -0800935 // Should we show the close button? It is not shown if there's no focus,
936 // field is not iconified by default and there is no text in it.
Amith Yamasani763bc072011-07-22 11:53:47 -0700937 final boolean showClose = hasText || (mIconifiedByDefault && !mExpandedInActionView);
Amith Yamasani167d69a2011-08-12 19:28:37 -0700938 mCloseButton.setVisibility(showClose ? VISIBLE : GONE);
Alan Viverettecb8ed372014-11-18 17:05:35 -0800939 final Drawable closeButtonImg = mCloseButton.getDrawable();
940 if (closeButtonImg != null){
941 closeButtonImg.setState(hasText ? ENABLED_STATE_SET : EMPTY_STATE_SET);
942 }
Amith Yamasani4aedb392010-12-15 16:04:57 -0800943 }
944
Amith Yamasania95e4882011-08-17 11:41:37 -0700945 private void postUpdateFocusedState() {
946 post(mUpdateDrawableStateRunnable);
947 }
948
949 private void updateFocusedState() {
Alan Viverettecb8ed372014-11-18 17:05:35 -0800950 final boolean focused = mSearchSrcTextView.hasFocus();
951 final int[] stateSet = focused ? FOCUSED_STATE_SET : EMPTY_STATE_SET;
952 final Drawable searchPlateBg = mSearchPlate.getBackground();
953 if (searchPlateBg != null) {
954 searchPlateBg.setState(stateSet);
955 }
956 final Drawable submitAreaBg = mSubmitArea.getBackground();
957 if (submitAreaBg != null) {
958 submitAreaBg.setState(stateSet);
959 }
Amith Yamasania95e4882011-08-17 11:41:37 -0700960 invalidate();
961 }
962
963 @Override
Amith Yamasania465b2d2011-08-19 13:01:22 -0700964 protected void onDetachedFromWindow() {
Amith Yamasania95e4882011-08-17 11:41:37 -0700965 removeCallbacks(mUpdateDrawableStateRunnable);
Amith Yamasani87907642011-11-03 11:32:44 -0700966 post(mReleaseCursorRunnable);
Amith Yamasania95e4882011-08-17 11:41:37 -0700967 super.onDetachedFromWindow();
Amith Yamasani79f74302011-03-08 14:16:35 -0800968 }
969
Adam Powellccdd4ee2011-07-27 20:05:14 -0700970 private void setImeVisibility(final boolean visible) {
971 if (visible) {
972 post(mShowImeRunnable);
973 } else {
974 removeCallbacks(mShowImeRunnable);
Yohei Yukawa777ef952015-11-25 20:32:24 -0800975 InputMethodManager imm = getContext().getSystemService(InputMethodManager.class);
repo sync6a81b822010-09-28 13:00:05 -0700976
Adam Powellccdd4ee2011-07-27 20:05:14 -0700977 if (imm != null) {
Amith Yamasani05944762010-10-08 13:52:38 -0700978 imm.hideSoftInputFromWindow(getWindowToken(), 0);
Amith Yamasani733cbd52010-09-03 12:21:39 -0700979 }
980 }
981 }
982
Amith Yamasanie678f462010-09-15 16:13:43 -0700983 /**
984 * Called by the SuggestionsAdapter
985 * @hide
986 */
987 /* package */void onQueryRefine(CharSequence queryText) {
988 setQuery(queryText);
989 }
990
Amith Yamasani733cbd52010-09-03 12:21:39 -0700991 private final OnClickListener mOnClickListener = new OnClickListener() {
992
993 public void onClick(View v) {
994 if (v == mSearchButton) {
995 onSearchClicked();
996 } else if (v == mCloseButton) {
997 onCloseClicked();
Alan Viverettecb8ed372014-11-18 17:05:35 -0800998 } else if (v == mGoButton) {
Amith Yamasani733cbd52010-09-03 12:21:39 -0700999 onSubmitQuery();
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001000 } else if (v == mVoiceButton) {
1001 onVoiceClicked();
Alan Viverettecb8ed372014-11-18 17:05:35 -08001002 } else if (v == mSearchSrcTextView) {
Amith Yamasanif28d1872011-07-26 12:21:03 -07001003 forceSuggestionQuery();
Amith Yamasani733cbd52010-09-03 12:21:39 -07001004 }
1005 }
1006 };
1007
1008 /**
1009 * Handles the key down event for dealing with action keys.
1010 *
1011 * @param keyCode This is the keycode of the typed key, and is the same value as
1012 * found in the KeyEvent parameter.
1013 * @param event The complete event record for the typed key
1014 *
1015 * @return true if the event was handled here, or false if not.
1016 */
1017 @Override
1018 public boolean onKeyDown(int keyCode, KeyEvent event) {
1019 if (mSearchable == null) {
1020 return false;
1021 }
1022
1023 // if it's an action specified by the searchable activity, launch the
1024 // entered query with the action key
1025 SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
1026 if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) {
Alan Viverettecb8ed372014-11-18 17:05:35 -08001027 launchQuerySearch(keyCode, actionKey.getQueryActionMsg(), mSearchSrcTextView.getText()
Amith Yamasani93227752010-09-14 10:10:54 -07001028 .toString());
Amith Yamasani733cbd52010-09-03 12:21:39 -07001029 return true;
1030 }
1031
1032 return super.onKeyDown(keyCode, event);
1033 }
1034
Amith Yamasani968ec932010-12-02 14:00:47 -08001035 /**
1036 * React to the user typing "enter" or other hardwired keys while typing in
1037 * the search box. This handles these special keys while the edit box has
1038 * focus.
1039 */
1040 View.OnKeyListener mTextKeyListener = new View.OnKeyListener() {
1041 public boolean onKey(View v, int keyCode, KeyEvent event) {
1042 // guard against possible race conditions
1043 if (mSearchable == null) {
1044 return false;
1045 }
1046
1047 if (DBG) {
1048 Log.d(LOG_TAG, "mTextListener.onKey(" + keyCode + "," + event + "), selection: "
Alan Viverettecb8ed372014-11-18 17:05:35 -08001049 + mSearchSrcTextView.getListSelection());
Amith Yamasani968ec932010-12-02 14:00:47 -08001050 }
1051
1052 // If a suggestion is selected, handle enter, search key, and action keys
1053 // as presses on the selected suggestion
Alan Viverettecb8ed372014-11-18 17:05:35 -08001054 if (mSearchSrcTextView.isPopupShowing()
1055 && mSearchSrcTextView.getListSelection() != ListView.INVALID_POSITION) {
Amith Yamasani968ec932010-12-02 14:00:47 -08001056 return onSuggestionsKey(v, keyCode, event);
1057 }
1058
1059 // If there is text in the query box, handle enter, and action keys
1060 // The search key is handled by the dialog's onKeyDown().
Alan Viverettecb8ed372014-11-18 17:05:35 -08001061 if (!mSearchSrcTextView.isEmpty() && event.hasNoModifiers()) {
Jeff Brown4e6319b2010-12-13 10:36:51 -08001062 if (event.getAction() == KeyEvent.ACTION_UP) {
1063 if (keyCode == KeyEvent.KEYCODE_ENTER) {
1064 v.cancelLongPress();
Amith Yamasani968ec932010-12-02 14:00:47 -08001065
Jeff Brown4e6319b2010-12-13 10:36:51 -08001066 // Launch as a regular search.
Alan Viverettecb8ed372014-11-18 17:05:35 -08001067 launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null, mSearchSrcTextView.getText()
Jeff Brown4e6319b2010-12-13 10:36:51 -08001068 .toString());
1069 return true;
1070 }
Amith Yamasani968ec932010-12-02 14:00:47 -08001071 }
1072 if (event.getAction() == KeyEvent.ACTION_DOWN) {
1073 SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
1074 if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) {
Alan Viverettecb8ed372014-11-18 17:05:35 -08001075 launchQuerySearch(keyCode, actionKey.getQueryActionMsg(), mSearchSrcTextView
Amith Yamasani968ec932010-12-02 14:00:47 -08001076 .getText().toString());
1077 return true;
1078 }
1079 }
1080 }
1081 return false;
1082 }
1083 };
1084
1085 /**
1086 * React to the user typing while in the suggestions list. First, check for
1087 * action keys. If not handled, try refocusing regular characters into the
1088 * EditText.
1089 */
1090 private boolean onSuggestionsKey(View v, int keyCode, KeyEvent event) {
1091 // guard against possible race conditions (late arrival after dismiss)
1092 if (mSearchable == null) {
1093 return false;
1094 }
1095 if (mSuggestionsAdapter == null) {
1096 return false;
1097 }
Jeff Brown4e6319b2010-12-13 10:36:51 -08001098 if (event.getAction() == KeyEvent.ACTION_DOWN && event.hasNoModifiers()) {
Amith Yamasani968ec932010-12-02 14:00:47 -08001099 // First, check for enter or search (both of which we'll treat as a
1100 // "click")
Jeff Brown4e6319b2010-12-13 10:36:51 -08001101 if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_SEARCH
1102 || keyCode == KeyEvent.KEYCODE_TAB) {
Alan Viverettecb8ed372014-11-18 17:05:35 -08001103 int position = mSearchSrcTextView.getListSelection();
Amith Yamasani968ec932010-12-02 14:00:47 -08001104 return onItemClicked(position, KeyEvent.KEYCODE_UNKNOWN, null);
1105 }
1106
1107 // Next, check for left/right moves, which we use to "return" the
1108 // user to the edit view
1109 if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
1110 // give "focus" to text editor, with cursor at the beginning if
1111 // left key, at end if right key
1112 // TODO: Reverse left/right for right-to-left languages, e.g.
1113 // Arabic
Alan Viverettecb8ed372014-11-18 17:05:35 -08001114 int selPoint = (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) ? 0 : mSearchSrcTextView
Amith Yamasani968ec932010-12-02 14:00:47 -08001115 .length();
Alan Viverettecb8ed372014-11-18 17:05:35 -08001116 mSearchSrcTextView.setSelection(selPoint);
1117 mSearchSrcTextView.setListSelection(0);
1118 mSearchSrcTextView.clearListSelection();
1119 mSearchSrcTextView.ensureImeVisible(true);
Amith Yamasani968ec932010-12-02 14:00:47 -08001120
1121 return true;
1122 }
1123
1124 // Next, check for an "up and out" move
Alan Viverettecb8ed372014-11-18 17:05:35 -08001125 if (keyCode == KeyEvent.KEYCODE_DPAD_UP && 0 == mSearchSrcTextView.getListSelection()) {
Amith Yamasani968ec932010-12-02 14:00:47 -08001126 // TODO: restoreUserQuery();
1127 // let ACTV complete the move
1128 return false;
1129 }
1130
1131 // Next, check for an "action key"
1132 SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
1133 if ((actionKey != null)
1134 && ((actionKey.getSuggestActionMsg() != null) || (actionKey
1135 .getSuggestActionMsgColumn() != null))) {
1136 // launch suggestion using action key column
Alan Viverettecb8ed372014-11-18 17:05:35 -08001137 int position = mSearchSrcTextView.getListSelection();
Amith Yamasani968ec932010-12-02 14:00:47 -08001138 if (position != ListView.INVALID_POSITION) {
1139 Cursor c = mSuggestionsAdapter.getCursor();
1140 if (c.moveToPosition(position)) {
1141 final String actionMsg = getActionKeyMessage(c, actionKey);
1142 if (actionMsg != null && (actionMsg.length() > 0)) {
1143 return onItemClicked(position, keyCode, actionMsg);
1144 }
1145 }
1146 }
1147 }
1148 }
1149 return false;
1150 }
1151
1152 /**
1153 * For a given suggestion and a given cursor row, get the action message. If
1154 * not provided by the specific row/column, also check for a single
1155 * definition (for the action key).
1156 *
1157 * @param c The cursor providing suggestions
1158 * @param actionKey The actionkey record being examined
1159 *
1160 * @return Returns a string, or null if no action key message for this
1161 * suggestion
1162 */
1163 private static String getActionKeyMessage(Cursor c, SearchableInfo.ActionKeyInfo actionKey) {
1164 String result = null;
1165 // check first in the cursor data, for a suggestion-specific message
1166 final String column = actionKey.getSuggestActionMsgColumn();
1167 if (column != null) {
1168 result = SuggestionsAdapter.getColumnString(c, column);
1169 }
1170 // If the cursor didn't give us a message, see if there's a single
1171 // message defined
1172 // for the actionkey (for all suggestions)
1173 if (result == null) {
1174 result = actionKey.getSuggestActionMsg();
1175 }
1176 return result;
1177 }
1178
Amith Yamasanib4569fb2011-07-08 15:25:39 -07001179 private CharSequence getDecoratedHint(CharSequence hintText) {
Alan Viverettecb8ed372014-11-18 17:05:35 -08001180 // If the field is always expanded or we don't have a search hint icon,
1181 // then don't add the search icon to the hint.
1182 if (!mIconifiedByDefault || mSearchHintIcon == null) {
Alan Viverette5dddb702014-07-02 15:46:04 -07001183 return hintText;
1184 }
Amith Yamasanib4569fb2011-07-08 15:25:39 -07001185
Alan Viverettecb8ed372014-11-18 17:05:35 -08001186 final int textSize = (int) (mSearchSrcTextView.getTextSize() * 1.25);
1187 mSearchHintIcon.setBounds(0, 0, textSize, textSize);
Alan Viverette5dddb702014-07-02 15:46:04 -07001188
Alan Viverettecb8ed372014-11-18 17:05:35 -08001189 final SpannableStringBuilder ssb = new SpannableStringBuilder(" ");
1190 ssb.setSpan(new ImageSpan(mSearchHintIcon), 1, 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
Alan Viverette5dddb702014-07-02 15:46:04 -07001191 ssb.append(hintText);
Amith Yamasanib4569fb2011-07-08 15:25:39 -07001192 return ssb;
1193 }
1194
Amith Yamasani733cbd52010-09-03 12:21:39 -07001195 private void updateQueryHint() {
Alan Viveretteb4004df2015-04-29 16:55:42 -07001196 final CharSequence hint = getQueryHint();
1197 mSearchSrcTextView.setHint(getDecoratedHint(hint == null ? "" : hint));
Amith Yamasani733cbd52010-09-03 12:21:39 -07001198 }
1199
1200 /**
1201 * Updates the auto-complete text view.
1202 */
1203 private void updateSearchAutoComplete() {
Alan Viverettecb8ed372014-11-18 17:05:35 -08001204 mSearchSrcTextView.setDropDownAnimationStyle(0); // no animation
1205 mSearchSrcTextView.setThreshold(mSearchable.getSuggestThreshold());
1206 mSearchSrcTextView.setImeOptions(mSearchable.getImeOptions());
Amith Yamasani5607a382011-08-09 14:16:37 -07001207 int inputType = mSearchable.getInputType();
1208 // We only touch this if the input type is set up for text (which it almost certainly
1209 // should be, in the case of search!)
1210 if ((inputType & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_TEXT) {
1211 // The existence of a suggestions authority is the proxy for "suggestions
1212 // are available here"
1213 inputType &= ~InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;
1214 if (mSearchable.getSuggestAuthority() != null) {
1215 inputType |= InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;
Satoshi Kataoka9ce11162012-06-04 17:12:08 +09001216 // TYPE_TEXT_FLAG_AUTO_COMPLETE means that the text editor is performing
1217 // auto-completion based on its own semantics, which it will present to the user
1218 // as they type. This generally means that the input method should not show its
1219 // own candidates, and the spell checker should not be in action. The text editor
1220 // supplies its candidates by calling InputMethodManager.displayCompletions(),
1221 // which in turn will call InputMethodSession.displayCompletions().
1222 inputType |= InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
Amith Yamasani5607a382011-08-09 14:16:37 -07001223 }
1224 }
Alan Viverettecb8ed372014-11-18 17:05:35 -08001225 mSearchSrcTextView.setInputType(inputType);
Amith Yamasani87907642011-11-03 11:32:44 -07001226 if (mSuggestionsAdapter != null) {
1227 mSuggestionsAdapter.changeCursor(null);
1228 }
Amith Yamasani733cbd52010-09-03 12:21:39 -07001229 // attach the suggestions adapter, if suggestions are available
1230 // The existence of a suggestions authority is the proxy for "suggestions available here"
1231 if (mSearchable.getSuggestAuthority() != null) {
1232 mSuggestionsAdapter = new SuggestionsAdapter(getContext(),
1233 this, mSearchable, mOutsideDrawablesCache);
Alan Viverettecb8ed372014-11-18 17:05:35 -08001234 mSearchSrcTextView.setAdapter(mSuggestionsAdapter);
Amith Yamasanie678f462010-09-15 16:13:43 -07001235 ((SuggestionsAdapter) mSuggestionsAdapter).setQueryRefinement(
1236 mQueryRefinement ? SuggestionsAdapter.REFINE_ALL
1237 : SuggestionsAdapter.REFINE_BY_ENTRY);
Amith Yamasani733cbd52010-09-03 12:21:39 -07001238 }
1239 }
1240
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001241 /**
1242 * Update the visibility of the voice button. There are actually two voice search modes,
1243 * either of which will activate the button.
1244 * @param empty whether the search query text field is empty. If it is, then the other
Amith Yamasani79f74302011-03-08 14:16:35 -08001245 * criteria apply to make the voice button visible.
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001246 */
1247 private void updateVoiceButton(boolean empty) {
Amith Yamasani79f74302011-03-08 14:16:35 -08001248 int visibility = GONE;
Amith Yamasani167d69a2011-08-12 19:28:37 -07001249 if (mVoiceButtonEnabled && !isIconified() && empty) {
Amith Yamasani9b2e3022011-01-14 11:34:12 -08001250 visibility = VISIBLE;
Alan Viverettecb8ed372014-11-18 17:05:35 -08001251 mGoButton.setVisibility(GONE);
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001252 }
1253 mVoiceButton.setVisibility(visibility);
1254 }
1255
Amith Yamasani733cbd52010-09-03 12:21:39 -07001256 private final OnEditorActionListener mOnEditorActionListener = new OnEditorActionListener() {
1257
1258 /**
1259 * Called when the input method default action key is pressed.
1260 */
1261 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
1262 onSubmitQuery();
1263 return true;
1264 }
1265 };
1266
1267 private void onTextChanged(CharSequence newText) {
Alan Viverettecb8ed372014-11-18 17:05:35 -08001268 CharSequence text = mSearchSrcTextView.getText();
Amith Yamasani068d73c2011-05-27 15:15:14 -07001269 mUserQuery = text;
Amith Yamasani733cbd52010-09-03 12:21:39 -07001270 boolean hasText = !TextUtils.isEmpty(text);
Amith Yamasanicf72ab42011-11-04 13:49:28 -07001271 updateSubmitButton(hasText);
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001272 updateVoiceButton(!hasText);
Amith Yamasani73e00df2010-12-16 16:31:29 -08001273 updateCloseButton();
Amith Yamasani9b2e3022011-01-14 11:34:12 -08001274 updateSubmitArea();
Amith Yamasanib47c4fd2011-08-04 14:30:07 -07001275 if (mOnQueryChangeListener != null && !TextUtils.equals(newText, mOldQueryText)) {
Adam Powell01f21352011-01-20 18:30:10 -08001276 mOnQueryChangeListener.onQueryTextChange(newText.toString());
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001277 }
Amith Yamasanib47c4fd2011-08-04 14:30:07 -07001278 mOldQueryText = newText.toString();
Amith Yamasani733cbd52010-09-03 12:21:39 -07001279 }
1280
1281 private void onSubmitQuery() {
Alan Viverettecb8ed372014-11-18 17:05:35 -08001282 CharSequence query = mSearchSrcTextView.getText();
Amith Yamasani6a7421b2011-07-27 11:55:53 -07001283 if (query != null && TextUtils.getTrimmedLength(query) > 0) {
Amith Yamasani733cbd52010-09-03 12:21:39 -07001284 if (mOnQueryChangeListener == null
Adam Powell01f21352011-01-20 18:30:10 -08001285 || !mOnQueryChangeListener.onQueryTextSubmit(query.toString())) {
Amith Yamasani733cbd52010-09-03 12:21:39 -07001286 if (mSearchable != null) {
1287 launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null, query.toString());
1288 }
Fabrice Di Meglio2182f2a2014-03-25 14:03:33 -07001289 setImeVisibility(false);
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001290 dismissSuggestions();
Amith Yamasani733cbd52010-09-03 12:21:39 -07001291 }
1292 }
1293 }
1294
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001295 private void dismissSuggestions() {
Alan Viverettecb8ed372014-11-18 17:05:35 -08001296 mSearchSrcTextView.dismissDropDown();
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001297 }
1298
Amith Yamasani733cbd52010-09-03 12:21:39 -07001299 private void onCloseClicked() {
Alan Viverettecb8ed372014-11-18 17:05:35 -08001300 CharSequence text = mSearchSrcTextView.getText();
Amith Yamasani24652982011-06-23 16:16:05 -07001301 if (TextUtils.isEmpty(text)) {
1302 if (mIconifiedByDefault) {
Amith Yamasanib4569fb2011-07-08 15:25:39 -07001303 // If the app doesn't override the close behavior
1304 if (mOnCloseListener == null || !mOnCloseListener.onClose()) {
1305 // hide the keyboard and remove focus
1306 clearFocus();
1307 // collapse the search field
1308 updateViewsVisibility(true);
1309 }
Amith Yamasani05944762010-10-08 13:52:38 -07001310 }
Amith Yamasani24652982011-06-23 16:16:05 -07001311 } else {
Alan Viverettecb8ed372014-11-18 17:05:35 -08001312 mSearchSrcTextView.setText("");
1313 mSearchSrcTextView.requestFocus();
Amith Yamasani24652982011-06-23 16:16:05 -07001314 setImeVisibility(true);
1315 }
1316
Amith Yamasani733cbd52010-09-03 12:21:39 -07001317 }
1318
1319 private void onSearchClicked() {
Amith Yamasani733cbd52010-09-03 12:21:39 -07001320 updateViewsVisibility(false);
Alan Viverettecb8ed372014-11-18 17:05:35 -08001321 mSearchSrcTextView.requestFocus();
Amith Yamasani05944762010-10-08 13:52:38 -07001322 setImeVisibility(true);
Amith Yamasani48385482010-12-03 14:43:52 -08001323 if (mOnSearchClickListener != null) {
1324 mOnSearchClickListener.onClick(this);
1325 }
Amith Yamasani733cbd52010-09-03 12:21:39 -07001326 }
1327
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001328 private void onVoiceClicked() {
1329 // guard against possible race conditions
1330 if (mSearchable == null) {
1331 return;
1332 }
1333 SearchableInfo searchable = mSearchable;
1334 try {
1335 if (searchable.getVoiceSearchLaunchWebSearch()) {
1336 Intent webSearchIntent = createVoiceWebSearchIntent(mVoiceWebSearchIntent,
1337 searchable);
1338 getContext().startActivity(webSearchIntent);
1339 } else if (searchable.getVoiceSearchLaunchRecognizer()) {
1340 Intent appSearchIntent = createVoiceAppSearchIntent(mVoiceAppSearchIntent,
1341 searchable);
1342 getContext().startActivity(appSearchIntent);
1343 }
1344 } catch (ActivityNotFoundException e) {
1345 // Should not happen, since we check the availability of
1346 // voice search before showing the button. But just in case...
1347 Log.w(LOG_TAG, "Could not find voice search activity");
1348 }
1349 }
1350
Amith Yamasani4aedb392010-12-15 16:04:57 -08001351 void onTextFocusChanged() {
Amith Yamasani79f74302011-03-08 14:16:35 -08001352 updateViewsVisibility(isIconified());
Amith Yamasania95e4882011-08-17 11:41:37 -07001353 // Delayed update to make sure that the focus has settled down and window focus changes
1354 // don't affect it. A synchronous update was not working.
1355 postUpdateFocusedState();
Alan Viverettecb8ed372014-11-18 17:05:35 -08001356 if (mSearchSrcTextView.hasFocus()) {
Amith Yamasanif28d1872011-07-26 12:21:03 -07001357 forceSuggestionQuery();
1358 }
Amith Yamasani4aedb392010-12-15 16:04:57 -08001359 }
1360
Amith Yamasanib4569fb2011-07-08 15:25:39 -07001361 @Override
Amith Yamasania95e4882011-08-17 11:41:37 -07001362 public void onWindowFocusChanged(boolean hasWindowFocus) {
1363 super.onWindowFocusChanged(hasWindowFocus);
1364
1365 postUpdateFocusedState();
Amith Yamasanib4569fb2011-07-08 15:25:39 -07001366 }
1367
Amith Yamasani763bc072011-07-22 11:53:47 -07001368 /**
1369 * {@inheritDoc}
1370 */
1371 @Override
1372 public void onActionViewCollapsed() {
Adam Powell99d0ce92012-12-10 13:11:47 -08001373 setQuery("", false);
Amith Yamasani10da5902011-07-26 16:14:26 -07001374 clearFocus();
1375 updateViewsVisibility(true);
Alan Viverettecb8ed372014-11-18 17:05:35 -08001376 mSearchSrcTextView.setImeOptions(mCollapsedImeOptions);
Amith Yamasani763bc072011-07-22 11:53:47 -07001377 mExpandedInActionView = false;
1378 }
1379
1380 /**
1381 * {@inheritDoc}
1382 */
1383 @Override
1384 public void onActionViewExpanded() {
Amith Yamasani434c73f2011-11-01 11:44:50 -07001385 if (mExpandedInActionView) return;
1386
Amith Yamasani763bc072011-07-22 11:53:47 -07001387 mExpandedInActionView = true;
Alan Viverettecb8ed372014-11-18 17:05:35 -08001388 mCollapsedImeOptions = mSearchSrcTextView.getImeOptions();
1389 mSearchSrcTextView.setImeOptions(mCollapsedImeOptions | EditorInfo.IME_FLAG_NO_FULLSCREEN);
1390 mSearchSrcTextView.setText("");
Amith Yamasani763bc072011-07-22 11:53:47 -07001391 setIconified(false);
1392 }
1393
Aurimas Liutikas13fdea02016-02-11 10:10:10 -08001394 static class SavedState extends BaseSavedState {
1395 boolean isIconified;
1396
1397 SavedState(Parcelable superState) {
1398 super(superState);
1399 }
1400
1401 public SavedState(Parcel source) {
1402 super(source);
1403 isIconified = (Boolean) source.readValue(null);
1404 }
1405
1406 @Override
1407 public void writeToParcel(Parcel dest, int flags) {
1408 super.writeToParcel(dest, flags);
1409 dest.writeValue(isIconified);
1410 }
1411
1412 @Override
1413 public String toString() {
1414 return "SearchView.SavedState{"
1415 + Integer.toHexString(System.identityHashCode(this))
1416 + " isIconified=" + isIconified + "}";
1417 }
Aurimas Liutikas7849a3d2016-02-26 15:27:31 -08001418
1419 public static final Parcelable.Creator<SavedState> CREATOR =
1420 new Parcelable.Creator<SavedState>() {
1421 public SavedState createFromParcel(Parcel in) {
1422 return new SavedState(in);
1423 }
1424
1425 public SavedState[] newArray(int size) {
1426 return new SavedState[size];
1427 }
1428 };
Aurimas Liutikas13fdea02016-02-11 10:10:10 -08001429 }
1430
1431 @Override
1432 protected Parcelable onSaveInstanceState() {
1433 Parcelable superState = super.onSaveInstanceState();
1434 SavedState ss = new SavedState(superState);
1435 ss.isIconified = isIconified();
1436 return ss;
1437 }
1438
1439 @Override
1440 protected void onRestoreInstanceState(Parcelable state) {
1441 SavedState ss = (SavedState) state;
1442 super.onRestoreInstanceState(ss.getSuperState());
1443 updateViewsVisibility(ss.isIconified);
1444 requestLayout();
1445 }
1446
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001447 @Override
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -08001448 public CharSequence getAccessibilityClassName() {
1449 return SearchView.class.getName();
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001450 }
1451
Amith Yamasanib4569fb2011-07-08 15:25:39 -07001452 private void adjustDropDownSizeAndPosition() {
1453 if (mDropDownAnchor.getWidth() > 1) {
1454 Resources res = getContext().getResources();
1455 int anchorPadding = mSearchPlate.getPaddingLeft();
1456 Rect dropDownPadding = new Rect();
Fabrice Di Meglio3a69bea2012-07-26 13:50:10 -07001457 final boolean isLayoutRtl = isLayoutRtl();
Amith Yamasanib4569fb2011-07-08 15:25:39 -07001458 int iconOffset = mIconifiedByDefault
1459 ? res.getDimensionPixelSize(R.dimen.dropdownitem_icon_width)
1460 + res.getDimensionPixelSize(R.dimen.dropdownitem_text_padding_left)
1461 : 0;
Alan Viverettecb8ed372014-11-18 17:05:35 -08001462 mSearchSrcTextView.getDropDownBackground().getPadding(dropDownPadding);
Fabrice Di Meglio3a69bea2012-07-26 13:50:10 -07001463 int offset;
1464 if (isLayoutRtl) {
1465 offset = - dropDownPadding.left;
1466 } else {
1467 offset = anchorPadding - (dropDownPadding.left + iconOffset);
1468 }
Alan Viverettecb8ed372014-11-18 17:05:35 -08001469 mSearchSrcTextView.setDropDownHorizontalOffset(offset);
Fabrice Di Meglio3a69bea2012-07-26 13:50:10 -07001470 final int width = mDropDownAnchor.getWidth() + dropDownPadding.left
1471 + dropDownPadding.right + iconOffset - anchorPadding;
Alan Viverettecb8ed372014-11-18 17:05:35 -08001472 mSearchSrcTextView.setDropDownWidth(width);
Amith Yamasanib4569fb2011-07-08 15:25:39 -07001473 }
1474 }
1475
Amith Yamasani968ec932010-12-02 14:00:47 -08001476 private boolean onItemClicked(int position, int actionKey, String actionMsg) {
1477 if (mOnSuggestionListener == null
Adam Powell01f21352011-01-20 18:30:10 -08001478 || !mOnSuggestionListener.onSuggestionClick(position)) {
Amith Yamasani968ec932010-12-02 14:00:47 -08001479 launchSuggestion(position, KeyEvent.KEYCODE_UNKNOWN, null);
1480 setImeVisibility(false);
1481 dismissSuggestions();
1482 return true;
1483 }
1484 return false;
1485 }
1486
1487 private boolean onItemSelected(int position) {
1488 if (mOnSuggestionListener == null
Adam Powell01f21352011-01-20 18:30:10 -08001489 || !mOnSuggestionListener.onSuggestionSelect(position)) {
Amith Yamasani968ec932010-12-02 14:00:47 -08001490 rewriteQueryFromSuggestion(position);
1491 return true;
1492 }
1493 return false;
1494 }
1495
Amith Yamasani733cbd52010-09-03 12:21:39 -07001496 private final OnItemClickListener mOnItemClickListener = new OnItemClickListener() {
1497
1498 /**
1499 * Implements OnItemClickListener
1500 */
1501 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Amith Yamasani968ec932010-12-02 14:00:47 -08001502 if (DBG) Log.d(LOG_TAG, "onItemClick() position " + position);
1503 onItemClicked(position, KeyEvent.KEYCODE_UNKNOWN, null);
Amith Yamasani733cbd52010-09-03 12:21:39 -07001504 }
1505 };
1506
1507 private final OnItemSelectedListener mOnItemSelectedListener = new OnItemSelectedListener() {
1508
1509 /**
1510 * Implements OnItemSelectedListener
1511 */
1512 public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
Amith Yamasani968ec932010-12-02 14:00:47 -08001513 if (DBG) Log.d(LOG_TAG, "onItemSelected() position " + position);
1514 SearchView.this.onItemSelected(position);
Amith Yamasani733cbd52010-09-03 12:21:39 -07001515 }
1516
1517 /**
1518 * Implements OnItemSelectedListener
1519 */
1520 public void onNothingSelected(AdapterView<?> parent) {
1521 if (DBG)
1522 Log.d(LOG_TAG, "onNothingSelected()");
1523 }
1524 };
1525
1526 /**
1527 * Query rewriting.
1528 */
1529 private void rewriteQueryFromSuggestion(int position) {
Alan Viverettecb8ed372014-11-18 17:05:35 -08001530 CharSequence oldQuery = mSearchSrcTextView.getText();
Amith Yamasani733cbd52010-09-03 12:21:39 -07001531 Cursor c = mSuggestionsAdapter.getCursor();
1532 if (c == null) {
1533 return;
1534 }
1535 if (c.moveToPosition(position)) {
1536 // Get the new query from the suggestion.
1537 CharSequence newQuery = mSuggestionsAdapter.convertToString(c);
1538 if (newQuery != null) {
1539 // The suggestion rewrites the query.
1540 // Update the text field, without getting new suggestions.
1541 setQuery(newQuery);
1542 } else {
1543 // The suggestion does not rewrite the query, restore the user's query.
1544 setQuery(oldQuery);
1545 }
1546 } else {
1547 // We got a bad position, restore the user's query.
1548 setQuery(oldQuery);
1549 }
1550 }
1551
1552 /**
1553 * Launches an intent based on a suggestion.
1554 *
1555 * @param position The index of the suggestion to create the intent from.
1556 * @param actionKey The key code of the action key that was pressed,
1557 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
1558 * @param actionMsg The message for the action key that was pressed,
1559 * or <code>null</code> if none.
1560 * @return true if a successful launch, false if could not (e.g. bad position).
1561 */
1562 private boolean launchSuggestion(int position, int actionKey, String actionMsg) {
1563 Cursor c = mSuggestionsAdapter.getCursor();
1564 if ((c != null) && c.moveToPosition(position)) {
1565
1566 Intent intent = createIntentFromSuggestion(c, actionKey, actionMsg);
1567
1568 // launch the intent
1569 launchIntent(intent);
1570
1571 return true;
1572 }
1573 return false;
1574 }
1575
1576 /**
1577 * Launches an intent, including any special intent handling.
1578 */
1579 private void launchIntent(Intent intent) {
1580 if (intent == null) {
1581 return;
1582 }
1583 try {
1584 // If the intent was created from a suggestion, it will always have an explicit
1585 // component here.
1586 getContext().startActivity(intent);
1587 } catch (RuntimeException ex) {
1588 Log.e(LOG_TAG, "Failed launch activity: " + intent, ex);
1589 }
1590 }
1591
1592 /**
1593 * Sets the text in the query box, without updating the suggestions.
1594 */
1595 private void setQuery(CharSequence query) {
Alan Viverettecb8ed372014-11-18 17:05:35 -08001596 mSearchSrcTextView.setText(query, true);
Amith Yamasanie678f462010-09-15 16:13:43 -07001597 // Move the cursor to the end
Alan Viverettecb8ed372014-11-18 17:05:35 -08001598 mSearchSrcTextView.setSelection(TextUtils.isEmpty(query) ? 0 : query.length());
Amith Yamasani733cbd52010-09-03 12:21:39 -07001599 }
1600
1601 private void launchQuerySearch(int actionKey, String actionMsg, String query) {
1602 String action = Intent.ACTION_SEARCH;
Amith Yamasanie678f462010-09-15 16:13:43 -07001603 Intent intent = createIntent(action, null, null, query, actionKey, actionMsg);
Amith Yamasani733cbd52010-09-03 12:21:39 -07001604 getContext().startActivity(intent);
1605 }
1606
1607 /**
1608 * Constructs an intent from the given information and the search dialog state.
1609 *
1610 * @param action Intent action.
1611 * @param data Intent data, or <code>null</code>.
1612 * @param extraData Data for {@link SearchManager#EXTRA_DATA_KEY} or <code>null</code>.
1613 * @param query Intent query, or <code>null</code>.
Amith Yamasani733cbd52010-09-03 12:21:39 -07001614 * @param actionKey The key code of the action key that was pressed,
1615 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
1616 * @param actionMsg The message for the action key that was pressed,
1617 * or <code>null</code> if none.
1618 * @param mode The search mode, one of the acceptable values for
1619 * {@link SearchManager#SEARCH_MODE}, or {@code null}.
1620 * @return The intent.
1621 */
1622 private Intent createIntent(String action, Uri data, String extraData, String query,
Amith Yamasanie678f462010-09-15 16:13:43 -07001623 int actionKey, String actionMsg) {
Amith Yamasani733cbd52010-09-03 12:21:39 -07001624 // Now build the Intent
1625 Intent intent = new Intent(action);
1626 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
1627 // We need CLEAR_TOP to avoid reusing an old task that has other activities
1628 // on top of the one we want. We don't want to do this in in-app search though,
1629 // as it can be destructive to the activity stack.
1630 if (data != null) {
1631 intent.setData(data);
1632 }
Amith Yamasani068d73c2011-05-27 15:15:14 -07001633 intent.putExtra(SearchManager.USER_QUERY, mUserQuery);
Amith Yamasani733cbd52010-09-03 12:21:39 -07001634 if (query != null) {
1635 intent.putExtra(SearchManager.QUERY, query);
1636 }
1637 if (extraData != null) {
1638 intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData);
1639 }
Amith Yamasani940ef382011-03-02 18:43:23 -08001640 if (mAppSearchData != null) {
1641 intent.putExtra(SearchManager.APP_DATA, mAppSearchData);
1642 }
Amith Yamasani733cbd52010-09-03 12:21:39 -07001643 if (actionKey != KeyEvent.KEYCODE_UNKNOWN) {
1644 intent.putExtra(SearchManager.ACTION_KEY, actionKey);
1645 intent.putExtra(SearchManager.ACTION_MSG, actionMsg);
1646 }
1647 intent.setComponent(mSearchable.getSearchActivity());
1648 return intent;
1649 }
1650
1651 /**
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001652 * Create and return an Intent that can launch the voice search activity for web search.
1653 */
1654 private Intent createVoiceWebSearchIntent(Intent baseIntent, SearchableInfo searchable) {
1655 Intent voiceIntent = new Intent(baseIntent);
1656 ComponentName searchActivity = searchable.getSearchActivity();
1657 voiceIntent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, searchActivity == null ? null
1658 : searchActivity.flattenToShortString());
1659 return voiceIntent;
1660 }
1661
1662 /**
1663 * Create and return an Intent that can launch the voice search activity, perform a specific
1664 * voice transcription, and forward the results to the searchable activity.
1665 *
1666 * @param baseIntent The voice app search intent to start from
1667 * @return A completely-configured intent ready to send to the voice search activity
1668 */
1669 private Intent createVoiceAppSearchIntent(Intent baseIntent, SearchableInfo searchable) {
1670 ComponentName searchActivity = searchable.getSearchActivity();
1671
1672 // create the necessary intent to set up a search-and-forward operation
1673 // in the voice search system. We have to keep the bundle separate,
1674 // because it becomes immutable once it enters the PendingIntent
1675 Intent queryIntent = new Intent(Intent.ACTION_SEARCH);
1676 queryIntent.setComponent(searchActivity);
1677 PendingIntent pending = PendingIntent.getActivity(getContext(), 0, queryIntent,
1678 PendingIntent.FLAG_ONE_SHOT);
1679
1680 // Now set up the bundle that will be inserted into the pending intent
1681 // when it's time to do the search. We always build it here (even if empty)
1682 // because the voice search activity will always need to insert "QUERY" into
1683 // it anyway.
1684 Bundle queryExtras = new Bundle();
Jorge Ruesga1bcfe842012-09-03 01:26:59 +02001685 if (mAppSearchData != null) {
1686 queryExtras.putParcelable(SearchManager.APP_DATA, mAppSearchData);
1687 }
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001688
1689 // Now build the intent to launch the voice search. Add all necessary
1690 // extras to launch the voice recognizer, and then all the necessary extras
1691 // to forward the results to the searchable activity
1692 Intent voiceIntent = new Intent(baseIntent);
1693
1694 // Add all of the configuration options supplied by the searchable's metadata
1695 String languageModel = RecognizerIntent.LANGUAGE_MODEL_FREE_FORM;
1696 String prompt = null;
1697 String language = null;
1698 int maxResults = 1;
1699
1700 Resources resources = getResources();
1701 if (searchable.getVoiceLanguageModeId() != 0) {
1702 languageModel = resources.getString(searchable.getVoiceLanguageModeId());
1703 }
1704 if (searchable.getVoicePromptTextId() != 0) {
1705 prompt = resources.getString(searchable.getVoicePromptTextId());
1706 }
1707 if (searchable.getVoiceLanguageId() != 0) {
1708 language = resources.getString(searchable.getVoiceLanguageId());
1709 }
1710 if (searchable.getVoiceMaxResults() != 0) {
1711 maxResults = searchable.getVoiceMaxResults();
1712 }
1713 voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, languageModel);
1714 voiceIntent.putExtra(RecognizerIntent.EXTRA_PROMPT, prompt);
1715 voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, language);
1716 voiceIntent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, maxResults);
1717 voiceIntent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, searchActivity == null ? null
1718 : searchActivity.flattenToShortString());
1719
1720 // Add the values that configure forwarding the results
1721 voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT, pending);
1722 voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT_BUNDLE, queryExtras);
1723
1724 return voiceIntent;
1725 }
1726
1727 /**
Amith Yamasani733cbd52010-09-03 12:21:39 -07001728 * When a particular suggestion has been selected, perform the various lookups required
1729 * to use the suggestion. This includes checking the cursor for suggestion-specific data,
1730 * and/or falling back to the XML for defaults; It also creates REST style Uri data when
1731 * the suggestion includes a data id.
1732 *
1733 * @param c The suggestions cursor, moved to the row of the user's selection
1734 * @param actionKey The key code of the action key that was pressed,
1735 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
1736 * @param actionMsg The message for the action key that was pressed,
1737 * or <code>null</code> if none.
1738 * @return An intent for the suggestion at the cursor's position.
1739 */
1740 private Intent createIntentFromSuggestion(Cursor c, int actionKey, String actionMsg) {
1741 try {
1742 // use specific action if supplied, or default action if supplied, or fixed default
1743 String action = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_ACTION);
1744
Amith Yamasani733cbd52010-09-03 12:21:39 -07001745 if (action == null) {
1746 action = mSearchable.getSuggestIntentAction();
1747 }
1748 if (action == null) {
1749 action = Intent.ACTION_SEARCH;
1750 }
1751
1752 // use specific data if supplied, or default data if supplied
1753 String data = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA);
1754 if (data == null) {
1755 data = mSearchable.getSuggestIntentData();
1756 }
1757 // then, if an ID was provided, append it.
1758 if (data != null) {
1759 String id = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID);
1760 if (id != null) {
1761 data = data + "/" + Uri.encode(id);
1762 }
1763 }
1764 Uri dataUri = (data == null) ? null : Uri.parse(data);
1765
Amith Yamasani733cbd52010-09-03 12:21:39 -07001766 String query = getColumnString(c, SearchManager.SUGGEST_COLUMN_QUERY);
1767 String extraData = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA);
1768
Amith Yamasanie678f462010-09-15 16:13:43 -07001769 return createIntent(action, dataUri, extraData, query, actionKey, actionMsg);
Amith Yamasani733cbd52010-09-03 12:21:39 -07001770 } catch (RuntimeException e ) {
1771 int rowNum;
1772 try { // be really paranoid now
1773 rowNum = c.getPosition();
1774 } catch (RuntimeException e2 ) {
1775 rowNum = -1;
1776 }
Jake Wharton73af4512012-07-06 23:15:44 -07001777 Log.w(LOG_TAG, "Search suggestions cursor at row " + rowNum +
1778 " returned exception.", e);
Amith Yamasani733cbd52010-09-03 12:21:39 -07001779 return null;
1780 }
1781 }
1782
Amith Yamasanif28d1872011-07-26 12:21:03 -07001783 private void forceSuggestionQuery() {
Alan Viverettecb8ed372014-11-18 17:05:35 -08001784 mSearchSrcTextView.doBeforeTextChanged();
1785 mSearchSrcTextView.doAfterTextChanged();
Amith Yamasanif28d1872011-07-26 12:21:03 -07001786 }
1787
Amith Yamasani968ec932010-12-02 14:00:47 -08001788 static boolean isLandscapeMode(Context context) {
1789 return context.getResources().getConfiguration().orientation
1790 == Configuration.ORIENTATION_LANDSCAPE;
1791 }
1792
Amith Yamasani733cbd52010-09-03 12:21:39 -07001793 /**
1794 * Callback to watch the text field for empty/non-empty
1795 */
1796 private TextWatcher mTextWatcher = new TextWatcher() {
1797
1798 public void beforeTextChanged(CharSequence s, int start, int before, int after) { }
1799
1800 public void onTextChanged(CharSequence s, int start,
1801 int before, int after) {
1802 SearchView.this.onTextChanged(s);
1803 }
1804
1805 public void afterTextChanged(Editable s) {
1806 }
1807 };
Amith Yamasani968ec932010-12-02 14:00:47 -08001808
Aurimas Liutikasc8fd00a2016-04-15 13:55:53 -07001809 private static class UpdatableTouchDelegate extends TouchDelegate {
1810 /**
1811 * View that should receive forwarded touch events
1812 */
1813 private final View mDelegateView;
1814
1815 /**
1816 * Bounds in local coordinates of the containing view that should be mapped to the delegate
1817 * view. This rect is used for initial hit testing.
1818 */
1819 private final Rect mTargetBounds;
1820
1821 /**
1822 * Bounds in local coordinates of the containing view that are actual bounds of the delegate
1823 * view. This rect is used for event coordinate mapping.
1824 */
1825 private final Rect mActualBounds;
1826
1827 /**
1828 * mTargetBounds inflated to include some slop. This rect is to track whether the motion events
1829 * should be considered to be be within the delegate view.
1830 */
1831 private final Rect mSlopBounds;
1832
1833 private final int mSlop;
1834
1835 /**
1836 * True if the delegate had been targeted on a down event (intersected mTargetBounds).
1837 */
1838 private boolean mDelegateTargeted;
1839
1840 public UpdatableTouchDelegate(Rect targetBounds, Rect actualBounds, View delegateView) {
1841 super(targetBounds, delegateView);
1842 mSlop = ViewConfiguration.get(delegateView.getContext()).getScaledTouchSlop();
1843 mTargetBounds = new Rect();
1844 mSlopBounds = new Rect();
1845 mActualBounds = new Rect();
1846 setBounds(targetBounds, actualBounds);
1847 mDelegateView = delegateView;
1848 }
1849
1850 public void setBounds(Rect desiredBounds, Rect actualBounds) {
1851 mTargetBounds.set(desiredBounds);
1852 mSlopBounds.set(desiredBounds);
1853 mSlopBounds.inset(-mSlop, -mSlop);
1854 mActualBounds.set(actualBounds);
1855 }
1856
1857 @Override
1858 public boolean onTouchEvent(MotionEvent event) {
1859 final int x = (int) event.getX();
1860 final int y = (int) event.getY();
1861 boolean sendToDelegate = false;
1862 boolean hit = true;
1863 boolean handled = false;
1864
1865 switch (event.getAction()) {
1866 case MotionEvent.ACTION_DOWN:
1867 if (mTargetBounds.contains(x, y)) {
1868 mDelegateTargeted = true;
1869 sendToDelegate = true;
1870 }
1871 break;
1872 case MotionEvent.ACTION_UP:
1873 case MotionEvent.ACTION_MOVE:
1874 sendToDelegate = mDelegateTargeted;
1875 if (sendToDelegate) {
1876 if (!mSlopBounds.contains(x, y)) {
1877 hit = false;
1878 }
1879 }
1880 break;
1881 case MotionEvent.ACTION_CANCEL:
1882 sendToDelegate = mDelegateTargeted;
1883 mDelegateTargeted = false;
1884 break;
1885 }
1886 if (sendToDelegate) {
1887 if (hit && !mActualBounds.contains(x, y)) {
1888 // Offset event coordinates to be in the center of the target view since we
1889 // are within the targetBounds, but not inside the actual bounds of
1890 // mDelegateView
1891 event.setLocation(mDelegateView.getWidth() / 2,
1892 mDelegateView.getHeight() / 2);
1893 } else {
1894 // Offset event coordinates to the target view coordinates.
1895 event.setLocation(x - mActualBounds.left, y - mActualBounds.top);
1896 }
1897
1898 handled = mDelegateView.dispatchTouchEvent(event);
1899 }
1900 return handled;
1901 }
1902 }
1903
Amith Yamasani968ec932010-12-02 14:00:47 -08001904 /**
1905 * Local subclass for AutoCompleteTextView.
1906 * @hide
1907 */
1908 public static class SearchAutoComplete extends AutoCompleteTextView {
1909
1910 private int mThreshold;
1911 private SearchView mSearchView;
1912
1913 public SearchAutoComplete(Context context) {
1914 super(context);
1915 mThreshold = getThreshold();
1916 }
1917
1918 public SearchAutoComplete(Context context, AttributeSet attrs) {
1919 super(context, attrs);
1920 mThreshold = getThreshold();
1921 }
1922
Alan Viverette617feb92013-09-09 18:09:13 -07001923 public SearchAutoComplete(Context context, AttributeSet attrs, int defStyleAttrs) {
1924 super(context, attrs, defStyleAttrs);
1925 mThreshold = getThreshold();
1926 }
1927
1928 public SearchAutoComplete(
1929 Context context, AttributeSet attrs, int defStyleAttrs, int defStyleRes) {
1930 super(context, attrs, defStyleAttrs, defStyleRes);
Amith Yamasani968ec932010-12-02 14:00:47 -08001931 mThreshold = getThreshold();
1932 }
1933
Filip Gruszczynskib635fda2015-12-03 18:37:38 -08001934 @Override
1935 protected void onFinishInflate() {
1936 super.onFinishInflate();
1937 DisplayMetrics metrics = getResources().getDisplayMetrics();
1938 setMinWidth((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
1939 getSearchViewTextMinWidthDp(), metrics));
1940 }
1941
Amith Yamasani968ec932010-12-02 14:00:47 -08001942 void setSearchView(SearchView searchView) {
1943 mSearchView = searchView;
1944 }
1945
1946 @Override
1947 public void setThreshold(int threshold) {
1948 super.setThreshold(threshold);
1949 mThreshold = threshold;
1950 }
1951
1952 /**
1953 * Returns true if the text field is empty, or contains only whitespace.
1954 */
1955 private boolean isEmpty() {
1956 return TextUtils.getTrimmedLength(getText()) == 0;
1957 }
1958
1959 /**
1960 * We override this method to avoid replacing the query box text when a
1961 * suggestion is clicked.
1962 */
1963 @Override
1964 protected void replaceText(CharSequence text) {
1965 }
1966
1967 /**
1968 * We override this method to avoid an extra onItemClick being called on
1969 * the drop-down's OnItemClickListener by
1970 * {@link AutoCompleteTextView#onKeyUp(int, KeyEvent)} when an item is
1971 * clicked with the trackball.
1972 */
1973 @Override
1974 public void performCompletion() {
1975 }
1976
1977 /**
1978 * We override this method to be sure and show the soft keyboard if
1979 * appropriate when the TextView has focus.
1980 */
1981 @Override
1982 public void onWindowFocusChanged(boolean hasWindowFocus) {
1983 super.onWindowFocusChanged(hasWindowFocus);
1984
Amith Yamasaniacd8d2d2010-12-06 15:50:23 -08001985 if (hasWindowFocus && mSearchView.hasFocus() && getVisibility() == VISIBLE) {
Yohei Yukawa777ef952015-11-25 20:32:24 -08001986 InputMethodManager inputManager =
1987 getContext().getSystemService(InputMethodManager.class);
Amith Yamasani968ec932010-12-02 14:00:47 -08001988 inputManager.showSoftInput(this, 0);
1989 // If in landscape mode, then make sure that
1990 // the ime is in front of the dropdown.
1991 if (isLandscapeMode(getContext())) {
1992 ensureImeVisible(true);
1993 }
1994 }
1995 }
1996
Amith Yamasani4aedb392010-12-15 16:04:57 -08001997 @Override
1998 protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
1999 super.onFocusChanged(focused, direction, previouslyFocusedRect);
2000 mSearchView.onTextFocusChanged();
2001 }
2002
Amith Yamasani968ec932010-12-02 14:00:47 -08002003 /**
2004 * We override this method so that we can allow a threshold of zero,
2005 * which ACTV does not.
2006 */
2007 @Override
2008 public boolean enoughToFilter() {
2009 return mThreshold <= 0 || super.enoughToFilter();
2010 }
Amith Yamasanib4569fb2011-07-08 15:25:39 -07002011
2012 @Override
2013 public boolean onKeyPreIme(int keyCode, KeyEvent event) {
2014 if (keyCode == KeyEvent.KEYCODE_BACK) {
2015 // special case for the back key, we do not even try to send it
2016 // to the drop down list but instead, consume it immediately
2017 if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
2018 KeyEvent.DispatcherState state = getKeyDispatcherState();
2019 if (state != null) {
2020 state.startTracking(event, this);
2021 }
2022 return true;
2023 } else if (event.getAction() == KeyEvent.ACTION_UP) {
2024 KeyEvent.DispatcherState state = getKeyDispatcherState();
2025 if (state != null) {
2026 state.handleUpEvent(event);
2027 }
2028 if (event.isTracking() && !event.isCanceled()) {
2029 mSearchView.clearFocus();
2030 mSearchView.setImeVisibility(false);
2031 return true;
2032 }
2033 }
2034 }
2035 return super.onKeyPreIme(keyCode, event);
2036 }
2037
Filip Gruszczynskib635fda2015-12-03 18:37:38 -08002038 /**
2039 * Get minimum width of the search view text entry area.
2040 */
2041 private int getSearchViewTextMinWidthDp() {
2042 final Configuration configuration = getResources().getConfiguration();
2043 final int width = configuration.screenWidthDp;
2044 final int height = configuration.screenHeightDp;
2045 final int orientation = configuration.orientation;
2046 if (width >= 960 && height >= 720
2047 && orientation == Configuration.ORIENTATION_LANDSCAPE) {
2048 return 256;
2049 } else if (width >= 600 || (width >= 640 && height >= 480)) {
2050 return 192;
2051 };
2052 return 160;
2053 }
Amith Yamasani968ec932010-12-02 14:00:47 -08002054 }
Amith Yamasani05944762010-10-08 13:52:38 -07002055}