blob: 519a7dd8be437dd24022660346906fd6808ab202 [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;
Yohei Yukawa5cfc1b42017-03-07 00:52:09 -080061import android.view.inputmethod.InputConnection;
Amith Yamasani733cbd52010-09-03 12:21:39 -070062import android.view.inputmethod.InputMethodManager;
63import android.widget.AdapterView.OnItemClickListener;
64import android.widget.AdapterView.OnItemSelectedListener;
65import android.widget.TextView.OnEditorActionListener;
66
Amith Yamasanib4569fb2011-07-08 15:25:39 -070067import com.android.internal.R;
68
Amith Yamasani733cbd52010-09-03 12:21:39 -070069import java.util.WeakHashMap;
70
71/**
Amith Yamasani763bc072011-07-22 11:53:47 -070072 * A widget that provides a user interface for the user to enter a search query and submit a request
73 * to a search provider. Shows a list of query suggestions or results, if available, and allows the
74 * user to pick a suggestion or result to launch into.
Amith Yamasani5931b1f2010-10-18 16:13:14 -070075 *
Amith Yamasani763bc072011-07-22 11:53:47 -070076 * <p>
77 * When the SearchView is used in an ActionBar as an action view for a collapsible menu item, it
78 * needs to be set to iconified by default using {@link #setIconifiedByDefault(boolean)
79 * setIconifiedByDefault(true)}. This is the default, so nothing needs to be done.
80 * </p>
81 * <p>
82 * If you want the search field to always be visible, then call setIconifiedByDefault(false).
83 * </p>
Amith Yamasani5931b1f2010-10-18 16:13:14 -070084 *
Joe Fernandez3aef8e1d2011-12-20 10:38:34 -080085 * <div class="special reference">
86 * <h3>Developer Guides</h3>
87 * <p>For information about using {@code SearchView}, read the
88 * <a href="{@docRoot}guide/topics/search/index.html">Search</a> developer guide.</p>
89 * </div>
Amith Yamasani763bc072011-07-22 11:53:47 -070090 *
91 * @see android.view.MenuItem#SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW
Amith Yamasani5931b1f2010-10-18 16:13:14 -070092 * @attr ref android.R.styleable#SearchView_iconifiedByDefault
Amith Yamasani5607a382011-08-09 14:16:37 -070093 * @attr ref android.R.styleable#SearchView_imeOptions
94 * @attr ref android.R.styleable#SearchView_inputType
Amith Yamasani5931b1f2010-10-18 16:13:14 -070095 * @attr ref android.R.styleable#SearchView_maxWidth
Scott Mainabdf0d52011-02-08 10:20:27 -080096 * @attr ref android.R.styleable#SearchView_queryHint
Amith Yamasani733cbd52010-09-03 12:21:39 -070097 */
Amith Yamasani763bc072011-07-22 11:53:47 -070098public class SearchView extends LinearLayout implements CollapsibleActionView {
Amith Yamasani733cbd52010-09-03 12:21:39 -070099
100 private static final boolean DBG = false;
101 private static final String LOG_TAG = "SearchView";
102
Luca Zanolin535698c2011-10-06 13:36:15 +0100103 /**
104 * Private constant for removing the microphone in the keyboard.
105 */
106 private static final String IME_OPTION_NO_MICROPHONE = "nm";
107
Alan Viverettecb8ed372014-11-18 17:05:35 -0800108 private final SearchAutoComplete mSearchSrcTextView;
Alan Viverette5dddb702014-07-02 15:46:04 -0700109 private final View mSearchEditFrame;
110 private final View mSearchPlate;
111 private final View mSubmitArea;
112 private final ImageView mSearchButton;
Alan Viverettecb8ed372014-11-18 17:05:35 -0800113 private final ImageView mGoButton;
Alan Viverette5dddb702014-07-02 15:46:04 -0700114 private final ImageView mCloseButton;
115 private final ImageView mVoiceButton;
Alan Viverette5dddb702014-07-02 15:46:04 -0700116 private final View mDropDownAnchor;
Alan Viverettecb8ed372014-11-18 17:05:35 -0800117
Aurimas Liutikasc8fd00a2016-04-15 13:55:53 -0700118 private UpdatableTouchDelegate mTouchDelegate;
119 private Rect mSearchSrcTextViewBounds = new Rect();
120 private Rect mSearchSrtTextViewBoundsExpanded = new Rect();
121 private int[] mTemp = new int[2];
122 private int[] mTemp2 = new int[2];
123
Alan Viverettecb8ed372014-11-18 17:05:35 -0800124 /** Icon optionally displayed when the SearchView is collapsed. */
125 private final ImageView mCollapsedIcon;
126
127 /** Drawable used as an EditText hint. */
128 private final Drawable mSearchHintIcon;
Alan Viverette5dddb702014-07-02 15:46:04 -0700129
130 // Resources used by SuggestionsAdapter to display suggestions.
131 private final int mSuggestionRowLayout;
132 private final int mSuggestionCommitIconResId;
133
134 // Intents used for voice searching.
135 private final Intent mVoiceWebSearchIntent;
136 private final Intent mVoiceAppSearchIntent;
137
Alan Viveretteb4004df2015-04-29 16:55:42 -0700138 private final CharSequence mDefaultQueryHint;
139
Adam Powell01f21352011-01-20 18:30:10 -0800140 private OnQueryTextListener mOnQueryChangeListener;
Amith Yamasani733cbd52010-09-03 12:21:39 -0700141 private OnCloseListener mOnCloseListener;
Amith Yamasani05944762010-10-08 13:52:38 -0700142 private OnFocusChangeListener mOnQueryTextFocusChangeListener;
Adam Powell01f21352011-01-20 18:30:10 -0800143 private OnSuggestionListener mOnSuggestionListener;
Amith Yamasani48385482010-12-03 14:43:52 -0800144 private OnClickListener mOnSearchClickListener;
Amith Yamasani733cbd52010-09-03 12:21:39 -0700145
146 private boolean mIconifiedByDefault;
Amith Yamasani93227752010-09-14 10:10:54 -0700147 private boolean mIconified;
Amith Yamasani733cbd52010-09-03 12:21:39 -0700148 private CursorAdapter mSuggestionsAdapter;
Amith Yamasani733cbd52010-09-03 12:21:39 -0700149 private boolean mSubmitButtonEnabled;
150 private CharSequence mQueryHint;
Amith Yamasanie678f462010-09-15 16:13:43 -0700151 private boolean mQueryRefinement;
Amith Yamasani05944762010-10-08 13:52:38 -0700152 private boolean mClearingFocus;
Amith Yamasani5931b1f2010-10-18 16:13:14 -0700153 private int mMaxWidth;
Amith Yamasani9b2e3022011-01-14 11:34:12 -0800154 private boolean mVoiceButtonEnabled;
Amith Yamasanib47c4fd2011-08-04 14:30:07 -0700155 private CharSequence mOldQueryText;
Amith Yamasani068d73c2011-05-27 15:15:14 -0700156 private CharSequence mUserQuery;
Amith Yamasani763bc072011-07-22 11:53:47 -0700157 private boolean mExpandedInActionView;
Adam Powell53f56c42011-09-25 13:46:15 -0700158 private int mCollapsedImeOptions;
Amith Yamasani733cbd52010-09-03 12:21:39 -0700159
160 private SearchableInfo mSearchable;
Amith Yamasani940ef382011-03-02 18:43:23 -0800161 private Bundle mAppSearchData;
Amith Yamasani733cbd52010-09-03 12:21:39 -0700162
Amith Yamasania95e4882011-08-17 11:41:37 -0700163 private Runnable mUpdateDrawableStateRunnable = new Runnable() {
164 public void run() {
165 updateFocusedState();
166 }
167 };
168
Amith Yamasani87907642011-11-03 11:32:44 -0700169 private Runnable mReleaseCursorRunnable = new Runnable() {
170 public void run() {
171 if (mSuggestionsAdapter != null && mSuggestionsAdapter instanceof SuggestionsAdapter) {
172 mSuggestionsAdapter.changeCursor(null);
173 }
174 }
175 };
176
Amith Yamasani733cbd52010-09-03 12:21:39 -0700177 // A weak map of drawables we've gotten from other packages, so we don't load them
178 // more than once.
179 private final WeakHashMap<String, Drawable.ConstantState> mOutsideDrawablesCache =
180 new WeakHashMap<String, Drawable.ConstantState>();
181
182 /**
183 * Callbacks for changes to the query text.
184 */
Adam Powell01f21352011-01-20 18:30:10 -0800185 public interface OnQueryTextListener {
Amith Yamasani733cbd52010-09-03 12:21:39 -0700186
187 /**
188 * Called when the user submits the query. This could be due to a key press on the
189 * keyboard or due to pressing a submit button.
190 * The listener can override the standard behavior by returning true
191 * to indicate that it has handled the submit request. Otherwise return false to
192 * let the SearchView handle the submission by launching any associated intent.
193 *
194 * @param query the query text that is to be submitted
195 *
196 * @return true if the query has been handled by the listener, false to let the
197 * SearchView perform the default action.
198 */
Adam Powell01f21352011-01-20 18:30:10 -0800199 boolean onQueryTextSubmit(String query);
Amith Yamasani733cbd52010-09-03 12:21:39 -0700200
201 /**
202 * Called when the query text is changed by the user.
203 *
204 * @param newText the new content of the query text field.
205 *
206 * @return false if the SearchView should perform the default action of showing any
207 * suggestions if available, true if the action was handled by the listener.
208 */
Adam Powell01f21352011-01-20 18:30:10 -0800209 boolean onQueryTextChange(String newText);
Amith Yamasani733cbd52010-09-03 12:21:39 -0700210 }
211
212 public interface OnCloseListener {
213
214 /**
215 * The user is attempting to close the SearchView.
216 *
217 * @return true if the listener wants to override the default behavior of clearing the
218 * text field and dismissing it, false otherwise.
219 */
220 boolean onClose();
221 }
222
Amith Yamasani05944762010-10-08 13:52:38 -0700223 /**
224 * Callback interface for selection events on suggestions. These callbacks
225 * are only relevant when a SearchableInfo has been specified by {@link #setSearchableInfo}.
226 */
Adam Powell01f21352011-01-20 18:30:10 -0800227 public interface OnSuggestionListener {
Amith Yamasani05944762010-10-08 13:52:38 -0700228
229 /**
230 * Called when a suggestion was selected by navigating to it.
231 * @param position the absolute position in the list of suggestions.
232 *
233 * @return true if the listener handles the event and wants to override the default
234 * behavior of possibly rewriting the query based on the selected item, false otherwise.
235 */
Adam Powell01f21352011-01-20 18:30:10 -0800236 boolean onSuggestionSelect(int position);
Amith Yamasani05944762010-10-08 13:52:38 -0700237
238 /**
239 * Called when a suggestion was clicked.
240 * @param position the absolute position of the clicked item in the list of suggestions.
241 *
242 * @return true if the listener handles the event and wants to override the default
243 * behavior of launching any intent or submitting a search query specified on that item.
244 * Return false otherwise.
245 */
Adam Powell01f21352011-01-20 18:30:10 -0800246 boolean onSuggestionClick(int position);
Amith Yamasani05944762010-10-08 13:52:38 -0700247 }
248
Amith Yamasani733cbd52010-09-03 12:21:39 -0700249 public SearchView(Context context) {
250 this(context, null);
251 }
252
253 public SearchView(Context context, AttributeSet attrs) {
Alan Viverette5dddb702014-07-02 15:46:04 -0700254 this(context, attrs, R.attr.searchViewStyle);
Alan Viverette617feb92013-09-09 18:09:13 -0700255 }
256
257 public SearchView(Context context, AttributeSet attrs, int defStyleAttr) {
258 this(context, attrs, defStyleAttr, 0);
259 }
260
261 public SearchView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
262 super(context, attrs, defStyleAttr, defStyleRes);
Amith Yamasani733cbd52010-09-03 12:21:39 -0700263
Alan Viverette5dddb702014-07-02 15:46:04 -0700264 final TypedArray a = context.obtainStyledAttributes(
265 attrs, R.styleable.SearchView, defStyleAttr, defStyleRes);
266 final LayoutInflater inflater = (LayoutInflater) context.getSystemService(
267 Context.LAYOUT_INFLATER_SERVICE);
Alan Viverettecb8ed372014-11-18 17:05:35 -0800268 final int layoutResId = a.getResourceId(
269 R.styleable.SearchView_layout, R.layout.search_view);
Alan Viverette5dddb702014-07-02 15:46:04 -0700270 inflater.inflate(layoutResId, this, true);
Amith Yamasani733cbd52010-09-03 12:21:39 -0700271
Alan Viverettecb8ed372014-11-18 17:05:35 -0800272 mSearchSrcTextView = (SearchAutoComplete) findViewById(R.id.search_src_text);
273 mSearchSrcTextView.setSearchView(this);
Amith Yamasani968ec932010-12-02 14:00:47 -0800274
Amith Yamasani733cbd52010-09-03 12:21:39 -0700275 mSearchEditFrame = findViewById(R.id.search_edit_frame);
Amith Yamasani79f74302011-03-08 14:16:35 -0800276 mSearchPlate = findViewById(R.id.search_plate);
Amith Yamasani9b2e3022011-01-14 11:34:12 -0800277 mSubmitArea = findViewById(R.id.submit_area);
Alan Viverette5dddb702014-07-02 15:46:04 -0700278 mSearchButton = (ImageView) findViewById(R.id.search_button);
Alan Viverettecb8ed372014-11-18 17:05:35 -0800279 mGoButton = (ImageView) findViewById(R.id.search_go_btn);
Amith Yamasani4aedb392010-12-15 16:04:57 -0800280 mCloseButton = (ImageView) findViewById(R.id.search_close_btn);
Alan Viverette5dddb702014-07-02 15:46:04 -0700281 mVoiceButton = (ImageView) findViewById(R.id.search_voice_btn);
Alan Viverettecb8ed372014-11-18 17:05:35 -0800282 mCollapsedIcon = (ImageView) findViewById(R.id.search_mag_icon);
Amith Yamasani733cbd52010-09-03 12:21:39 -0700283
Alan Viverette5dddb702014-07-02 15:46:04 -0700284 // Set up icons and backgrounds.
285 mSearchPlate.setBackground(a.getDrawable(R.styleable.SearchView_queryBackground));
286 mSubmitArea.setBackground(a.getDrawable(R.styleable.SearchView_submitBackground));
Alan Viverettecb8ed372014-11-18 17:05:35 -0800287 mSearchButton.setImageDrawable(a.getDrawable(R.styleable.SearchView_searchIcon));
288 mGoButton.setImageDrawable(a.getDrawable(R.styleable.SearchView_goIcon));
Alan Viverette5dddb702014-07-02 15:46:04 -0700289 mCloseButton.setImageDrawable(a.getDrawable(R.styleable.SearchView_closeIcon));
290 mVoiceButton.setImageDrawable(a.getDrawable(R.styleable.SearchView_voiceIcon));
Alan Viverettecb8ed372014-11-18 17:05:35 -0800291 mCollapsedIcon.setImageDrawable(a.getDrawable(R.styleable.SearchView_searchIcon));
292
293 // Prior to L MR1, the search hint icon defaulted to searchIcon. If the
294 // style does not have an explicit value set, fall back to that.
295 if (a.hasValueOrEmpty(R.styleable.SearchView_searchHintIcon)) {
296 mSearchHintIcon = a.getDrawable(R.styleable.SearchView_searchHintIcon);
297 } else {
298 mSearchHintIcon = a.getDrawable(R.styleable.SearchView_searchIcon);
299 }
Alan Viverette5dddb702014-07-02 15:46:04 -0700300
301 // Extract dropdown layout resource IDs for later use.
Alan Viverette362f9842014-09-10 16:05:35 -0700302 mSuggestionRowLayout = a.getResourceId(R.styleable.SearchView_suggestionRowLayout,
303 R.layout.search_dropdown_item_icons_2line);
Alan Viverette5dddb702014-07-02 15:46:04 -0700304 mSuggestionCommitIconResId = a.getResourceId(R.styleable.SearchView_commitIcon, 0);
305
Amith Yamasani733cbd52010-09-03 12:21:39 -0700306 mSearchButton.setOnClickListener(mOnClickListener);
307 mCloseButton.setOnClickListener(mOnClickListener);
Alan Viverettecb8ed372014-11-18 17:05:35 -0800308 mGoButton.setOnClickListener(mOnClickListener);
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -0700309 mVoiceButton.setOnClickListener(mOnClickListener);
Alan Viverettecb8ed372014-11-18 17:05:35 -0800310 mSearchSrcTextView.setOnClickListener(mOnClickListener);
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -0700311
Alan Viverettecb8ed372014-11-18 17:05:35 -0800312 mSearchSrcTextView.addTextChangedListener(mTextWatcher);
313 mSearchSrcTextView.setOnEditorActionListener(mOnEditorActionListener);
314 mSearchSrcTextView.setOnItemClickListener(mOnItemClickListener);
315 mSearchSrcTextView.setOnItemSelectedListener(mOnItemSelectedListener);
316 mSearchSrcTextView.setOnKeyListener(mTextKeyListener);
Alan Viverette5dddb702014-07-02 15:46:04 -0700317
Luca Zanolin535698c2011-10-06 13:36:15 +0100318 // Inform any listener of focus changes
Alan Viverettecb8ed372014-11-18 17:05:35 -0800319 mSearchSrcTextView.setOnFocusChangeListener(new OnFocusChangeListener() {
Amith Yamasani05944762010-10-08 13:52:38 -0700320
321 public void onFocusChange(View v, boolean hasFocus) {
322 if (mOnQueryTextFocusChangeListener != null) {
323 mOnQueryTextFocusChangeListener.onFocusChange(SearchView.this, hasFocus);
324 }
325 }
326 });
Amith Yamasani733cbd52010-09-03 12:21:39 -0700327 setIconifiedByDefault(a.getBoolean(R.styleable.SearchView_iconifiedByDefault, true));
Alan Viverette5dddb702014-07-02 15:46:04 -0700328
329 final int maxWidth = a.getDimensionPixelSize(R.styleable.SearchView_maxWidth, -1);
Amith Yamasani5931b1f2010-10-18 16:13:14 -0700330 if (maxWidth != -1) {
331 setMaxWidth(maxWidth);
332 }
Alan Viverette5dddb702014-07-02 15:46:04 -0700333
Alan Viveretteb4004df2015-04-29 16:55:42 -0700334 mDefaultQueryHint = a.getText(R.styleable.SearchView_defaultQueryHint);
335 mQueryHint = a.getText(R.styleable.SearchView_queryHint);
Alan Viverette5dddb702014-07-02 15:46:04 -0700336
337 final int imeOptions = a.getInt(R.styleable.SearchView_imeOptions, -1);
Amith Yamasani5607a382011-08-09 14:16:37 -0700338 if (imeOptions != -1) {
339 setImeOptions(imeOptions);
340 }
Alan Viverette5dddb702014-07-02 15:46:04 -0700341
342 final int inputType = a.getInt(R.styleable.SearchView_inputType, -1);
Amith Yamasani5607a382011-08-09 14:16:37 -0700343 if (inputType != -1) {
344 setInputType(inputType);
345 }
346
Evan Rosky4c8c9632016-12-16 17:27:55 -0800347 if (getFocusable() == FOCUSABLE_AUTO) {
348 setFocusable(FOCUSABLE);
349 }
Adam Powellea4ecd62014-09-03 19:35:37 -0700350
Amith Yamasani733cbd52010-09-03 12:21:39 -0700351 a.recycle();
352
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -0700353 // Save voice intent for later queries/launching
354 mVoiceWebSearchIntent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH);
355 mVoiceWebSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
356 mVoiceWebSearchIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
357 RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH);
358
359 mVoiceAppSearchIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
360 mVoiceAppSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
361
Alan Viverettecb8ed372014-11-18 17:05:35 -0800362 mDropDownAnchor = findViewById(mSearchSrcTextView.getDropDownAnchor());
Amith Yamasanib4569fb2011-07-08 15:25:39 -0700363 if (mDropDownAnchor != null) {
364 mDropDownAnchor.addOnLayoutChangeListener(new OnLayoutChangeListener() {
365 @Override
366 public void onLayoutChange(View v, int left, int top, int right, int bottom,
367 int oldLeft, int oldTop, int oldRight, int oldBottom) {
368 adjustDropDownSizeAndPosition();
369 }
Amith Yamasanib4569fb2011-07-08 15:25:39 -0700370 });
371 }
372
Amith Yamasani733cbd52010-09-03 12:21:39 -0700373 updateViewsVisibility(mIconifiedByDefault);
Amith Yamasanib4569fb2011-07-08 15:25:39 -0700374 updateQueryHint();
Amith Yamasani733cbd52010-09-03 12:21:39 -0700375 }
376
Alan Viverette5dddb702014-07-02 15:46:04 -0700377 int getSuggestionRowLayout() {
378 return mSuggestionRowLayout;
379 }
380
381 int getSuggestionCommitIconResId() {
382 return mSuggestionCommitIconResId;
383 }
384
Amith Yamasani733cbd52010-09-03 12:21:39 -0700385 /**
386 * Sets the SearchableInfo for this SearchView. Properties in the SearchableInfo are used
387 * to display labels, hints, suggestions, create intents for launching search results screens
388 * and controlling other affordances such as a voice button.
389 *
390 * @param searchable a SearchableInfo can be retrieved from the SearchManager, for a specific
391 * activity or a global search provider.
392 */
393 public void setSearchableInfo(SearchableInfo searchable) {
394 mSearchable = searchable;
395 if (mSearchable != null) {
396 updateSearchAutoComplete();
Amith Yamasani79f74302011-03-08 14:16:35 -0800397 updateQueryHint();
Amith Yamasani733cbd52010-09-03 12:21:39 -0700398 }
Amith Yamasani9b2e3022011-01-14 11:34:12 -0800399 // Cache the voice search capability
400 mVoiceButtonEnabled = hasVoiceSearch();
Luca Zanolin535698c2011-10-06 13:36:15 +0100401
402 if (mVoiceButtonEnabled) {
403 // Disable the microphone on the keyboard, as a mic is displayed near the text box
404 // TODO: use imeOptions to disable voice input when the new API will be available
Alan Viverettecb8ed372014-11-18 17:05:35 -0800405 mSearchSrcTextView.setPrivateImeOptions(IME_OPTION_NO_MICROPHONE);
Luca Zanolin535698c2011-10-06 13:36:15 +0100406 }
Amith Yamasanib4569fb2011-07-08 15:25:39 -0700407 updateViewsVisibility(isIconified());
Amith Yamasani733cbd52010-09-03 12:21:39 -0700408 }
409
Amith Yamasani940ef382011-03-02 18:43:23 -0800410 /**
411 * Sets the APP_DATA for legacy SearchDialog use.
412 * @param appSearchData bundle provided by the app when launching the search dialog
413 * @hide
414 */
415 public void setAppSearchData(Bundle appSearchData) {
416 mAppSearchData = appSearchData;
417 }
418
Amith Yamasani5607a382011-08-09 14:16:37 -0700419 /**
420 * Sets the IME options on the query text field.
421 *
422 * @see TextView#setImeOptions(int)
423 * @param imeOptions the options to set on the query text field
424 *
425 * @attr ref android.R.styleable#SearchView_imeOptions
426 */
427 public void setImeOptions(int imeOptions) {
Alan Viverettecb8ed372014-11-18 17:05:35 -0800428 mSearchSrcTextView.setImeOptions(imeOptions);
Amith Yamasani5607a382011-08-09 14:16:37 -0700429 }
430
431 /**
Amith Yamasanieca59d32012-04-25 18:57:18 -0700432 * Returns the IME options set on the query text field.
433 * @return the ime options
434 * @see TextView#setImeOptions(int)
435 *
436 * @attr ref android.R.styleable#SearchView_imeOptions
437 */
438 public int getImeOptions() {
Alan Viverettecb8ed372014-11-18 17:05:35 -0800439 return mSearchSrcTextView.getImeOptions();
Amith Yamasanieca59d32012-04-25 18:57:18 -0700440 }
441
442 /**
Amith Yamasani5607a382011-08-09 14:16:37 -0700443 * Sets the input type on the query text field.
444 *
445 * @see TextView#setInputType(int)
446 * @param inputType the input type to set on the query text field
447 *
448 * @attr ref android.R.styleable#SearchView_inputType
449 */
450 public void setInputType(int inputType) {
Alan Viverettecb8ed372014-11-18 17:05:35 -0800451 mSearchSrcTextView.setInputType(inputType);
Amith Yamasani5607a382011-08-09 14:16:37 -0700452 }
453
Amith Yamasanieca59d32012-04-25 18:57:18 -0700454 /**
455 * Returns the input type set on the query text field.
456 * @return the input type
457 *
458 * @attr ref android.R.styleable#SearchView_inputType
459 */
460 public int getInputType() {
Alan Viverettecb8ed372014-11-18 17:05:35 -0800461 return mSearchSrcTextView.getInputType();
Amith Yamasanieca59d32012-04-25 18:57:18 -0700462 }
463
Amith Yamasani05944762010-10-08 13:52:38 -0700464 /** @hide */
465 @Override
466 public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
Amith Yamasani7f8aef62011-01-25 11:58:09 -0800467 // Don't accept focus if in the middle of clearing focus
468 if (mClearingFocus) return false;
469 // Check if SearchView is focusable.
470 if (!isFocusable()) return false;
471 // If it is not iconified, then give the focus to the text field
472 if (!isIconified()) {
Alan Viverettecb8ed372014-11-18 17:05:35 -0800473 boolean result = mSearchSrcTextView.requestFocus(direction, previouslyFocusedRect);
Amith Yamasanif28d1872011-07-26 12:21:03 -0700474 if (result) {
475 updateViewsVisibility(false);
476 }
Amith Yamasani7f8aef62011-01-25 11:58:09 -0800477 return result;
478 } else {
479 return super.requestFocus(direction, previouslyFocusedRect);
480 }
Amith Yamasani05944762010-10-08 13:52:38 -0700481 }
482
483 /** @hide */
484 @Override
485 public void clearFocus() {
486 mClearingFocus = true;
487 super.clearFocus();
Alan Viverettecb8ed372014-11-18 17:05:35 -0800488 mSearchSrcTextView.clearFocus();
Yohei Yukawa5cfc1b42017-03-07 00:52:09 -0800489 mSearchSrcTextView.setImeVisibility(false);
Amith Yamasani05944762010-10-08 13:52:38 -0700490 mClearingFocus = false;
491 }
492
Amith Yamasani733cbd52010-09-03 12:21:39 -0700493 /**
494 * Sets a listener for user actions within the SearchView.
495 *
496 * @param listener the listener object that receives callbacks when the user performs
497 * actions in the SearchView such as clicking on buttons or typing a query.
498 */
Adam Powell01f21352011-01-20 18:30:10 -0800499 public void setOnQueryTextListener(OnQueryTextListener listener) {
Amith Yamasani733cbd52010-09-03 12:21:39 -0700500 mOnQueryChangeListener = listener;
501 }
502
503 /**
Amith Yamasani93227752010-09-14 10:10:54 -0700504 * Sets a listener to inform when the user closes the SearchView.
505 *
506 * @param listener the listener to call when the user closes the SearchView.
507 */
508 public void setOnCloseListener(OnCloseListener listener) {
509 mOnCloseListener = listener;
510 }
511
512 /**
Amith Yamasani05944762010-10-08 13:52:38 -0700513 * Sets a listener to inform when the focus of the query text field changes.
514 *
515 * @param listener the listener to inform of focus changes.
516 */
517 public void setOnQueryTextFocusChangeListener(OnFocusChangeListener listener) {
518 mOnQueryTextFocusChangeListener = listener;
519 }
520
521 /**
522 * Sets a listener to inform when a suggestion is focused or clicked.
523 *
524 * @param listener the listener to inform of suggestion selection events.
525 */
Adam Powell01f21352011-01-20 18:30:10 -0800526 public void setOnSuggestionListener(OnSuggestionListener listener) {
Amith Yamasani05944762010-10-08 13:52:38 -0700527 mOnSuggestionListener = listener;
528 }
529
530 /**
Amith Yamasani48385482010-12-03 14:43:52 -0800531 * Sets a listener to inform when the search button is pressed. This is only
Scott Maincccdbe92011-02-06 15:51:47 -0800532 * relevant when the text field is not visible by default. Calling {@link #setIconified
533 * setIconified(false)} can also cause this listener to be informed.
Amith Yamasani48385482010-12-03 14:43:52 -0800534 *
535 * @param listener the listener to inform when the search button is clicked or
536 * the text field is programmatically de-iconified.
537 */
538 public void setOnSearchClickListener(OnClickListener listener) {
539 mOnSearchClickListener = listener;
540 }
541
542 /**
543 * Returns the query string currently in the text field.
544 *
545 * @return the query string
546 */
547 public CharSequence getQuery() {
Alan Viverettecb8ed372014-11-18 17:05:35 -0800548 return mSearchSrcTextView.getText();
Amith Yamasani48385482010-12-03 14:43:52 -0800549 }
550
551 /**
Amith Yamasani733cbd52010-09-03 12:21:39 -0700552 * Sets a query string in the text field and optionally submits the query as well.
553 *
554 * @param query the query string. This replaces any query text already present in the
555 * text field.
556 * @param submit whether to submit the query right now or only update the contents of
557 * text field.
558 */
559 public void setQuery(CharSequence query, boolean submit) {
Alan Viverettecb8ed372014-11-18 17:05:35 -0800560 mSearchSrcTextView.setText(query);
Dmitri Plotnikov87c50252010-10-21 21:16:42 -0700561 if (query != null) {
Alan Viverettecb8ed372014-11-18 17:05:35 -0800562 mSearchSrcTextView.setSelection(mSearchSrcTextView.length());
Amith Yamasani068d73c2011-05-27 15:15:14 -0700563 mUserQuery = query;
Dmitri Plotnikov87c50252010-10-21 21:16:42 -0700564 }
565
Amith Yamasani733cbd52010-09-03 12:21:39 -0700566 // If the query is not empty and submit is requested, submit the query
567 if (submit && !TextUtils.isEmpty(query)) {
568 onSubmitQuery();
569 }
570 }
571
572 /**
Alan Viveretteb4004df2015-04-29 16:55:42 -0700573 * Sets the hint text to display in the query text field. This overrides
574 * any hint specified in the {@link SearchableInfo}.
575 * <p>
576 * This value may be specified as an empty string to prevent any query hint
577 * from being displayed.
Amith Yamasani733cbd52010-09-03 12:21:39 -0700578 *
Alan Viveretteb4004df2015-04-29 16:55:42 -0700579 * @param hint the hint text to display or {@code null} to clear
Scott Mainabdf0d52011-02-08 10:20:27 -0800580 * @attr ref android.R.styleable#SearchView_queryHint
Amith Yamasani733cbd52010-09-03 12:21:39 -0700581 */
Alan Viveretteb4004df2015-04-29 16:55:42 -0700582 public void setQueryHint(@Nullable CharSequence hint) {
Amith Yamasani733cbd52010-09-03 12:21:39 -0700583 mQueryHint = hint;
584 updateQueryHint();
585 }
586
587 /**
Alan Viveretteb4004df2015-04-29 16:55:42 -0700588 * Returns the hint text that will be displayed in the query text field.
589 * <p>
590 * The displayed query hint is chosen in the following order:
591 * <ol>
592 * <li>Non-null value set with {@link #setQueryHint(CharSequence)}
593 * <li>Value specified in XML using
594 * {@link android.R.styleable#SearchView_queryHint android:queryHint}
595 * <li>Valid string resource ID exposed by the {@link SearchableInfo} via
596 * {@link SearchableInfo#getHintId()}
597 * <li>Default hint provided by the theme against which the view was
598 * inflated
599 * </ol>
Amith Yamasanieca59d32012-04-25 18:57:18 -0700600 *
Alan Viveretteb4004df2015-04-29 16:55:42 -0700601 * @return the displayed query hint text, or {@code null} if none set
Amith Yamasanieca59d32012-04-25 18:57:18 -0700602 * @attr ref android.R.styleable#SearchView_queryHint
603 */
Alan Viveretteb4004df2015-04-29 16:55:42 -0700604 @Nullable
Amith Yamasanieca59d32012-04-25 18:57:18 -0700605 public CharSequence getQueryHint() {
Alan Viveretteb4004df2015-04-29 16:55:42 -0700606 final CharSequence hint;
Amith Yamasanieca59d32012-04-25 18:57:18 -0700607 if (mQueryHint != null) {
Alan Viveretteb4004df2015-04-29 16:55:42 -0700608 hint = mQueryHint;
609 } else if (mSearchable != null && mSearchable.getHintId() != 0) {
610 hint = getContext().getText(mSearchable.getHintId());
611 } else {
612 hint = mDefaultQueryHint;
Amith Yamasanieca59d32012-04-25 18:57:18 -0700613 }
Alan Viveretteb4004df2015-04-29 16:55:42 -0700614 return hint;
Amith Yamasanieca59d32012-04-25 18:57:18 -0700615 }
616
617 /**
Amith Yamasani733cbd52010-09-03 12:21:39 -0700618 * Sets the default or resting state of the search field. If true, a single search icon is
619 * shown by default and expands to show the text field and other buttons when pressed. Also,
620 * if the default state is iconified, then it collapses to that state when the close button
Amith Yamasani93227752010-09-14 10:10:54 -0700621 * is pressed. Changes to this property will take effect immediately.
Amith Yamasani733cbd52010-09-03 12:21:39 -0700622 *
Scott Maincccdbe92011-02-06 15:51:47 -0800623 * <p>The default value is true.</p>
Amith Yamasani93227752010-09-14 10:10:54 -0700624 *
625 * @param iconified whether the search field should be iconified by default
Scott Mainabdf0d52011-02-08 10:20:27 -0800626 *
627 * @attr ref android.R.styleable#SearchView_iconifiedByDefault
Amith Yamasani733cbd52010-09-03 12:21:39 -0700628 */
629 public void setIconifiedByDefault(boolean iconified) {
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -0700630 if (mIconifiedByDefault == iconified) return;
Amith Yamasani733cbd52010-09-03 12:21:39 -0700631 mIconifiedByDefault = iconified;
632 updateViewsVisibility(iconified);
Amith Yamasanib47c4fd2011-08-04 14:30:07 -0700633 updateQueryHint();
Amith Yamasani733cbd52010-09-03 12:21:39 -0700634 }
635
Amith Yamasani93227752010-09-14 10:10:54 -0700636 /**
637 * Returns the default iconified state of the search field.
638 * @return
Amith Yamasani3d3e7a52012-05-04 14:46:31 -0700639 *
640 * @attr ref android.R.styleable#SearchView_iconifiedByDefault
Amith Yamasani93227752010-09-14 10:10:54 -0700641 */
Amith Yamasani733cbd52010-09-03 12:21:39 -0700642 public boolean isIconfiedByDefault() {
643 return mIconifiedByDefault;
644 }
645
646 /**
Amith Yamasani93227752010-09-14 10:10:54 -0700647 * Iconifies or expands the SearchView. Any query text is cleared when iconified. This is
648 * a temporary state and does not override the default iconified state set by
649 * {@link #setIconifiedByDefault(boolean)}. If the default state is iconified, then
650 * a false here will only be valid until the user closes the field. And if the default
651 * state is expanded, then a true here will only clear the text field and not close it.
652 *
653 * @param iconify a true value will collapse the SearchView to an icon, while a false will
654 * expand it.
655 */
656 public void setIconified(boolean iconify) {
657 if (iconify) {
658 onCloseClicked();
659 } else {
660 onSearchClicked();
661 }
662 }
663
664 /**
665 * Returns the current iconified state of the SearchView.
666 *
667 * @return true if the SearchView is currently iconified, false if the search field is
668 * fully visible.
669 */
670 public boolean isIconified() {
671 return mIconified;
672 }
673
674 /**
Amith Yamasani733cbd52010-09-03 12:21:39 -0700675 * Enables showing a submit button when the query is non-empty. In cases where the SearchView
676 * is being used to filter the contents of the current activity and doesn't launch a separate
677 * results activity, then the submit button should be disabled.
678 *
679 * @param enabled true to show a submit button for submitting queries, false if a submit
680 * button is not required.
681 */
682 public void setSubmitButtonEnabled(boolean enabled) {
Amith Yamasani733cbd52010-09-03 12:21:39 -0700683 mSubmitButtonEnabled = enabled;
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -0700684 updateViewsVisibility(isIconified());
Amith Yamasani733cbd52010-09-03 12:21:39 -0700685 }
686
687 /**
688 * Returns whether the submit button is enabled when necessary or never displayed.
689 *
690 * @return whether the submit button is enabled automatically when necessary
691 */
692 public boolean isSubmitButtonEnabled() {
693 return mSubmitButtonEnabled;
694 }
695
Amith Yamasanie678f462010-09-15 16:13:43 -0700696 /**
697 * Specifies if a query refinement button should be displayed alongside each suggestion
698 * or if it should depend on the flags set in the individual items retrieved from the
699 * suggestions provider. Clicking on the query refinement button will replace the text
700 * in the query text field with the text from the suggestion. This flag only takes effect
701 * if a SearchableInfo has been specified with {@link #setSearchableInfo(SearchableInfo)}
702 * and not when using a custom adapter.
703 *
704 * @param enable true if all items should have a query refinement button, false if only
705 * those items that have a query refinement flag set should have the button.
706 *
707 * @see SearchManager#SUGGEST_COLUMN_FLAGS
708 * @see SearchManager#FLAG_QUERY_REFINEMENT
709 */
710 public void setQueryRefinementEnabled(boolean enable) {
711 mQueryRefinement = enable;
712 if (mSuggestionsAdapter instanceof SuggestionsAdapter) {
713 ((SuggestionsAdapter) mSuggestionsAdapter).setQueryRefinement(
714 enable ? SuggestionsAdapter.REFINE_ALL : SuggestionsAdapter.REFINE_BY_ENTRY);
715 }
716 }
717
718 /**
719 * Returns whether query refinement is enabled for all items or only specific ones.
720 * @return true if enabled for all items, false otherwise.
721 */
722 public boolean isQueryRefinementEnabled() {
723 return mQueryRefinement;
Amith Yamasani733cbd52010-09-03 12:21:39 -0700724 }
725
726 /**
727 * You can set a custom adapter if you wish. Otherwise the default adapter is used to
728 * display the suggestions from the suggestions provider associated with the SearchableInfo.
729 *
730 * @see #setSearchableInfo(SearchableInfo)
731 */
732 public void setSuggestionsAdapter(CursorAdapter adapter) {
733 mSuggestionsAdapter = adapter;
734
Alan Viverettecb8ed372014-11-18 17:05:35 -0800735 mSearchSrcTextView.setAdapter(mSuggestionsAdapter);
Amith Yamasani733cbd52010-09-03 12:21:39 -0700736 }
737
738 /**
739 * Returns the adapter used for suggestions, if any.
740 * @return the suggestions adapter
741 */
742 public CursorAdapter getSuggestionsAdapter() {
743 return mSuggestionsAdapter;
744 }
745
Amith Yamasani5931b1f2010-10-18 16:13:14 -0700746 /**
747 * Makes the view at most this many pixels wide
748 *
749 * @attr ref android.R.styleable#SearchView_maxWidth
750 */
751 public void setMaxWidth(int maxpixels) {
752 mMaxWidth = maxpixels;
753
754 requestLayout();
755 }
756
Amith Yamasanieca59d32012-04-25 18:57:18 -0700757 /**
758 * Gets the specified maximum width in pixels, if set. Returns zero if
759 * no maximum width was specified.
760 * @return the maximum width of the view
Amith Yamasani3d3e7a52012-05-04 14:46:31 -0700761 *
762 * @attr ref android.R.styleable#SearchView_maxWidth
Amith Yamasanieca59d32012-04-25 18:57:18 -0700763 */
764 public int getMaxWidth() {
765 return mMaxWidth;
766 }
767
Amith Yamasani5931b1f2010-10-18 16:13:14 -0700768 @Override
769 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Amith Yamasania95e4882011-08-17 11:41:37 -0700770 // Let the standard measurements take effect in iconified state.
771 if (isIconified()) {
772 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
773 return;
774 }
775
Amith Yamasani5931b1f2010-10-18 16:13:14 -0700776 int widthMode = MeasureSpec.getMode(widthMeasureSpec);
777 int width = MeasureSpec.getSize(widthMeasureSpec);
778
Amith Yamasani167d69a2011-08-12 19:28:37 -0700779 switch (widthMode) {
780 case MeasureSpec.AT_MOST:
781 // If there is an upper limit, don't exceed maximum width (explicit or implicit)
782 if (mMaxWidth > 0) {
783 width = Math.min(mMaxWidth, width);
784 } else {
785 width = Math.min(getPreferredWidth(), width);
786 }
787 break;
788 case MeasureSpec.EXACTLY:
789 // If an exact width is specified, still don't exceed any specified maximum width
790 if (mMaxWidth > 0) {
791 width = Math.min(mMaxWidth, width);
792 }
793 break;
794 case MeasureSpec.UNSPECIFIED:
795 // Use maximum width, if specified, else preferred width
796 width = mMaxWidth > 0 ? mMaxWidth : getPreferredWidth();
797 break;
Amith Yamasani5931b1f2010-10-18 16:13:14 -0700798 }
Amith Yamasani167d69a2011-08-12 19:28:37 -0700799 widthMode = MeasureSpec.EXACTLY;
Aurimas Liutikasc8fd00a2016-04-15 13:55:53 -0700800
801 int heightMode = MeasureSpec.getMode(heightMeasureSpec);
802 int height = MeasureSpec.getSize(heightMeasureSpec);
803
804 switch (heightMode) {
805 case MeasureSpec.AT_MOST:
Aurimas Liutikasc8fd00a2016-04-15 13:55:53 -0700806 height = Math.min(getPreferredHeight(), height);
807 break;
Aurimas Liutikasf6a50be2016-09-09 15:32:55 -0700808 case MeasureSpec.UNSPECIFIED:
809 height = getPreferredHeight();
810 break;
Aurimas Liutikasc8fd00a2016-04-15 13:55:53 -0700811 }
812 heightMode = MeasureSpec.EXACTLY;
813
814 super.onMeasure(MeasureSpec.makeMeasureSpec(width, widthMode),
815 MeasureSpec.makeMeasureSpec(height, heightMode));
816 }
817
818 @Override
819 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
820 super.onLayout(changed, left, top, right, bottom);
821
822 if (changed) {
823 // Expand mSearchSrcTextView touch target to be the height of the parent in order to
824 // allow it to be up to 48dp.
825 getChildBoundsWithinSearchView(mSearchSrcTextView, mSearchSrcTextViewBounds);
826 mSearchSrtTextViewBoundsExpanded.set(
827 mSearchSrcTextViewBounds.left, 0, mSearchSrcTextViewBounds.right, bottom - top);
828 if (mTouchDelegate == null) {
829 mTouchDelegate = new UpdatableTouchDelegate(mSearchSrtTextViewBoundsExpanded,
830 mSearchSrcTextViewBounds, mSearchSrcTextView);
831 setTouchDelegate(mTouchDelegate);
832 } else {
833 mTouchDelegate.setBounds(mSearchSrtTextViewBoundsExpanded, mSearchSrcTextViewBounds);
834 }
835 }
836 }
837
838 private void getChildBoundsWithinSearchView(View view, Rect rect) {
839 view.getLocationInWindow(mTemp);
840 getLocationInWindow(mTemp2);
841 final int top = mTemp[1] - mTemp2[1];
842 final int left = mTemp[0] - mTemp2[0];
843 rect.set(left , top, left + view.getWidth(), top + view.getHeight());
Amith Yamasani167d69a2011-08-12 19:28:37 -0700844 }
845
846 private int getPreferredWidth() {
847 return getContext().getResources()
848 .getDimensionPixelSize(R.dimen.search_view_preferred_width);
Amith Yamasani5931b1f2010-10-18 16:13:14 -0700849 }
850
Aurimas Liutikasc8fd00a2016-04-15 13:55:53 -0700851 private int getPreferredHeight() {
852 return getContext().getResources()
853 .getDimensionPixelSize(R.dimen.search_view_preferred_height);
854 }
855
Amith Yamasani93227752010-09-14 10:10:54 -0700856 private void updateViewsVisibility(final boolean collapsed) {
857 mIconified = collapsed;
Amith Yamasani733cbd52010-09-03 12:21:39 -0700858 // Visibility of views that are visible when collapsed
Amith Yamasani93227752010-09-14 10:10:54 -0700859 final int visCollapsed = collapsed ? VISIBLE : GONE;
Amith Yamasani05944762010-10-08 13:52:38 -0700860 // Is there text in the query
Alan Viverettecb8ed372014-11-18 17:05:35 -0800861 final boolean hasText = !TextUtils.isEmpty(mSearchSrcTextView.getText());
Amith Yamasani733cbd52010-09-03 12:21:39 -0700862
863 mSearchButton.setVisibility(visCollapsed);
Amith Yamasani9b2e3022011-01-14 11:34:12 -0800864 updateSubmitButton(hasText);
865 mSearchEditFrame.setVisibility(collapsed ? GONE : VISIBLE);
Alan Viverette53b165e2015-08-25 12:33:01 -0400866
867 final int iconVisibility;
868 if (mCollapsedIcon.getDrawable() == null || mIconifiedByDefault) {
869 iconVisibility = GONE;
870 } else {
871 iconVisibility = VISIBLE;
872 }
873 mCollapsedIcon.setVisibility(iconVisibility);
874
Amith Yamasani4aedb392010-12-15 16:04:57 -0800875 updateCloseButton();
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -0700876 updateVoiceButton(!hasText);
Amith Yamasani9b2e3022011-01-14 11:34:12 -0800877 updateSubmitArea();
Amith Yamasani9b2e3022011-01-14 11:34:12 -0800878 }
879
880 private boolean hasVoiceSearch() {
881 if (mSearchable != null && mSearchable.getVoiceSearchEnabled()) {
882 Intent testIntent = null;
883 if (mSearchable.getVoiceSearchLaunchWebSearch()) {
884 testIntent = mVoiceWebSearchIntent;
885 } else if (mSearchable.getVoiceSearchLaunchRecognizer()) {
886 testIntent = mVoiceAppSearchIntent;
887 }
888 if (testIntent != null) {
889 ResolveInfo ri = getContext().getPackageManager().resolveActivity(testIntent,
890 PackageManager.MATCH_DEFAULT_ONLY);
891 return ri != null;
892 }
893 }
894 return false;
895 }
896
897 private boolean isSubmitAreaEnabled() {
898 return (mSubmitButtonEnabled || mVoiceButtonEnabled) && !isIconified();
899 }
900
901 private void updateSubmitButton(boolean hasText) {
Amith Yamasani79f74302011-03-08 14:16:35 -0800902 int visibility = GONE;
Amith Yamasanicf72ab42011-11-04 13:49:28 -0700903 if (mSubmitButtonEnabled && isSubmitAreaEnabled() && hasFocus()
904 && (hasText || !mVoiceButtonEnabled)) {
Amith Yamasani79f74302011-03-08 14:16:35 -0800905 visibility = VISIBLE;
906 }
Alan Viverettecb8ed372014-11-18 17:05:35 -0800907 mGoButton.setVisibility(visibility);
Amith Yamasani9b2e3022011-01-14 11:34:12 -0800908 }
909
910 private void updateSubmitArea() {
911 int visibility = GONE;
Amith Yamasani79f74302011-03-08 14:16:35 -0800912 if (isSubmitAreaEnabled()
Alan Viverettecb8ed372014-11-18 17:05:35 -0800913 && (mGoButton.getVisibility() == VISIBLE
Amith Yamasani79f74302011-03-08 14:16:35 -0800914 || mVoiceButton.getVisibility() == VISIBLE)) {
915 visibility = VISIBLE;
Amith Yamasani9b2e3022011-01-14 11:34:12 -0800916 }
917 mSubmitArea.setVisibility(visibility);
Amith Yamasani733cbd52010-09-03 12:21:39 -0700918 }
919
Amith Yamasani4aedb392010-12-15 16:04:57 -0800920 private void updateCloseButton() {
Alan Viverettecb8ed372014-11-18 17:05:35 -0800921 final boolean hasText = !TextUtils.isEmpty(mSearchSrcTextView.getText());
Amith Yamasani4aedb392010-12-15 16:04:57 -0800922 // Should we show the close button? It is not shown if there's no focus,
923 // field is not iconified by default and there is no text in it.
Amith Yamasani763bc072011-07-22 11:53:47 -0700924 final boolean showClose = hasText || (mIconifiedByDefault && !mExpandedInActionView);
Amith Yamasani167d69a2011-08-12 19:28:37 -0700925 mCloseButton.setVisibility(showClose ? VISIBLE : GONE);
Alan Viverettecb8ed372014-11-18 17:05:35 -0800926 final Drawable closeButtonImg = mCloseButton.getDrawable();
927 if (closeButtonImg != null){
928 closeButtonImg.setState(hasText ? ENABLED_STATE_SET : EMPTY_STATE_SET);
929 }
Amith Yamasani4aedb392010-12-15 16:04:57 -0800930 }
931
Amith Yamasania95e4882011-08-17 11:41:37 -0700932 private void postUpdateFocusedState() {
933 post(mUpdateDrawableStateRunnable);
934 }
935
936 private void updateFocusedState() {
Alan Viverettecb8ed372014-11-18 17:05:35 -0800937 final boolean focused = mSearchSrcTextView.hasFocus();
938 final int[] stateSet = focused ? FOCUSED_STATE_SET : EMPTY_STATE_SET;
939 final Drawable searchPlateBg = mSearchPlate.getBackground();
940 if (searchPlateBg != null) {
941 searchPlateBg.setState(stateSet);
942 }
943 final Drawable submitAreaBg = mSubmitArea.getBackground();
944 if (submitAreaBg != null) {
945 submitAreaBg.setState(stateSet);
946 }
Amith Yamasania95e4882011-08-17 11:41:37 -0700947 invalidate();
948 }
949
950 @Override
Amith Yamasania465b2d2011-08-19 13:01:22 -0700951 protected void onDetachedFromWindow() {
Amith Yamasania95e4882011-08-17 11:41:37 -0700952 removeCallbacks(mUpdateDrawableStateRunnable);
Amith Yamasani87907642011-11-03 11:32:44 -0700953 post(mReleaseCursorRunnable);
Amith Yamasania95e4882011-08-17 11:41:37 -0700954 super.onDetachedFromWindow();
Amith Yamasani79f74302011-03-08 14:16:35 -0800955 }
956
Amith Yamasanie678f462010-09-15 16:13:43 -0700957 /**
958 * Called by the SuggestionsAdapter
959 * @hide
960 */
961 /* package */void onQueryRefine(CharSequence queryText) {
962 setQuery(queryText);
963 }
964
Amith Yamasani733cbd52010-09-03 12:21:39 -0700965 private final OnClickListener mOnClickListener = new OnClickListener() {
966
967 public void onClick(View v) {
968 if (v == mSearchButton) {
969 onSearchClicked();
970 } else if (v == mCloseButton) {
971 onCloseClicked();
Alan Viverettecb8ed372014-11-18 17:05:35 -0800972 } else if (v == mGoButton) {
Amith Yamasani733cbd52010-09-03 12:21:39 -0700973 onSubmitQuery();
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -0700974 } else if (v == mVoiceButton) {
975 onVoiceClicked();
Alan Viverettecb8ed372014-11-18 17:05:35 -0800976 } else if (v == mSearchSrcTextView) {
Amith Yamasanif28d1872011-07-26 12:21:03 -0700977 forceSuggestionQuery();
Amith Yamasani733cbd52010-09-03 12:21:39 -0700978 }
979 }
980 };
981
982 /**
983 * Handles the key down event for dealing with action keys.
984 *
985 * @param keyCode This is the keycode of the typed key, and is the same value as
986 * found in the KeyEvent parameter.
987 * @param event The complete event record for the typed key
988 *
989 * @return true if the event was handled here, or false if not.
990 */
991 @Override
992 public boolean onKeyDown(int keyCode, KeyEvent event) {
993 if (mSearchable == null) {
994 return false;
995 }
996
997 // if it's an action specified by the searchable activity, launch the
998 // entered query with the action key
999 SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
1000 if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) {
Alan Viverettecb8ed372014-11-18 17:05:35 -08001001 launchQuerySearch(keyCode, actionKey.getQueryActionMsg(), mSearchSrcTextView.getText()
Amith Yamasani93227752010-09-14 10:10:54 -07001002 .toString());
Amith Yamasani733cbd52010-09-03 12:21:39 -07001003 return true;
1004 }
1005
1006 return super.onKeyDown(keyCode, event);
1007 }
1008
Amith Yamasani968ec932010-12-02 14:00:47 -08001009 /**
1010 * React to the user typing "enter" or other hardwired keys while typing in
1011 * the search box. This handles these special keys while the edit box has
1012 * focus.
1013 */
1014 View.OnKeyListener mTextKeyListener = new View.OnKeyListener() {
1015 public boolean onKey(View v, int keyCode, KeyEvent event) {
1016 // guard against possible race conditions
1017 if (mSearchable == null) {
1018 return false;
1019 }
1020
1021 if (DBG) {
1022 Log.d(LOG_TAG, "mTextListener.onKey(" + keyCode + "," + event + "), selection: "
Alan Viverettecb8ed372014-11-18 17:05:35 -08001023 + mSearchSrcTextView.getListSelection());
Amith Yamasani968ec932010-12-02 14:00:47 -08001024 }
1025
1026 // If a suggestion is selected, handle enter, search key, and action keys
1027 // as presses on the selected suggestion
Alan Viverettecb8ed372014-11-18 17:05:35 -08001028 if (mSearchSrcTextView.isPopupShowing()
1029 && mSearchSrcTextView.getListSelection() != ListView.INVALID_POSITION) {
Amith Yamasani968ec932010-12-02 14:00:47 -08001030 return onSuggestionsKey(v, keyCode, event);
1031 }
1032
1033 // If there is text in the query box, handle enter, and action keys
1034 // The search key is handled by the dialog's onKeyDown().
Alan Viverettecb8ed372014-11-18 17:05:35 -08001035 if (!mSearchSrcTextView.isEmpty() && event.hasNoModifiers()) {
Jeff Brown4e6319b2010-12-13 10:36:51 -08001036 if (event.getAction() == KeyEvent.ACTION_UP) {
1037 if (keyCode == KeyEvent.KEYCODE_ENTER) {
1038 v.cancelLongPress();
Amith Yamasani968ec932010-12-02 14:00:47 -08001039
Jeff Brown4e6319b2010-12-13 10:36:51 -08001040 // Launch as a regular search.
Alan Viverettecb8ed372014-11-18 17:05:35 -08001041 launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null, mSearchSrcTextView.getText()
Jeff Brown4e6319b2010-12-13 10:36:51 -08001042 .toString());
1043 return true;
1044 }
Amith Yamasani968ec932010-12-02 14:00:47 -08001045 }
1046 if (event.getAction() == KeyEvent.ACTION_DOWN) {
1047 SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
1048 if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) {
Alan Viverettecb8ed372014-11-18 17:05:35 -08001049 launchQuerySearch(keyCode, actionKey.getQueryActionMsg(), mSearchSrcTextView
Amith Yamasani968ec932010-12-02 14:00:47 -08001050 .getText().toString());
1051 return true;
1052 }
1053 }
1054 }
1055 return false;
1056 }
1057 };
1058
1059 /**
1060 * React to the user typing while in the suggestions list. First, check for
1061 * action keys. If not handled, try refocusing regular characters into the
1062 * EditText.
1063 */
1064 private boolean onSuggestionsKey(View v, int keyCode, KeyEvent event) {
1065 // guard against possible race conditions (late arrival after dismiss)
1066 if (mSearchable == null) {
1067 return false;
1068 }
1069 if (mSuggestionsAdapter == null) {
1070 return false;
1071 }
Jeff Brown4e6319b2010-12-13 10:36:51 -08001072 if (event.getAction() == KeyEvent.ACTION_DOWN && event.hasNoModifiers()) {
Amith Yamasani968ec932010-12-02 14:00:47 -08001073 // First, check for enter or search (both of which we'll treat as a
1074 // "click")
Jeff Brown4e6319b2010-12-13 10:36:51 -08001075 if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_SEARCH
1076 || keyCode == KeyEvent.KEYCODE_TAB) {
Alan Viverettecb8ed372014-11-18 17:05:35 -08001077 int position = mSearchSrcTextView.getListSelection();
Amith Yamasani968ec932010-12-02 14:00:47 -08001078 return onItemClicked(position, KeyEvent.KEYCODE_UNKNOWN, null);
1079 }
1080
1081 // Next, check for left/right moves, which we use to "return" the
1082 // user to the edit view
1083 if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
1084 // give "focus" to text editor, with cursor at the beginning if
1085 // left key, at end if right key
1086 // TODO: Reverse left/right for right-to-left languages, e.g.
1087 // Arabic
Alan Viverettecb8ed372014-11-18 17:05:35 -08001088 int selPoint = (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) ? 0 : mSearchSrcTextView
Amith Yamasani968ec932010-12-02 14:00:47 -08001089 .length();
Alan Viverettecb8ed372014-11-18 17:05:35 -08001090 mSearchSrcTextView.setSelection(selPoint);
1091 mSearchSrcTextView.setListSelection(0);
1092 mSearchSrcTextView.clearListSelection();
1093 mSearchSrcTextView.ensureImeVisible(true);
Amith Yamasani968ec932010-12-02 14:00:47 -08001094
1095 return true;
1096 }
1097
1098 // Next, check for an "up and out" move
Alan Viverettecb8ed372014-11-18 17:05:35 -08001099 if (keyCode == KeyEvent.KEYCODE_DPAD_UP && 0 == mSearchSrcTextView.getListSelection()) {
Amith Yamasani968ec932010-12-02 14:00:47 -08001100 // TODO: restoreUserQuery();
1101 // let ACTV complete the move
1102 return false;
1103 }
1104
1105 // Next, check for an "action key"
1106 SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
1107 if ((actionKey != null)
1108 && ((actionKey.getSuggestActionMsg() != null) || (actionKey
1109 .getSuggestActionMsgColumn() != null))) {
1110 // launch suggestion using action key column
Alan Viverettecb8ed372014-11-18 17:05:35 -08001111 int position = mSearchSrcTextView.getListSelection();
Amith Yamasani968ec932010-12-02 14:00:47 -08001112 if (position != ListView.INVALID_POSITION) {
1113 Cursor c = mSuggestionsAdapter.getCursor();
1114 if (c.moveToPosition(position)) {
1115 final String actionMsg = getActionKeyMessage(c, actionKey);
1116 if (actionMsg != null && (actionMsg.length() > 0)) {
1117 return onItemClicked(position, keyCode, actionMsg);
1118 }
1119 }
1120 }
1121 }
1122 }
1123 return false;
1124 }
1125
1126 /**
1127 * For a given suggestion and a given cursor row, get the action message. If
1128 * not provided by the specific row/column, also check for a single
1129 * definition (for the action key).
1130 *
1131 * @param c The cursor providing suggestions
1132 * @param actionKey The actionkey record being examined
1133 *
1134 * @return Returns a string, or null if no action key message for this
1135 * suggestion
1136 */
1137 private static String getActionKeyMessage(Cursor c, SearchableInfo.ActionKeyInfo actionKey) {
1138 String result = null;
1139 // check first in the cursor data, for a suggestion-specific message
1140 final String column = actionKey.getSuggestActionMsgColumn();
1141 if (column != null) {
1142 result = SuggestionsAdapter.getColumnString(c, column);
1143 }
1144 // If the cursor didn't give us a message, see if there's a single
1145 // message defined
1146 // for the actionkey (for all suggestions)
1147 if (result == null) {
1148 result = actionKey.getSuggestActionMsg();
1149 }
1150 return result;
1151 }
1152
Amith Yamasanib4569fb2011-07-08 15:25:39 -07001153 private CharSequence getDecoratedHint(CharSequence hintText) {
Alan Viverettecb8ed372014-11-18 17:05:35 -08001154 // If the field is always expanded or we don't have a search hint icon,
1155 // then don't add the search icon to the hint.
1156 if (!mIconifiedByDefault || mSearchHintIcon == null) {
Alan Viverette5dddb702014-07-02 15:46:04 -07001157 return hintText;
1158 }
Amith Yamasanib4569fb2011-07-08 15:25:39 -07001159
Alan Viverettecb8ed372014-11-18 17:05:35 -08001160 final int textSize = (int) (mSearchSrcTextView.getTextSize() * 1.25);
1161 mSearchHintIcon.setBounds(0, 0, textSize, textSize);
Alan Viverette5dddb702014-07-02 15:46:04 -07001162
Alan Viverettecb8ed372014-11-18 17:05:35 -08001163 final SpannableStringBuilder ssb = new SpannableStringBuilder(" ");
1164 ssb.setSpan(new ImageSpan(mSearchHintIcon), 1, 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
Alan Viverette5dddb702014-07-02 15:46:04 -07001165 ssb.append(hintText);
Amith Yamasanib4569fb2011-07-08 15:25:39 -07001166 return ssb;
1167 }
1168
Amith Yamasani733cbd52010-09-03 12:21:39 -07001169 private void updateQueryHint() {
Alan Viveretteb4004df2015-04-29 16:55:42 -07001170 final CharSequence hint = getQueryHint();
1171 mSearchSrcTextView.setHint(getDecoratedHint(hint == null ? "" : hint));
Amith Yamasani733cbd52010-09-03 12:21:39 -07001172 }
1173
1174 /**
1175 * Updates the auto-complete text view.
1176 */
1177 private void updateSearchAutoComplete() {
Alan Viverettecb8ed372014-11-18 17:05:35 -08001178 mSearchSrcTextView.setDropDownAnimationStyle(0); // no animation
1179 mSearchSrcTextView.setThreshold(mSearchable.getSuggestThreshold());
1180 mSearchSrcTextView.setImeOptions(mSearchable.getImeOptions());
Amith Yamasani5607a382011-08-09 14:16:37 -07001181 int inputType = mSearchable.getInputType();
1182 // We only touch this if the input type is set up for text (which it almost certainly
1183 // should be, in the case of search!)
1184 if ((inputType & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_TEXT) {
1185 // The existence of a suggestions authority is the proxy for "suggestions
1186 // are available here"
1187 inputType &= ~InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;
1188 if (mSearchable.getSuggestAuthority() != null) {
1189 inputType |= InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;
Satoshi Kataoka9ce11162012-06-04 17:12:08 +09001190 // TYPE_TEXT_FLAG_AUTO_COMPLETE means that the text editor is performing
1191 // auto-completion based on its own semantics, which it will present to the user
1192 // as they type. This generally means that the input method should not show its
1193 // own candidates, and the spell checker should not be in action. The text editor
1194 // supplies its candidates by calling InputMethodManager.displayCompletions(),
1195 // which in turn will call InputMethodSession.displayCompletions().
1196 inputType |= InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
Amith Yamasani5607a382011-08-09 14:16:37 -07001197 }
1198 }
Alan Viverettecb8ed372014-11-18 17:05:35 -08001199 mSearchSrcTextView.setInputType(inputType);
Amith Yamasani87907642011-11-03 11:32:44 -07001200 if (mSuggestionsAdapter != null) {
1201 mSuggestionsAdapter.changeCursor(null);
1202 }
Amith Yamasani733cbd52010-09-03 12:21:39 -07001203 // attach the suggestions adapter, if suggestions are available
1204 // The existence of a suggestions authority is the proxy for "suggestions available here"
1205 if (mSearchable.getSuggestAuthority() != null) {
1206 mSuggestionsAdapter = new SuggestionsAdapter(getContext(),
1207 this, mSearchable, mOutsideDrawablesCache);
Alan Viverettecb8ed372014-11-18 17:05:35 -08001208 mSearchSrcTextView.setAdapter(mSuggestionsAdapter);
Amith Yamasanie678f462010-09-15 16:13:43 -07001209 ((SuggestionsAdapter) mSuggestionsAdapter).setQueryRefinement(
1210 mQueryRefinement ? SuggestionsAdapter.REFINE_ALL
1211 : SuggestionsAdapter.REFINE_BY_ENTRY);
Amith Yamasani733cbd52010-09-03 12:21:39 -07001212 }
1213 }
1214
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001215 /**
1216 * Update the visibility of the voice button. There are actually two voice search modes,
1217 * either of which will activate the button.
1218 * @param empty whether the search query text field is empty. If it is, then the other
Amith Yamasani79f74302011-03-08 14:16:35 -08001219 * criteria apply to make the voice button visible.
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001220 */
1221 private void updateVoiceButton(boolean empty) {
Amith Yamasani79f74302011-03-08 14:16:35 -08001222 int visibility = GONE;
Amith Yamasani167d69a2011-08-12 19:28:37 -07001223 if (mVoiceButtonEnabled && !isIconified() && empty) {
Amith Yamasani9b2e3022011-01-14 11:34:12 -08001224 visibility = VISIBLE;
Alan Viverettecb8ed372014-11-18 17:05:35 -08001225 mGoButton.setVisibility(GONE);
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001226 }
1227 mVoiceButton.setVisibility(visibility);
1228 }
1229
Amith Yamasani733cbd52010-09-03 12:21:39 -07001230 private final OnEditorActionListener mOnEditorActionListener = new OnEditorActionListener() {
1231
1232 /**
1233 * Called when the input method default action key is pressed.
1234 */
1235 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
1236 onSubmitQuery();
1237 return true;
1238 }
1239 };
1240
1241 private void onTextChanged(CharSequence newText) {
Alan Viverettecb8ed372014-11-18 17:05:35 -08001242 CharSequence text = mSearchSrcTextView.getText();
Amith Yamasani068d73c2011-05-27 15:15:14 -07001243 mUserQuery = text;
Amith Yamasani733cbd52010-09-03 12:21:39 -07001244 boolean hasText = !TextUtils.isEmpty(text);
Amith Yamasanicf72ab42011-11-04 13:49:28 -07001245 updateSubmitButton(hasText);
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001246 updateVoiceButton(!hasText);
Amith Yamasani73e00df2010-12-16 16:31:29 -08001247 updateCloseButton();
Amith Yamasani9b2e3022011-01-14 11:34:12 -08001248 updateSubmitArea();
Amith Yamasanib47c4fd2011-08-04 14:30:07 -07001249 if (mOnQueryChangeListener != null && !TextUtils.equals(newText, mOldQueryText)) {
Adam Powell01f21352011-01-20 18:30:10 -08001250 mOnQueryChangeListener.onQueryTextChange(newText.toString());
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001251 }
Amith Yamasanib47c4fd2011-08-04 14:30:07 -07001252 mOldQueryText = newText.toString();
Amith Yamasani733cbd52010-09-03 12:21:39 -07001253 }
1254
1255 private void onSubmitQuery() {
Alan Viverettecb8ed372014-11-18 17:05:35 -08001256 CharSequence query = mSearchSrcTextView.getText();
Amith Yamasani6a7421b2011-07-27 11:55:53 -07001257 if (query != null && TextUtils.getTrimmedLength(query) > 0) {
Amith Yamasani733cbd52010-09-03 12:21:39 -07001258 if (mOnQueryChangeListener == null
Adam Powell01f21352011-01-20 18:30:10 -08001259 || !mOnQueryChangeListener.onQueryTextSubmit(query.toString())) {
Amith Yamasani733cbd52010-09-03 12:21:39 -07001260 if (mSearchable != null) {
1261 launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null, query.toString());
1262 }
Yohei Yukawa5cfc1b42017-03-07 00:52:09 -08001263 mSearchSrcTextView.setImeVisibility(false);
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001264 dismissSuggestions();
Amith Yamasani733cbd52010-09-03 12:21:39 -07001265 }
1266 }
1267 }
1268
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001269 private void dismissSuggestions() {
Alan Viverettecb8ed372014-11-18 17:05:35 -08001270 mSearchSrcTextView.dismissDropDown();
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001271 }
1272
Amith Yamasani733cbd52010-09-03 12:21:39 -07001273 private void onCloseClicked() {
Alan Viverettecb8ed372014-11-18 17:05:35 -08001274 CharSequence text = mSearchSrcTextView.getText();
Amith Yamasani24652982011-06-23 16:16:05 -07001275 if (TextUtils.isEmpty(text)) {
1276 if (mIconifiedByDefault) {
Amith Yamasanib4569fb2011-07-08 15:25:39 -07001277 // If the app doesn't override the close behavior
1278 if (mOnCloseListener == null || !mOnCloseListener.onClose()) {
1279 // hide the keyboard and remove focus
1280 clearFocus();
1281 // collapse the search field
1282 updateViewsVisibility(true);
1283 }
Amith Yamasani05944762010-10-08 13:52:38 -07001284 }
Amith Yamasani24652982011-06-23 16:16:05 -07001285 } else {
Alan Viverettecb8ed372014-11-18 17:05:35 -08001286 mSearchSrcTextView.setText("");
1287 mSearchSrcTextView.requestFocus();
Yohei Yukawa5cfc1b42017-03-07 00:52:09 -08001288 mSearchSrcTextView.setImeVisibility(true);
Amith Yamasani24652982011-06-23 16:16:05 -07001289 }
1290
Amith Yamasani733cbd52010-09-03 12:21:39 -07001291 }
1292
1293 private void onSearchClicked() {
Amith Yamasani733cbd52010-09-03 12:21:39 -07001294 updateViewsVisibility(false);
Alan Viverettecb8ed372014-11-18 17:05:35 -08001295 mSearchSrcTextView.requestFocus();
Yohei Yukawa5cfc1b42017-03-07 00:52:09 -08001296 mSearchSrcTextView.setImeVisibility(true);
Amith Yamasani48385482010-12-03 14:43:52 -08001297 if (mOnSearchClickListener != null) {
1298 mOnSearchClickListener.onClick(this);
1299 }
Amith Yamasani733cbd52010-09-03 12:21:39 -07001300 }
1301
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001302 private void onVoiceClicked() {
1303 // guard against possible race conditions
1304 if (mSearchable == null) {
1305 return;
1306 }
1307 SearchableInfo searchable = mSearchable;
1308 try {
1309 if (searchable.getVoiceSearchLaunchWebSearch()) {
1310 Intent webSearchIntent = createVoiceWebSearchIntent(mVoiceWebSearchIntent,
1311 searchable);
1312 getContext().startActivity(webSearchIntent);
1313 } else if (searchable.getVoiceSearchLaunchRecognizer()) {
1314 Intent appSearchIntent = createVoiceAppSearchIntent(mVoiceAppSearchIntent,
1315 searchable);
1316 getContext().startActivity(appSearchIntent);
1317 }
1318 } catch (ActivityNotFoundException e) {
1319 // Should not happen, since we check the availability of
1320 // voice search before showing the button. But just in case...
1321 Log.w(LOG_TAG, "Could not find voice search activity");
1322 }
1323 }
1324
Amith Yamasani4aedb392010-12-15 16:04:57 -08001325 void onTextFocusChanged() {
Amith Yamasani79f74302011-03-08 14:16:35 -08001326 updateViewsVisibility(isIconified());
Amith Yamasania95e4882011-08-17 11:41:37 -07001327 // Delayed update to make sure that the focus has settled down and window focus changes
1328 // don't affect it. A synchronous update was not working.
1329 postUpdateFocusedState();
Alan Viverettecb8ed372014-11-18 17:05:35 -08001330 if (mSearchSrcTextView.hasFocus()) {
Amith Yamasanif28d1872011-07-26 12:21:03 -07001331 forceSuggestionQuery();
1332 }
Amith Yamasani4aedb392010-12-15 16:04:57 -08001333 }
1334
Amith Yamasanib4569fb2011-07-08 15:25:39 -07001335 @Override
Amith Yamasania95e4882011-08-17 11:41:37 -07001336 public void onWindowFocusChanged(boolean hasWindowFocus) {
1337 super.onWindowFocusChanged(hasWindowFocus);
1338
1339 postUpdateFocusedState();
Amith Yamasanib4569fb2011-07-08 15:25:39 -07001340 }
1341
Amith Yamasani763bc072011-07-22 11:53:47 -07001342 /**
1343 * {@inheritDoc}
1344 */
1345 @Override
1346 public void onActionViewCollapsed() {
Adam Powell99d0ce92012-12-10 13:11:47 -08001347 setQuery("", false);
Amith Yamasani10da5902011-07-26 16:14:26 -07001348 clearFocus();
1349 updateViewsVisibility(true);
Alan Viverettecb8ed372014-11-18 17:05:35 -08001350 mSearchSrcTextView.setImeOptions(mCollapsedImeOptions);
Amith Yamasani763bc072011-07-22 11:53:47 -07001351 mExpandedInActionView = false;
1352 }
1353
1354 /**
1355 * {@inheritDoc}
1356 */
1357 @Override
1358 public void onActionViewExpanded() {
Amith Yamasani434c73f2011-11-01 11:44:50 -07001359 if (mExpandedInActionView) return;
1360
Amith Yamasani763bc072011-07-22 11:53:47 -07001361 mExpandedInActionView = true;
Alan Viverettecb8ed372014-11-18 17:05:35 -08001362 mCollapsedImeOptions = mSearchSrcTextView.getImeOptions();
1363 mSearchSrcTextView.setImeOptions(mCollapsedImeOptions | EditorInfo.IME_FLAG_NO_FULLSCREEN);
1364 mSearchSrcTextView.setText("");
Amith Yamasani763bc072011-07-22 11:53:47 -07001365 setIconified(false);
1366 }
1367
Aurimas Liutikas13fdea02016-02-11 10:10:10 -08001368 static class SavedState extends BaseSavedState {
1369 boolean isIconified;
1370
1371 SavedState(Parcelable superState) {
1372 super(superState);
1373 }
1374
1375 public SavedState(Parcel source) {
1376 super(source);
1377 isIconified = (Boolean) source.readValue(null);
1378 }
1379
1380 @Override
1381 public void writeToParcel(Parcel dest, int flags) {
1382 super.writeToParcel(dest, flags);
1383 dest.writeValue(isIconified);
1384 }
1385
1386 @Override
1387 public String toString() {
1388 return "SearchView.SavedState{"
1389 + Integer.toHexString(System.identityHashCode(this))
1390 + " isIconified=" + isIconified + "}";
1391 }
Aurimas Liutikas7849a3d2016-02-26 15:27:31 -08001392
1393 public static final Parcelable.Creator<SavedState> CREATOR =
1394 new Parcelable.Creator<SavedState>() {
1395 public SavedState createFromParcel(Parcel in) {
1396 return new SavedState(in);
1397 }
1398
1399 public SavedState[] newArray(int size) {
1400 return new SavedState[size];
1401 }
1402 };
Aurimas Liutikas13fdea02016-02-11 10:10:10 -08001403 }
1404
1405 @Override
1406 protected Parcelable onSaveInstanceState() {
1407 Parcelable superState = super.onSaveInstanceState();
1408 SavedState ss = new SavedState(superState);
1409 ss.isIconified = isIconified();
1410 return ss;
1411 }
1412
1413 @Override
1414 protected void onRestoreInstanceState(Parcelable state) {
1415 SavedState ss = (SavedState) state;
1416 super.onRestoreInstanceState(ss.getSuperState());
1417 updateViewsVisibility(ss.isIconified);
1418 requestLayout();
1419 }
1420
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001421 @Override
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -08001422 public CharSequence getAccessibilityClassName() {
1423 return SearchView.class.getName();
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001424 }
1425
Amith Yamasanib4569fb2011-07-08 15:25:39 -07001426 private void adjustDropDownSizeAndPosition() {
1427 if (mDropDownAnchor.getWidth() > 1) {
1428 Resources res = getContext().getResources();
1429 int anchorPadding = mSearchPlate.getPaddingLeft();
1430 Rect dropDownPadding = new Rect();
Fabrice Di Meglio3a69bea2012-07-26 13:50:10 -07001431 final boolean isLayoutRtl = isLayoutRtl();
Amith Yamasanib4569fb2011-07-08 15:25:39 -07001432 int iconOffset = mIconifiedByDefault
1433 ? res.getDimensionPixelSize(R.dimen.dropdownitem_icon_width)
1434 + res.getDimensionPixelSize(R.dimen.dropdownitem_text_padding_left)
1435 : 0;
Alan Viverettecb8ed372014-11-18 17:05:35 -08001436 mSearchSrcTextView.getDropDownBackground().getPadding(dropDownPadding);
Fabrice Di Meglio3a69bea2012-07-26 13:50:10 -07001437 int offset;
1438 if (isLayoutRtl) {
1439 offset = - dropDownPadding.left;
1440 } else {
1441 offset = anchorPadding - (dropDownPadding.left + iconOffset);
1442 }
Alan Viverettecb8ed372014-11-18 17:05:35 -08001443 mSearchSrcTextView.setDropDownHorizontalOffset(offset);
Fabrice Di Meglio3a69bea2012-07-26 13:50:10 -07001444 final int width = mDropDownAnchor.getWidth() + dropDownPadding.left
1445 + dropDownPadding.right + iconOffset - anchorPadding;
Alan Viverettecb8ed372014-11-18 17:05:35 -08001446 mSearchSrcTextView.setDropDownWidth(width);
Amith Yamasanib4569fb2011-07-08 15:25:39 -07001447 }
1448 }
1449
Amith Yamasani968ec932010-12-02 14:00:47 -08001450 private boolean onItemClicked(int position, int actionKey, String actionMsg) {
1451 if (mOnSuggestionListener == null
Adam Powell01f21352011-01-20 18:30:10 -08001452 || !mOnSuggestionListener.onSuggestionClick(position)) {
Amith Yamasani968ec932010-12-02 14:00:47 -08001453 launchSuggestion(position, KeyEvent.KEYCODE_UNKNOWN, null);
Yohei Yukawa5cfc1b42017-03-07 00:52:09 -08001454 mSearchSrcTextView.setImeVisibility(false);
Amith Yamasani968ec932010-12-02 14:00:47 -08001455 dismissSuggestions();
1456 return true;
1457 }
1458 return false;
1459 }
1460
1461 private boolean onItemSelected(int position) {
1462 if (mOnSuggestionListener == null
Adam Powell01f21352011-01-20 18:30:10 -08001463 || !mOnSuggestionListener.onSuggestionSelect(position)) {
Amith Yamasani968ec932010-12-02 14:00:47 -08001464 rewriteQueryFromSuggestion(position);
1465 return true;
1466 }
1467 return false;
1468 }
1469
Amith Yamasani733cbd52010-09-03 12:21:39 -07001470 private final OnItemClickListener mOnItemClickListener = new OnItemClickListener() {
1471
1472 /**
1473 * Implements OnItemClickListener
1474 */
1475 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Amith Yamasani968ec932010-12-02 14:00:47 -08001476 if (DBG) Log.d(LOG_TAG, "onItemClick() position " + position);
1477 onItemClicked(position, KeyEvent.KEYCODE_UNKNOWN, null);
Amith Yamasani733cbd52010-09-03 12:21:39 -07001478 }
1479 };
1480
1481 private final OnItemSelectedListener mOnItemSelectedListener = new OnItemSelectedListener() {
1482
1483 /**
1484 * Implements OnItemSelectedListener
1485 */
1486 public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
Amith Yamasani968ec932010-12-02 14:00:47 -08001487 if (DBG) Log.d(LOG_TAG, "onItemSelected() position " + position);
1488 SearchView.this.onItemSelected(position);
Amith Yamasani733cbd52010-09-03 12:21:39 -07001489 }
1490
1491 /**
1492 * Implements OnItemSelectedListener
1493 */
1494 public void onNothingSelected(AdapterView<?> parent) {
1495 if (DBG)
1496 Log.d(LOG_TAG, "onNothingSelected()");
1497 }
1498 };
1499
1500 /**
1501 * Query rewriting.
1502 */
1503 private void rewriteQueryFromSuggestion(int position) {
Alan Viverettecb8ed372014-11-18 17:05:35 -08001504 CharSequence oldQuery = mSearchSrcTextView.getText();
Amith Yamasani733cbd52010-09-03 12:21:39 -07001505 Cursor c = mSuggestionsAdapter.getCursor();
1506 if (c == null) {
1507 return;
1508 }
1509 if (c.moveToPosition(position)) {
1510 // Get the new query from the suggestion.
1511 CharSequence newQuery = mSuggestionsAdapter.convertToString(c);
1512 if (newQuery != null) {
1513 // The suggestion rewrites the query.
1514 // Update the text field, without getting new suggestions.
1515 setQuery(newQuery);
1516 } else {
1517 // The suggestion does not rewrite the query, restore the user's query.
1518 setQuery(oldQuery);
1519 }
1520 } else {
1521 // We got a bad position, restore the user's query.
1522 setQuery(oldQuery);
1523 }
1524 }
1525
1526 /**
1527 * Launches an intent based on a suggestion.
1528 *
1529 * @param position The index of the suggestion to create the intent from.
1530 * @param actionKey The key code of the action key that was pressed,
1531 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
1532 * @param actionMsg The message for the action key that was pressed,
1533 * or <code>null</code> if none.
1534 * @return true if a successful launch, false if could not (e.g. bad position).
1535 */
1536 private boolean launchSuggestion(int position, int actionKey, String actionMsg) {
1537 Cursor c = mSuggestionsAdapter.getCursor();
1538 if ((c != null) && c.moveToPosition(position)) {
1539
1540 Intent intent = createIntentFromSuggestion(c, actionKey, actionMsg);
1541
1542 // launch the intent
1543 launchIntent(intent);
1544
1545 return true;
1546 }
1547 return false;
1548 }
1549
1550 /**
1551 * Launches an intent, including any special intent handling.
1552 */
1553 private void launchIntent(Intent intent) {
1554 if (intent == null) {
1555 return;
1556 }
1557 try {
1558 // If the intent was created from a suggestion, it will always have an explicit
1559 // component here.
1560 getContext().startActivity(intent);
1561 } catch (RuntimeException ex) {
1562 Log.e(LOG_TAG, "Failed launch activity: " + intent, ex);
1563 }
1564 }
1565
1566 /**
1567 * Sets the text in the query box, without updating the suggestions.
1568 */
1569 private void setQuery(CharSequence query) {
Alan Viverettecb8ed372014-11-18 17:05:35 -08001570 mSearchSrcTextView.setText(query, true);
Amith Yamasanie678f462010-09-15 16:13:43 -07001571 // Move the cursor to the end
Alan Viverettecb8ed372014-11-18 17:05:35 -08001572 mSearchSrcTextView.setSelection(TextUtils.isEmpty(query) ? 0 : query.length());
Amith Yamasani733cbd52010-09-03 12:21:39 -07001573 }
1574
1575 private void launchQuerySearch(int actionKey, String actionMsg, String query) {
1576 String action = Intent.ACTION_SEARCH;
Amith Yamasanie678f462010-09-15 16:13:43 -07001577 Intent intent = createIntent(action, null, null, query, actionKey, actionMsg);
Amith Yamasani733cbd52010-09-03 12:21:39 -07001578 getContext().startActivity(intent);
1579 }
1580
1581 /**
1582 * Constructs an intent from the given information and the search dialog state.
1583 *
1584 * @param action Intent action.
1585 * @param data Intent data, or <code>null</code>.
1586 * @param extraData Data for {@link SearchManager#EXTRA_DATA_KEY} or <code>null</code>.
1587 * @param query Intent query, or <code>null</code>.
Amith Yamasani733cbd52010-09-03 12:21:39 -07001588 * @param actionKey The key code of the action key that was pressed,
1589 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
1590 * @param actionMsg The message for the action key that was pressed,
1591 * or <code>null</code> if none.
1592 * @param mode The search mode, one of the acceptable values for
1593 * {@link SearchManager#SEARCH_MODE}, or {@code null}.
1594 * @return The intent.
1595 */
1596 private Intent createIntent(String action, Uri data, String extraData, String query,
Amith Yamasanie678f462010-09-15 16:13:43 -07001597 int actionKey, String actionMsg) {
Amith Yamasani733cbd52010-09-03 12:21:39 -07001598 // Now build the Intent
1599 Intent intent = new Intent(action);
1600 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
1601 // We need CLEAR_TOP to avoid reusing an old task that has other activities
1602 // on top of the one we want. We don't want to do this in in-app search though,
1603 // as it can be destructive to the activity stack.
1604 if (data != null) {
1605 intent.setData(data);
1606 }
Amith Yamasani068d73c2011-05-27 15:15:14 -07001607 intent.putExtra(SearchManager.USER_QUERY, mUserQuery);
Amith Yamasani733cbd52010-09-03 12:21:39 -07001608 if (query != null) {
1609 intent.putExtra(SearchManager.QUERY, query);
1610 }
1611 if (extraData != null) {
1612 intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData);
1613 }
Amith Yamasani940ef382011-03-02 18:43:23 -08001614 if (mAppSearchData != null) {
1615 intent.putExtra(SearchManager.APP_DATA, mAppSearchData);
1616 }
Amith Yamasani733cbd52010-09-03 12:21:39 -07001617 if (actionKey != KeyEvent.KEYCODE_UNKNOWN) {
1618 intent.putExtra(SearchManager.ACTION_KEY, actionKey);
1619 intent.putExtra(SearchManager.ACTION_MSG, actionMsg);
1620 }
1621 intent.setComponent(mSearchable.getSearchActivity());
1622 return intent;
1623 }
1624
1625 /**
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001626 * Create and return an Intent that can launch the voice search activity for web search.
1627 */
1628 private Intent createVoiceWebSearchIntent(Intent baseIntent, SearchableInfo searchable) {
1629 Intent voiceIntent = new Intent(baseIntent);
1630 ComponentName searchActivity = searchable.getSearchActivity();
1631 voiceIntent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, searchActivity == null ? null
1632 : searchActivity.flattenToShortString());
1633 return voiceIntent;
1634 }
1635
1636 /**
1637 * Create and return an Intent that can launch the voice search activity, perform a specific
1638 * voice transcription, and forward the results to the searchable activity.
1639 *
1640 * @param baseIntent The voice app search intent to start from
1641 * @return A completely-configured intent ready to send to the voice search activity
1642 */
1643 private Intent createVoiceAppSearchIntent(Intent baseIntent, SearchableInfo searchable) {
1644 ComponentName searchActivity = searchable.getSearchActivity();
1645
1646 // create the necessary intent to set up a search-and-forward operation
1647 // in the voice search system. We have to keep the bundle separate,
1648 // because it becomes immutable once it enters the PendingIntent
1649 Intent queryIntent = new Intent(Intent.ACTION_SEARCH);
1650 queryIntent.setComponent(searchActivity);
1651 PendingIntent pending = PendingIntent.getActivity(getContext(), 0, queryIntent,
1652 PendingIntent.FLAG_ONE_SHOT);
1653
1654 // Now set up the bundle that will be inserted into the pending intent
1655 // when it's time to do the search. We always build it here (even if empty)
1656 // because the voice search activity will always need to insert "QUERY" into
1657 // it anyway.
1658 Bundle queryExtras = new Bundle();
Jorge Ruesga1bcfe842012-09-03 01:26:59 +02001659 if (mAppSearchData != null) {
1660 queryExtras.putParcelable(SearchManager.APP_DATA, mAppSearchData);
1661 }
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001662
1663 // Now build the intent to launch the voice search. Add all necessary
1664 // extras to launch the voice recognizer, and then all the necessary extras
1665 // to forward the results to the searchable activity
1666 Intent voiceIntent = new Intent(baseIntent);
1667
1668 // Add all of the configuration options supplied by the searchable's metadata
1669 String languageModel = RecognizerIntent.LANGUAGE_MODEL_FREE_FORM;
1670 String prompt = null;
1671 String language = null;
1672 int maxResults = 1;
1673
1674 Resources resources = getResources();
1675 if (searchable.getVoiceLanguageModeId() != 0) {
1676 languageModel = resources.getString(searchable.getVoiceLanguageModeId());
1677 }
1678 if (searchable.getVoicePromptTextId() != 0) {
1679 prompt = resources.getString(searchable.getVoicePromptTextId());
1680 }
1681 if (searchable.getVoiceLanguageId() != 0) {
1682 language = resources.getString(searchable.getVoiceLanguageId());
1683 }
1684 if (searchable.getVoiceMaxResults() != 0) {
1685 maxResults = searchable.getVoiceMaxResults();
1686 }
1687 voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, languageModel);
1688 voiceIntent.putExtra(RecognizerIntent.EXTRA_PROMPT, prompt);
1689 voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, language);
1690 voiceIntent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, maxResults);
1691 voiceIntent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, searchActivity == null ? null
1692 : searchActivity.flattenToShortString());
1693
1694 // Add the values that configure forwarding the results
1695 voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT, pending);
1696 voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT_BUNDLE, queryExtras);
1697
1698 return voiceIntent;
1699 }
1700
1701 /**
Amith Yamasani733cbd52010-09-03 12:21:39 -07001702 * When a particular suggestion has been selected, perform the various lookups required
1703 * to use the suggestion. This includes checking the cursor for suggestion-specific data,
1704 * and/or falling back to the XML for defaults; It also creates REST style Uri data when
1705 * the suggestion includes a data id.
1706 *
1707 * @param c The suggestions cursor, moved to the row of the user's selection
1708 * @param actionKey The key code of the action key that was pressed,
1709 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
1710 * @param actionMsg The message for the action key that was pressed,
1711 * or <code>null</code> if none.
1712 * @return An intent for the suggestion at the cursor's position.
1713 */
1714 private Intent createIntentFromSuggestion(Cursor c, int actionKey, String actionMsg) {
1715 try {
1716 // use specific action if supplied, or default action if supplied, or fixed default
1717 String action = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_ACTION);
1718
Amith Yamasani733cbd52010-09-03 12:21:39 -07001719 if (action == null) {
1720 action = mSearchable.getSuggestIntentAction();
1721 }
1722 if (action == null) {
1723 action = Intent.ACTION_SEARCH;
1724 }
1725
1726 // use specific data if supplied, or default data if supplied
1727 String data = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA);
1728 if (data == null) {
1729 data = mSearchable.getSuggestIntentData();
1730 }
1731 // then, if an ID was provided, append it.
1732 if (data != null) {
1733 String id = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID);
1734 if (id != null) {
1735 data = data + "/" + Uri.encode(id);
1736 }
1737 }
1738 Uri dataUri = (data == null) ? null : Uri.parse(data);
1739
Amith Yamasani733cbd52010-09-03 12:21:39 -07001740 String query = getColumnString(c, SearchManager.SUGGEST_COLUMN_QUERY);
1741 String extraData = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA);
1742
Amith Yamasanie678f462010-09-15 16:13:43 -07001743 return createIntent(action, dataUri, extraData, query, actionKey, actionMsg);
Amith Yamasani733cbd52010-09-03 12:21:39 -07001744 } catch (RuntimeException e ) {
1745 int rowNum;
1746 try { // be really paranoid now
1747 rowNum = c.getPosition();
1748 } catch (RuntimeException e2 ) {
1749 rowNum = -1;
1750 }
Jake Wharton73af4512012-07-06 23:15:44 -07001751 Log.w(LOG_TAG, "Search suggestions cursor at row " + rowNum +
1752 " returned exception.", e);
Amith Yamasani733cbd52010-09-03 12:21:39 -07001753 return null;
1754 }
1755 }
1756
Amith Yamasanif28d1872011-07-26 12:21:03 -07001757 private void forceSuggestionQuery() {
Alan Viverettecb8ed372014-11-18 17:05:35 -08001758 mSearchSrcTextView.doBeforeTextChanged();
1759 mSearchSrcTextView.doAfterTextChanged();
Amith Yamasanif28d1872011-07-26 12:21:03 -07001760 }
1761
Amith Yamasani968ec932010-12-02 14:00:47 -08001762 static boolean isLandscapeMode(Context context) {
1763 return context.getResources().getConfiguration().orientation
1764 == Configuration.ORIENTATION_LANDSCAPE;
1765 }
1766
Amith Yamasani733cbd52010-09-03 12:21:39 -07001767 /**
1768 * Callback to watch the text field for empty/non-empty
1769 */
1770 private TextWatcher mTextWatcher = new TextWatcher() {
1771
1772 public void beforeTextChanged(CharSequence s, int start, int before, int after) { }
1773
1774 public void onTextChanged(CharSequence s, int start,
1775 int before, int after) {
1776 SearchView.this.onTextChanged(s);
1777 }
1778
1779 public void afterTextChanged(Editable s) {
1780 }
1781 };
Amith Yamasani968ec932010-12-02 14:00:47 -08001782
Aurimas Liutikasc8fd00a2016-04-15 13:55:53 -07001783 private static class UpdatableTouchDelegate extends TouchDelegate {
1784 /**
1785 * View that should receive forwarded touch events
1786 */
1787 private final View mDelegateView;
1788
1789 /**
1790 * Bounds in local coordinates of the containing view that should be mapped to the delegate
1791 * view. This rect is used for initial hit testing.
1792 */
1793 private final Rect mTargetBounds;
1794
1795 /**
1796 * Bounds in local coordinates of the containing view that are actual bounds of the delegate
1797 * view. This rect is used for event coordinate mapping.
1798 */
1799 private final Rect mActualBounds;
1800
1801 /**
1802 * mTargetBounds inflated to include some slop. This rect is to track whether the motion events
1803 * should be considered to be be within the delegate view.
1804 */
1805 private final Rect mSlopBounds;
1806
1807 private final int mSlop;
1808
1809 /**
1810 * True if the delegate had been targeted on a down event (intersected mTargetBounds).
1811 */
1812 private boolean mDelegateTargeted;
1813
1814 public UpdatableTouchDelegate(Rect targetBounds, Rect actualBounds, View delegateView) {
1815 super(targetBounds, delegateView);
1816 mSlop = ViewConfiguration.get(delegateView.getContext()).getScaledTouchSlop();
1817 mTargetBounds = new Rect();
1818 mSlopBounds = new Rect();
1819 mActualBounds = new Rect();
1820 setBounds(targetBounds, actualBounds);
1821 mDelegateView = delegateView;
1822 }
1823
1824 public void setBounds(Rect desiredBounds, Rect actualBounds) {
1825 mTargetBounds.set(desiredBounds);
1826 mSlopBounds.set(desiredBounds);
1827 mSlopBounds.inset(-mSlop, -mSlop);
1828 mActualBounds.set(actualBounds);
1829 }
1830
1831 @Override
1832 public boolean onTouchEvent(MotionEvent event) {
1833 final int x = (int) event.getX();
1834 final int y = (int) event.getY();
1835 boolean sendToDelegate = false;
1836 boolean hit = true;
1837 boolean handled = false;
1838
1839 switch (event.getAction()) {
1840 case MotionEvent.ACTION_DOWN:
1841 if (mTargetBounds.contains(x, y)) {
1842 mDelegateTargeted = true;
1843 sendToDelegate = true;
1844 }
1845 break;
1846 case MotionEvent.ACTION_UP:
1847 case MotionEvent.ACTION_MOVE:
1848 sendToDelegate = mDelegateTargeted;
1849 if (sendToDelegate) {
1850 if (!mSlopBounds.contains(x, y)) {
1851 hit = false;
1852 }
1853 }
1854 break;
1855 case MotionEvent.ACTION_CANCEL:
1856 sendToDelegate = mDelegateTargeted;
1857 mDelegateTargeted = false;
1858 break;
1859 }
1860 if (sendToDelegate) {
1861 if (hit && !mActualBounds.contains(x, y)) {
1862 // Offset event coordinates to be in the center of the target view since we
1863 // are within the targetBounds, but not inside the actual bounds of
1864 // mDelegateView
1865 event.setLocation(mDelegateView.getWidth() / 2,
1866 mDelegateView.getHeight() / 2);
1867 } else {
1868 // Offset event coordinates to the target view coordinates.
1869 event.setLocation(x - mActualBounds.left, y - mActualBounds.top);
1870 }
1871
1872 handled = mDelegateView.dispatchTouchEvent(event);
1873 }
1874 return handled;
1875 }
1876 }
1877
Amith Yamasani968ec932010-12-02 14:00:47 -08001878 /**
1879 * Local subclass for AutoCompleteTextView.
1880 * @hide
1881 */
1882 public static class SearchAutoComplete extends AutoCompleteTextView {
1883
1884 private int mThreshold;
1885 private SearchView mSearchView;
1886
Yohei Yukawa5cfc1b42017-03-07 00:52:09 -08001887 private boolean mHasPendingShowSoftInputRequest;
1888 final Runnable mRunShowSoftInputIfNecessary = () -> showSoftInputIfNecessary();
1889
Amith Yamasani968ec932010-12-02 14:00:47 -08001890 public SearchAutoComplete(Context context) {
1891 super(context);
1892 mThreshold = getThreshold();
1893 }
1894
1895 public SearchAutoComplete(Context context, AttributeSet attrs) {
1896 super(context, attrs);
1897 mThreshold = getThreshold();
1898 }
1899
Alan Viverette617feb92013-09-09 18:09:13 -07001900 public SearchAutoComplete(Context context, AttributeSet attrs, int defStyleAttrs) {
1901 super(context, attrs, defStyleAttrs);
1902 mThreshold = getThreshold();
1903 }
1904
1905 public SearchAutoComplete(
1906 Context context, AttributeSet attrs, int defStyleAttrs, int defStyleRes) {
1907 super(context, attrs, defStyleAttrs, defStyleRes);
Amith Yamasani968ec932010-12-02 14:00:47 -08001908 mThreshold = getThreshold();
1909 }
1910
Filip Gruszczynskib635fda2015-12-03 18:37:38 -08001911 @Override
1912 protected void onFinishInflate() {
1913 super.onFinishInflate();
1914 DisplayMetrics metrics = getResources().getDisplayMetrics();
1915 setMinWidth((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
1916 getSearchViewTextMinWidthDp(), metrics));
1917 }
1918
Amith Yamasani968ec932010-12-02 14:00:47 -08001919 void setSearchView(SearchView searchView) {
1920 mSearchView = searchView;
1921 }
1922
1923 @Override
1924 public void setThreshold(int threshold) {
1925 super.setThreshold(threshold);
1926 mThreshold = threshold;
1927 }
1928
1929 /**
1930 * Returns true if the text field is empty, or contains only whitespace.
1931 */
1932 private boolean isEmpty() {
1933 return TextUtils.getTrimmedLength(getText()) == 0;
1934 }
1935
1936 /**
1937 * We override this method to avoid replacing the query box text when a
1938 * suggestion is clicked.
1939 */
1940 @Override
1941 protected void replaceText(CharSequence text) {
1942 }
1943
1944 /**
1945 * We override this method to avoid an extra onItemClick being called on
1946 * the drop-down's OnItemClickListener by
1947 * {@link AutoCompleteTextView#onKeyUp(int, KeyEvent)} when an item is
1948 * clicked with the trackball.
1949 */
1950 @Override
1951 public void performCompletion() {
1952 }
1953
1954 /**
1955 * We override this method to be sure and show the soft keyboard if
1956 * appropriate when the TextView has focus.
1957 */
1958 @Override
1959 public void onWindowFocusChanged(boolean hasWindowFocus) {
1960 super.onWindowFocusChanged(hasWindowFocus);
1961
Amith Yamasaniacd8d2d2010-12-06 15:50:23 -08001962 if (hasWindowFocus && mSearchView.hasFocus() && getVisibility() == VISIBLE) {
Yohei Yukawa5cfc1b42017-03-07 00:52:09 -08001963 // Since InputMethodManager#onPostWindowFocus() will be called after this callback,
1964 // it is a bit too early to call InputMethodManager#showSoftInput() here. We still
1965 // need to wait until the system calls back onCreateInputConnection() to call
1966 // InputMethodManager#showSoftInput().
1967 mHasPendingShowSoftInputRequest = true;
1968
1969 // If in landscape mode, then make sure that the ime is in front of the dropdown.
Amith Yamasani968ec932010-12-02 14:00:47 -08001970 if (isLandscapeMode(getContext())) {
1971 ensureImeVisible(true);
1972 }
1973 }
1974 }
1975
Amith Yamasani4aedb392010-12-15 16:04:57 -08001976 @Override
1977 protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
1978 super.onFocusChanged(focused, direction, previouslyFocusedRect);
1979 mSearchView.onTextFocusChanged();
1980 }
1981
Amith Yamasani968ec932010-12-02 14:00:47 -08001982 /**
1983 * We override this method so that we can allow a threshold of zero,
1984 * which ACTV does not.
1985 */
1986 @Override
1987 public boolean enoughToFilter() {
1988 return mThreshold <= 0 || super.enoughToFilter();
1989 }
Amith Yamasanib4569fb2011-07-08 15:25:39 -07001990
1991 @Override
1992 public boolean onKeyPreIme(int keyCode, KeyEvent event) {
1993 if (keyCode == KeyEvent.KEYCODE_BACK) {
1994 // special case for the back key, we do not even try to send it
1995 // to the drop down list but instead, consume it immediately
1996 if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
1997 KeyEvent.DispatcherState state = getKeyDispatcherState();
1998 if (state != null) {
1999 state.startTracking(event, this);
2000 }
2001 return true;
2002 } else if (event.getAction() == KeyEvent.ACTION_UP) {
2003 KeyEvent.DispatcherState state = getKeyDispatcherState();
2004 if (state != null) {
2005 state.handleUpEvent(event);
2006 }
2007 if (event.isTracking() && !event.isCanceled()) {
2008 mSearchView.clearFocus();
Yohei Yukawa5cfc1b42017-03-07 00:52:09 -08002009 setImeVisibility(false);
Amith Yamasanib4569fb2011-07-08 15:25:39 -07002010 return true;
2011 }
2012 }
2013 }
2014 return super.onKeyPreIme(keyCode, event);
2015 }
2016
Filip Gruszczynskib635fda2015-12-03 18:37:38 -08002017 /**
2018 * Get minimum width of the search view text entry area.
2019 */
2020 private int getSearchViewTextMinWidthDp() {
2021 final Configuration configuration = getResources().getConfiguration();
2022 final int width = configuration.screenWidthDp;
2023 final int height = configuration.screenHeightDp;
2024 final int orientation = configuration.orientation;
2025 if (width >= 960 && height >= 720
2026 && orientation == Configuration.ORIENTATION_LANDSCAPE) {
2027 return 256;
2028 } else if (width >= 600 || (width >= 640 && height >= 480)) {
2029 return 192;
2030 };
2031 return 160;
2032 }
Yohei Yukawa5cfc1b42017-03-07 00:52:09 -08002033
2034 /**
2035 * We override {@link View#onCreateInputConnection(EditorInfo)} as a signal to schedule a
2036 * pending {@link InputMethodManager#showSoftInput(View, int)} request (if any).
2037 */
2038 @Override
2039 public InputConnection onCreateInputConnection(EditorInfo editorInfo) {
2040 final InputConnection ic = super.onCreateInputConnection(editorInfo);
2041 if (mHasPendingShowSoftInputRequest) {
2042 removeCallbacks(mRunShowSoftInputIfNecessary);
2043 post(mRunShowSoftInputIfNecessary);
2044 }
2045 return ic;
2046 }
2047
2048 private void showSoftInputIfNecessary() {
2049 if (mHasPendingShowSoftInputRequest) {
2050 final InputMethodManager imm =
2051 getContext().getSystemService(InputMethodManager.class);
2052 imm.showSoftInput(this, 0);
2053 mHasPendingShowSoftInputRequest = false;
2054 }
2055 }
2056
2057 private void setImeVisibility(final boolean visible) {
2058 final InputMethodManager imm = getContext().getSystemService(InputMethodManager.class);
2059 if (!visible) {
2060 mHasPendingShowSoftInputRequest = false;
2061 removeCallbacks(mRunShowSoftInputIfNecessary);
2062 imm.hideSoftInputFromWindow(getWindowToken(), 0);
2063 return;
2064 }
2065
2066 if (imm.isActive(this)) {
2067 // This means that SearchAutoComplete is already connected to the IME.
2068 // InputMethodManager#showSoftInput() is guaranteed to pass client-side focus check.
2069 mHasPendingShowSoftInputRequest = false;
2070 removeCallbacks(mRunShowSoftInputIfNecessary);
2071 imm.showSoftInput(this, 0);
2072 return;
2073 }
2074
2075 // Otherwise, InputMethodManager#showSoftInput() should be deferred after
2076 // onCreateInputConnection().
2077 mHasPendingShowSoftInputRequest = true;
2078 }
Amith Yamasani968ec932010-12-02 14:00:47 -08002079 }
Amith Yamasani05944762010-10-08 13:52:38 -07002080}