blob: 9a4d69fd4e9ae46a6832e071c75c020d59f51d1c [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;
56import android.view.View;
Amith Yamasani5607a382011-08-09 14:16:37 -070057import android.view.inputmethod.EditorInfo;
Amith Yamasani733cbd52010-09-03 12:21:39 -070058import android.view.inputmethod.InputMethodManager;
59import android.widget.AdapterView.OnItemClickListener;
60import android.widget.AdapterView.OnItemSelectedListener;
61import android.widget.TextView.OnEditorActionListener;
62
Amith Yamasanib4569fb2011-07-08 15:25:39 -070063import com.android.internal.R;
64
Amith Yamasani733cbd52010-09-03 12:21:39 -070065import java.util.WeakHashMap;
66
67/**
Amith Yamasani763bc072011-07-22 11:53:47 -070068 * A widget that provides a user interface for the user to enter a search query and submit a request
69 * to a search provider. Shows a list of query suggestions or results, if available, and allows the
70 * user to pick a suggestion or result to launch into.
Amith Yamasani5931b1f2010-10-18 16:13:14 -070071 *
Amith Yamasani763bc072011-07-22 11:53:47 -070072 * <p>
73 * When the SearchView is used in an ActionBar as an action view for a collapsible menu item, it
74 * needs to be set to iconified by default using {@link #setIconifiedByDefault(boolean)
75 * setIconifiedByDefault(true)}. This is the default, so nothing needs to be done.
76 * </p>
77 * <p>
78 * If you want the search field to always be visible, then call setIconifiedByDefault(false).
79 * </p>
Amith Yamasani5931b1f2010-10-18 16:13:14 -070080 *
Joe Fernandez3aef8e1d2011-12-20 10:38:34 -080081 * <div class="special reference">
82 * <h3>Developer Guides</h3>
83 * <p>For information about using {@code SearchView}, read the
84 * <a href="{@docRoot}guide/topics/search/index.html">Search</a> developer guide.</p>
85 * </div>
Amith Yamasani763bc072011-07-22 11:53:47 -070086 *
87 * @see android.view.MenuItem#SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW
Amith Yamasani5931b1f2010-10-18 16:13:14 -070088 * @attr ref android.R.styleable#SearchView_iconifiedByDefault
Amith Yamasani5607a382011-08-09 14:16:37 -070089 * @attr ref android.R.styleable#SearchView_imeOptions
90 * @attr ref android.R.styleable#SearchView_inputType
Amith Yamasani5931b1f2010-10-18 16:13:14 -070091 * @attr ref android.R.styleable#SearchView_maxWidth
Scott Mainabdf0d52011-02-08 10:20:27 -080092 * @attr ref android.R.styleable#SearchView_queryHint
Amith Yamasani733cbd52010-09-03 12:21:39 -070093 */
Amith Yamasani763bc072011-07-22 11:53:47 -070094public class SearchView extends LinearLayout implements CollapsibleActionView {
Amith Yamasani733cbd52010-09-03 12:21:39 -070095
96 private static final boolean DBG = false;
97 private static final String LOG_TAG = "SearchView";
98
Luca Zanolin535698c2011-10-06 13:36:15 +010099 /**
100 * Private constant for removing the microphone in the keyboard.
101 */
102 private static final String IME_OPTION_NO_MICROPHONE = "nm";
103
Alan Viverettecb8ed372014-11-18 17:05:35 -0800104 private final SearchAutoComplete mSearchSrcTextView;
Alan Viverette5dddb702014-07-02 15:46:04 -0700105 private final View mSearchEditFrame;
106 private final View mSearchPlate;
107 private final View mSubmitArea;
108 private final ImageView mSearchButton;
Alan Viverettecb8ed372014-11-18 17:05:35 -0800109 private final ImageView mGoButton;
Alan Viverette5dddb702014-07-02 15:46:04 -0700110 private final ImageView mCloseButton;
111 private final ImageView mVoiceButton;
Alan Viverette5dddb702014-07-02 15:46:04 -0700112 private final View mDropDownAnchor;
Alan Viverettecb8ed372014-11-18 17:05:35 -0800113
114 /** Icon optionally displayed when the SearchView is collapsed. */
115 private final ImageView mCollapsedIcon;
116
117 /** Drawable used as an EditText hint. */
118 private final Drawable mSearchHintIcon;
Alan Viverette5dddb702014-07-02 15:46:04 -0700119
120 // Resources used by SuggestionsAdapter to display suggestions.
121 private final int mSuggestionRowLayout;
122 private final int mSuggestionCommitIconResId;
123
124 // Intents used for voice searching.
125 private final Intent mVoiceWebSearchIntent;
126 private final Intent mVoiceAppSearchIntent;
127
Alan Viveretteb4004df2015-04-29 16:55:42 -0700128 private final CharSequence mDefaultQueryHint;
129
Adam Powell01f21352011-01-20 18:30:10 -0800130 private OnQueryTextListener mOnQueryChangeListener;
Amith Yamasani733cbd52010-09-03 12:21:39 -0700131 private OnCloseListener mOnCloseListener;
Amith Yamasani05944762010-10-08 13:52:38 -0700132 private OnFocusChangeListener mOnQueryTextFocusChangeListener;
Adam Powell01f21352011-01-20 18:30:10 -0800133 private OnSuggestionListener mOnSuggestionListener;
Amith Yamasani48385482010-12-03 14:43:52 -0800134 private OnClickListener mOnSearchClickListener;
Amith Yamasani733cbd52010-09-03 12:21:39 -0700135
136 private boolean mIconifiedByDefault;
Amith Yamasani93227752010-09-14 10:10:54 -0700137 private boolean mIconified;
Amith Yamasani733cbd52010-09-03 12:21:39 -0700138 private CursorAdapter mSuggestionsAdapter;
Amith Yamasani733cbd52010-09-03 12:21:39 -0700139 private boolean mSubmitButtonEnabled;
140 private CharSequence mQueryHint;
Amith Yamasanie678f462010-09-15 16:13:43 -0700141 private boolean mQueryRefinement;
Amith Yamasani05944762010-10-08 13:52:38 -0700142 private boolean mClearingFocus;
Amith Yamasani5931b1f2010-10-18 16:13:14 -0700143 private int mMaxWidth;
Amith Yamasani9b2e3022011-01-14 11:34:12 -0800144 private boolean mVoiceButtonEnabled;
Amith Yamasanib47c4fd2011-08-04 14:30:07 -0700145 private CharSequence mOldQueryText;
Amith Yamasani068d73c2011-05-27 15:15:14 -0700146 private CharSequence mUserQuery;
Amith Yamasani763bc072011-07-22 11:53:47 -0700147 private boolean mExpandedInActionView;
Adam Powell53f56c42011-09-25 13:46:15 -0700148 private int mCollapsedImeOptions;
Amith Yamasani733cbd52010-09-03 12:21:39 -0700149
150 private SearchableInfo mSearchable;
Amith Yamasani940ef382011-03-02 18:43:23 -0800151 private Bundle mAppSearchData;
Amith Yamasani733cbd52010-09-03 12:21:39 -0700152
Adam Powellccdd4ee2011-07-27 20:05:14 -0700153 /*
154 * SearchView can be set expanded before the IME is ready to be shown during
155 * initial UI setup. The show operation is asynchronous to account for this.
156 */
157 private Runnable mShowImeRunnable = new Runnable() {
158 public void run() {
Yohei Yukawa777ef952015-11-25 20:32:24 -0800159 InputMethodManager imm = getContext().getSystemService(InputMethodManager.class);
Adam Powellccdd4ee2011-07-27 20:05:14 -0700160
161 if (imm != null) {
162 imm.showSoftInputUnchecked(0, null);
163 }
164 }
165 };
166
Amith Yamasania95e4882011-08-17 11:41:37 -0700167 private Runnable mUpdateDrawableStateRunnable = new Runnable() {
168 public void run() {
169 updateFocusedState();
170 }
171 };
172
Amith Yamasani87907642011-11-03 11:32:44 -0700173 private Runnable mReleaseCursorRunnable = new Runnable() {
174 public void run() {
175 if (mSuggestionsAdapter != null && mSuggestionsAdapter instanceof SuggestionsAdapter) {
176 mSuggestionsAdapter.changeCursor(null);
177 }
178 }
179 };
180
Amith Yamasani733cbd52010-09-03 12:21:39 -0700181 // A weak map of drawables we've gotten from other packages, so we don't load them
182 // more than once.
183 private final WeakHashMap<String, Drawable.ConstantState> mOutsideDrawablesCache =
184 new WeakHashMap<String, Drawable.ConstantState>();
185
186 /**
187 * Callbacks for changes to the query text.
188 */
Adam Powell01f21352011-01-20 18:30:10 -0800189 public interface OnQueryTextListener {
Amith Yamasani733cbd52010-09-03 12:21:39 -0700190
191 /**
192 * Called when the user submits the query. This could be due to a key press on the
193 * keyboard or due to pressing a submit button.
194 * The listener can override the standard behavior by returning true
195 * to indicate that it has handled the submit request. Otherwise return false to
196 * let the SearchView handle the submission by launching any associated intent.
197 *
198 * @param query the query text that is to be submitted
199 *
200 * @return true if the query has been handled by the listener, false to let the
201 * SearchView perform the default action.
202 */
Adam Powell01f21352011-01-20 18:30:10 -0800203 boolean onQueryTextSubmit(String query);
Amith Yamasani733cbd52010-09-03 12:21:39 -0700204
205 /**
206 * Called when the query text is changed by the user.
207 *
208 * @param newText the new content of the query text field.
209 *
210 * @return false if the SearchView should perform the default action of showing any
211 * suggestions if available, true if the action was handled by the listener.
212 */
Adam Powell01f21352011-01-20 18:30:10 -0800213 boolean onQueryTextChange(String newText);
Amith Yamasani733cbd52010-09-03 12:21:39 -0700214 }
215
216 public interface OnCloseListener {
217
218 /**
219 * The user is attempting to close the SearchView.
220 *
221 * @return true if the listener wants to override the default behavior of clearing the
222 * text field and dismissing it, false otherwise.
223 */
224 boolean onClose();
225 }
226
Amith Yamasani05944762010-10-08 13:52:38 -0700227 /**
228 * Callback interface for selection events on suggestions. These callbacks
229 * are only relevant when a SearchableInfo has been specified by {@link #setSearchableInfo}.
230 */
Adam Powell01f21352011-01-20 18:30:10 -0800231 public interface OnSuggestionListener {
Amith Yamasani05944762010-10-08 13:52:38 -0700232
233 /**
234 * Called when a suggestion was selected by navigating to it.
235 * @param position the absolute position in the list of suggestions.
236 *
237 * @return true if the listener handles the event and wants to override the default
238 * behavior of possibly rewriting the query based on the selected item, false otherwise.
239 */
Adam Powell01f21352011-01-20 18:30:10 -0800240 boolean onSuggestionSelect(int position);
Amith Yamasani05944762010-10-08 13:52:38 -0700241
242 /**
243 * Called when a suggestion was clicked.
244 * @param position the absolute position of the clicked item in the list of suggestions.
245 *
246 * @return true if the listener handles the event and wants to override the default
247 * behavior of launching any intent or submitting a search query specified on that item.
248 * Return false otherwise.
249 */
Adam Powell01f21352011-01-20 18:30:10 -0800250 boolean onSuggestionClick(int position);
Amith Yamasani05944762010-10-08 13:52:38 -0700251 }
252
Amith Yamasani733cbd52010-09-03 12:21:39 -0700253 public SearchView(Context context) {
254 this(context, null);
255 }
256
257 public SearchView(Context context, AttributeSet attrs) {
Alan Viverette5dddb702014-07-02 15:46:04 -0700258 this(context, attrs, R.attr.searchViewStyle);
Alan Viverette617feb92013-09-09 18:09:13 -0700259 }
260
261 public SearchView(Context context, AttributeSet attrs, int defStyleAttr) {
262 this(context, attrs, defStyleAttr, 0);
263 }
264
265 public SearchView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
266 super(context, attrs, defStyleAttr, defStyleRes);
Amith Yamasani733cbd52010-09-03 12:21:39 -0700267
Alan Viverette5dddb702014-07-02 15:46:04 -0700268 final TypedArray a = context.obtainStyledAttributes(
269 attrs, R.styleable.SearchView, defStyleAttr, defStyleRes);
270 final LayoutInflater inflater = (LayoutInflater) context.getSystemService(
271 Context.LAYOUT_INFLATER_SERVICE);
Alan Viverettecb8ed372014-11-18 17:05:35 -0800272 final int layoutResId = a.getResourceId(
273 R.styleable.SearchView_layout, R.layout.search_view);
Alan Viverette5dddb702014-07-02 15:46:04 -0700274 inflater.inflate(layoutResId, this, true);
Amith Yamasani733cbd52010-09-03 12:21:39 -0700275
Alan Viverettecb8ed372014-11-18 17:05:35 -0800276 mSearchSrcTextView = (SearchAutoComplete) findViewById(R.id.search_src_text);
277 mSearchSrcTextView.setSearchView(this);
Amith Yamasani968ec932010-12-02 14:00:47 -0800278
Amith Yamasani733cbd52010-09-03 12:21:39 -0700279 mSearchEditFrame = findViewById(R.id.search_edit_frame);
Amith Yamasani79f74302011-03-08 14:16:35 -0800280 mSearchPlate = findViewById(R.id.search_plate);
Amith Yamasani9b2e3022011-01-14 11:34:12 -0800281 mSubmitArea = findViewById(R.id.submit_area);
Alan Viverette5dddb702014-07-02 15:46:04 -0700282 mSearchButton = (ImageView) findViewById(R.id.search_button);
Alan Viverettecb8ed372014-11-18 17:05:35 -0800283 mGoButton = (ImageView) findViewById(R.id.search_go_btn);
Amith Yamasani4aedb392010-12-15 16:04:57 -0800284 mCloseButton = (ImageView) findViewById(R.id.search_close_btn);
Alan Viverette5dddb702014-07-02 15:46:04 -0700285 mVoiceButton = (ImageView) findViewById(R.id.search_voice_btn);
Alan Viverettecb8ed372014-11-18 17:05:35 -0800286 mCollapsedIcon = (ImageView) findViewById(R.id.search_mag_icon);
Amith Yamasani733cbd52010-09-03 12:21:39 -0700287
Alan Viverette5dddb702014-07-02 15:46:04 -0700288 // Set up icons and backgrounds.
289 mSearchPlate.setBackground(a.getDrawable(R.styleable.SearchView_queryBackground));
290 mSubmitArea.setBackground(a.getDrawable(R.styleable.SearchView_submitBackground));
Alan Viverettecb8ed372014-11-18 17:05:35 -0800291 mSearchButton.setImageDrawable(a.getDrawable(R.styleable.SearchView_searchIcon));
292 mGoButton.setImageDrawable(a.getDrawable(R.styleable.SearchView_goIcon));
Alan Viverette5dddb702014-07-02 15:46:04 -0700293 mCloseButton.setImageDrawable(a.getDrawable(R.styleable.SearchView_closeIcon));
294 mVoiceButton.setImageDrawable(a.getDrawable(R.styleable.SearchView_voiceIcon));
Alan Viverettecb8ed372014-11-18 17:05:35 -0800295 mCollapsedIcon.setImageDrawable(a.getDrawable(R.styleable.SearchView_searchIcon));
296
297 // Prior to L MR1, the search hint icon defaulted to searchIcon. If the
298 // style does not have an explicit value set, fall back to that.
299 if (a.hasValueOrEmpty(R.styleable.SearchView_searchHintIcon)) {
300 mSearchHintIcon = a.getDrawable(R.styleable.SearchView_searchHintIcon);
301 } else {
302 mSearchHintIcon = a.getDrawable(R.styleable.SearchView_searchIcon);
303 }
Alan Viverette5dddb702014-07-02 15:46:04 -0700304
305 // Extract dropdown layout resource IDs for later use.
Alan Viverette362f9842014-09-10 16:05:35 -0700306 mSuggestionRowLayout = a.getResourceId(R.styleable.SearchView_suggestionRowLayout,
307 R.layout.search_dropdown_item_icons_2line);
Alan Viverette5dddb702014-07-02 15:46:04 -0700308 mSuggestionCommitIconResId = a.getResourceId(R.styleable.SearchView_commitIcon, 0);
309
Amith Yamasani733cbd52010-09-03 12:21:39 -0700310 mSearchButton.setOnClickListener(mOnClickListener);
311 mCloseButton.setOnClickListener(mOnClickListener);
Alan Viverettecb8ed372014-11-18 17:05:35 -0800312 mGoButton.setOnClickListener(mOnClickListener);
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -0700313 mVoiceButton.setOnClickListener(mOnClickListener);
Alan Viverettecb8ed372014-11-18 17:05:35 -0800314 mSearchSrcTextView.setOnClickListener(mOnClickListener);
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -0700315
Alan Viverettecb8ed372014-11-18 17:05:35 -0800316 mSearchSrcTextView.addTextChangedListener(mTextWatcher);
317 mSearchSrcTextView.setOnEditorActionListener(mOnEditorActionListener);
318 mSearchSrcTextView.setOnItemClickListener(mOnItemClickListener);
319 mSearchSrcTextView.setOnItemSelectedListener(mOnItemSelectedListener);
320 mSearchSrcTextView.setOnKeyListener(mTextKeyListener);
Alan Viverette5dddb702014-07-02 15:46:04 -0700321
Luca Zanolin535698c2011-10-06 13:36:15 +0100322 // Inform any listener of focus changes
Alan Viverettecb8ed372014-11-18 17:05:35 -0800323 mSearchSrcTextView.setOnFocusChangeListener(new OnFocusChangeListener() {
Amith Yamasani05944762010-10-08 13:52:38 -0700324
325 public void onFocusChange(View v, boolean hasFocus) {
326 if (mOnQueryTextFocusChangeListener != null) {
327 mOnQueryTextFocusChangeListener.onFocusChange(SearchView.this, hasFocus);
328 }
329 }
330 });
Amith Yamasani733cbd52010-09-03 12:21:39 -0700331 setIconifiedByDefault(a.getBoolean(R.styleable.SearchView_iconifiedByDefault, true));
Alan Viverette5dddb702014-07-02 15:46:04 -0700332
333 final int maxWidth = a.getDimensionPixelSize(R.styleable.SearchView_maxWidth, -1);
Amith Yamasani5931b1f2010-10-18 16:13:14 -0700334 if (maxWidth != -1) {
335 setMaxWidth(maxWidth);
336 }
Alan Viverette5dddb702014-07-02 15:46:04 -0700337
Alan Viveretteb4004df2015-04-29 16:55:42 -0700338 mDefaultQueryHint = a.getText(R.styleable.SearchView_defaultQueryHint);
339 mQueryHint = a.getText(R.styleable.SearchView_queryHint);
Alan Viverette5dddb702014-07-02 15:46:04 -0700340
341 final int imeOptions = a.getInt(R.styleable.SearchView_imeOptions, -1);
Amith Yamasani5607a382011-08-09 14:16:37 -0700342 if (imeOptions != -1) {
343 setImeOptions(imeOptions);
344 }
Alan Viverette5dddb702014-07-02 15:46:04 -0700345
346 final int inputType = a.getInt(R.styleable.SearchView_inputType, -1);
Amith Yamasani5607a382011-08-09 14:16:37 -0700347 if (inputType != -1) {
348 setInputType(inputType);
349 }
350
Adam Powellea4ecd62014-09-03 19:35:37 -0700351 boolean focusable = true;
352 focusable = a.getBoolean(R.styleable.SearchView_focusable, focusable);
353 setFocusable(focusable);
354
Amith Yamasani733cbd52010-09-03 12:21:39 -0700355 a.recycle();
356
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -0700357 // Save voice intent for later queries/launching
358 mVoiceWebSearchIntent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH);
359 mVoiceWebSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
360 mVoiceWebSearchIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
361 RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH);
362
363 mVoiceAppSearchIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
364 mVoiceAppSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
365
Alan Viverettecb8ed372014-11-18 17:05:35 -0800366 mDropDownAnchor = findViewById(mSearchSrcTextView.getDropDownAnchor());
Amith Yamasanib4569fb2011-07-08 15:25:39 -0700367 if (mDropDownAnchor != null) {
368 mDropDownAnchor.addOnLayoutChangeListener(new OnLayoutChangeListener() {
369 @Override
370 public void onLayoutChange(View v, int left, int top, int right, int bottom,
371 int oldLeft, int oldTop, int oldRight, int oldBottom) {
372 adjustDropDownSizeAndPosition();
373 }
Amith Yamasanib4569fb2011-07-08 15:25:39 -0700374 });
375 }
376
Amith Yamasani733cbd52010-09-03 12:21:39 -0700377 updateViewsVisibility(mIconifiedByDefault);
Amith Yamasanib4569fb2011-07-08 15:25:39 -0700378 updateQueryHint();
Amith Yamasani733cbd52010-09-03 12:21:39 -0700379 }
380
Alan Viverette5dddb702014-07-02 15:46:04 -0700381 int getSuggestionRowLayout() {
382 return mSuggestionRowLayout;
383 }
384
385 int getSuggestionCommitIconResId() {
386 return mSuggestionCommitIconResId;
387 }
388
Amith Yamasani733cbd52010-09-03 12:21:39 -0700389 /**
390 * Sets the SearchableInfo for this SearchView. Properties in the SearchableInfo are used
391 * to display labels, hints, suggestions, create intents for launching search results screens
392 * and controlling other affordances such as a voice button.
393 *
394 * @param searchable a SearchableInfo can be retrieved from the SearchManager, for a specific
395 * activity or a global search provider.
396 */
397 public void setSearchableInfo(SearchableInfo searchable) {
398 mSearchable = searchable;
399 if (mSearchable != null) {
400 updateSearchAutoComplete();
Amith Yamasani79f74302011-03-08 14:16:35 -0800401 updateQueryHint();
Amith Yamasani733cbd52010-09-03 12:21:39 -0700402 }
Amith Yamasani9b2e3022011-01-14 11:34:12 -0800403 // Cache the voice search capability
404 mVoiceButtonEnabled = hasVoiceSearch();
Luca Zanolin535698c2011-10-06 13:36:15 +0100405
406 if (mVoiceButtonEnabled) {
407 // Disable the microphone on the keyboard, as a mic is displayed near the text box
408 // TODO: use imeOptions to disable voice input when the new API will be available
Alan Viverettecb8ed372014-11-18 17:05:35 -0800409 mSearchSrcTextView.setPrivateImeOptions(IME_OPTION_NO_MICROPHONE);
Luca Zanolin535698c2011-10-06 13:36:15 +0100410 }
Amith Yamasanib4569fb2011-07-08 15:25:39 -0700411 updateViewsVisibility(isIconified());
Amith Yamasani733cbd52010-09-03 12:21:39 -0700412 }
413
Amith Yamasani940ef382011-03-02 18:43:23 -0800414 /**
415 * Sets the APP_DATA for legacy SearchDialog use.
416 * @param appSearchData bundle provided by the app when launching the search dialog
417 * @hide
418 */
419 public void setAppSearchData(Bundle appSearchData) {
420 mAppSearchData = appSearchData;
421 }
422
Amith Yamasani5607a382011-08-09 14:16:37 -0700423 /**
424 * Sets the IME options on the query text field.
425 *
426 * @see TextView#setImeOptions(int)
427 * @param imeOptions the options to set on the query text field
428 *
429 * @attr ref android.R.styleable#SearchView_imeOptions
430 */
431 public void setImeOptions(int imeOptions) {
Alan Viverettecb8ed372014-11-18 17:05:35 -0800432 mSearchSrcTextView.setImeOptions(imeOptions);
Amith Yamasani5607a382011-08-09 14:16:37 -0700433 }
434
435 /**
Amith Yamasanieca59d32012-04-25 18:57:18 -0700436 * Returns the IME options set on the query text field.
437 * @return the ime options
438 * @see TextView#setImeOptions(int)
439 *
440 * @attr ref android.R.styleable#SearchView_imeOptions
441 */
442 public int getImeOptions() {
Alan Viverettecb8ed372014-11-18 17:05:35 -0800443 return mSearchSrcTextView.getImeOptions();
Amith Yamasanieca59d32012-04-25 18:57:18 -0700444 }
445
446 /**
Amith Yamasani5607a382011-08-09 14:16:37 -0700447 * Sets the input type on the query text field.
448 *
449 * @see TextView#setInputType(int)
450 * @param inputType the input type to set on the query text field
451 *
452 * @attr ref android.R.styleable#SearchView_inputType
453 */
454 public void setInputType(int inputType) {
Alan Viverettecb8ed372014-11-18 17:05:35 -0800455 mSearchSrcTextView.setInputType(inputType);
Amith Yamasani5607a382011-08-09 14:16:37 -0700456 }
457
Amith Yamasanieca59d32012-04-25 18:57:18 -0700458 /**
459 * Returns the input type set on the query text field.
460 * @return the input type
461 *
462 * @attr ref android.R.styleable#SearchView_inputType
463 */
464 public int getInputType() {
Alan Viverettecb8ed372014-11-18 17:05:35 -0800465 return mSearchSrcTextView.getInputType();
Amith Yamasanieca59d32012-04-25 18:57:18 -0700466 }
467
Amith Yamasani05944762010-10-08 13:52:38 -0700468 /** @hide */
469 @Override
470 public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
Amith Yamasani7f8aef62011-01-25 11:58:09 -0800471 // Don't accept focus if in the middle of clearing focus
472 if (mClearingFocus) return false;
473 // Check if SearchView is focusable.
474 if (!isFocusable()) return false;
475 // If it is not iconified, then give the focus to the text field
476 if (!isIconified()) {
Alan Viverettecb8ed372014-11-18 17:05:35 -0800477 boolean result = mSearchSrcTextView.requestFocus(direction, previouslyFocusedRect);
Amith Yamasanif28d1872011-07-26 12:21:03 -0700478 if (result) {
479 updateViewsVisibility(false);
480 }
Amith Yamasani7f8aef62011-01-25 11:58:09 -0800481 return result;
482 } else {
483 return super.requestFocus(direction, previouslyFocusedRect);
484 }
Amith Yamasani05944762010-10-08 13:52:38 -0700485 }
486
487 /** @hide */
488 @Override
489 public void clearFocus() {
490 mClearingFocus = true;
Amith Yamasani10da5902011-07-26 16:14:26 -0700491 setImeVisibility(false);
Amith Yamasani05944762010-10-08 13:52:38 -0700492 super.clearFocus();
Alan Viverettecb8ed372014-11-18 17:05:35 -0800493 mSearchSrcTextView.clearFocus();
Amith Yamasani05944762010-10-08 13:52:38 -0700494 mClearingFocus = false;
495 }
496
Amith Yamasani733cbd52010-09-03 12:21:39 -0700497 /**
498 * Sets a listener for user actions within the SearchView.
499 *
500 * @param listener the listener object that receives callbacks when the user performs
501 * actions in the SearchView such as clicking on buttons or typing a query.
502 */
Adam Powell01f21352011-01-20 18:30:10 -0800503 public void setOnQueryTextListener(OnQueryTextListener listener) {
Amith Yamasani733cbd52010-09-03 12:21:39 -0700504 mOnQueryChangeListener = listener;
505 }
506
507 /**
Amith Yamasani93227752010-09-14 10:10:54 -0700508 * Sets a listener to inform when the user closes the SearchView.
509 *
510 * @param listener the listener to call when the user closes the SearchView.
511 */
512 public void setOnCloseListener(OnCloseListener listener) {
513 mOnCloseListener = listener;
514 }
515
516 /**
Amith Yamasani05944762010-10-08 13:52:38 -0700517 * Sets a listener to inform when the focus of the query text field changes.
518 *
519 * @param listener the listener to inform of focus changes.
520 */
521 public void setOnQueryTextFocusChangeListener(OnFocusChangeListener listener) {
522 mOnQueryTextFocusChangeListener = listener;
523 }
524
525 /**
526 * Sets a listener to inform when a suggestion is focused or clicked.
527 *
528 * @param listener the listener to inform of suggestion selection events.
529 */
Adam Powell01f21352011-01-20 18:30:10 -0800530 public void setOnSuggestionListener(OnSuggestionListener listener) {
Amith Yamasani05944762010-10-08 13:52:38 -0700531 mOnSuggestionListener = listener;
532 }
533
534 /**
Amith Yamasani48385482010-12-03 14:43:52 -0800535 * Sets a listener to inform when the search button is pressed. This is only
Scott Maincccdbe92011-02-06 15:51:47 -0800536 * relevant when the text field is not visible by default. Calling {@link #setIconified
537 * setIconified(false)} can also cause this listener to be informed.
Amith Yamasani48385482010-12-03 14:43:52 -0800538 *
539 * @param listener the listener to inform when the search button is clicked or
540 * the text field is programmatically de-iconified.
541 */
542 public void setOnSearchClickListener(OnClickListener listener) {
543 mOnSearchClickListener = listener;
544 }
545
546 /**
547 * Returns the query string currently in the text field.
548 *
549 * @return the query string
550 */
551 public CharSequence getQuery() {
Alan Viverettecb8ed372014-11-18 17:05:35 -0800552 return mSearchSrcTextView.getText();
Amith Yamasani48385482010-12-03 14:43:52 -0800553 }
554
555 /**
Amith Yamasani733cbd52010-09-03 12:21:39 -0700556 * Sets a query string in the text field and optionally submits the query as well.
557 *
558 * @param query the query string. This replaces any query text already present in the
559 * text field.
560 * @param submit whether to submit the query right now or only update the contents of
561 * text field.
562 */
563 public void setQuery(CharSequence query, boolean submit) {
Alan Viverettecb8ed372014-11-18 17:05:35 -0800564 mSearchSrcTextView.setText(query);
Dmitri Plotnikov87c50252010-10-21 21:16:42 -0700565 if (query != null) {
Alan Viverettecb8ed372014-11-18 17:05:35 -0800566 mSearchSrcTextView.setSelection(mSearchSrcTextView.length());
Amith Yamasani068d73c2011-05-27 15:15:14 -0700567 mUserQuery = query;
Dmitri Plotnikov87c50252010-10-21 21:16:42 -0700568 }
569
Amith Yamasani733cbd52010-09-03 12:21:39 -0700570 // If the query is not empty and submit is requested, submit the query
571 if (submit && !TextUtils.isEmpty(query)) {
572 onSubmitQuery();
573 }
574 }
575
576 /**
Alan Viveretteb4004df2015-04-29 16:55:42 -0700577 * Sets the hint text to display in the query text field. This overrides
578 * any hint specified in the {@link SearchableInfo}.
579 * <p>
580 * This value may be specified as an empty string to prevent any query hint
581 * from being displayed.
Amith Yamasani733cbd52010-09-03 12:21:39 -0700582 *
Alan Viveretteb4004df2015-04-29 16:55:42 -0700583 * @param hint the hint text to display or {@code null} to clear
Scott Mainabdf0d52011-02-08 10:20:27 -0800584 * @attr ref android.R.styleable#SearchView_queryHint
Amith Yamasani733cbd52010-09-03 12:21:39 -0700585 */
Alan Viveretteb4004df2015-04-29 16:55:42 -0700586 public void setQueryHint(@Nullable CharSequence hint) {
Amith Yamasani733cbd52010-09-03 12:21:39 -0700587 mQueryHint = hint;
588 updateQueryHint();
589 }
590
591 /**
Alan Viveretteb4004df2015-04-29 16:55:42 -0700592 * Returns the hint text that will be displayed in the query text field.
593 * <p>
594 * The displayed query hint is chosen in the following order:
595 * <ol>
596 * <li>Non-null value set with {@link #setQueryHint(CharSequence)}
597 * <li>Value specified in XML using
598 * {@link android.R.styleable#SearchView_queryHint android:queryHint}
599 * <li>Valid string resource ID exposed by the {@link SearchableInfo} via
600 * {@link SearchableInfo#getHintId()}
601 * <li>Default hint provided by the theme against which the view was
602 * inflated
603 * </ol>
Amith Yamasanieca59d32012-04-25 18:57:18 -0700604 *
Alan Viveretteb4004df2015-04-29 16:55:42 -0700605 * @return the displayed query hint text, or {@code null} if none set
Amith Yamasanieca59d32012-04-25 18:57:18 -0700606 * @attr ref android.R.styleable#SearchView_queryHint
607 */
Alan Viveretteb4004df2015-04-29 16:55:42 -0700608 @Nullable
Amith Yamasanieca59d32012-04-25 18:57:18 -0700609 public CharSequence getQueryHint() {
Alan Viveretteb4004df2015-04-29 16:55:42 -0700610 final CharSequence hint;
Amith Yamasanieca59d32012-04-25 18:57:18 -0700611 if (mQueryHint != null) {
Alan Viveretteb4004df2015-04-29 16:55:42 -0700612 hint = mQueryHint;
613 } else if (mSearchable != null && mSearchable.getHintId() != 0) {
614 hint = getContext().getText(mSearchable.getHintId());
615 } else {
616 hint = mDefaultQueryHint;
Amith Yamasanieca59d32012-04-25 18:57:18 -0700617 }
Alan Viveretteb4004df2015-04-29 16:55:42 -0700618 return hint;
Amith Yamasanieca59d32012-04-25 18:57:18 -0700619 }
620
621 /**
Amith Yamasani733cbd52010-09-03 12:21:39 -0700622 * Sets the default or resting state of the search field. If true, a single search icon is
623 * shown by default and expands to show the text field and other buttons when pressed. Also,
624 * if the default state is iconified, then it collapses to that state when the close button
Amith Yamasani93227752010-09-14 10:10:54 -0700625 * is pressed. Changes to this property will take effect immediately.
Amith Yamasani733cbd52010-09-03 12:21:39 -0700626 *
Scott Maincccdbe92011-02-06 15:51:47 -0800627 * <p>The default value is true.</p>
Amith Yamasani93227752010-09-14 10:10:54 -0700628 *
629 * @param iconified whether the search field should be iconified by default
Scott Mainabdf0d52011-02-08 10:20:27 -0800630 *
631 * @attr ref android.R.styleable#SearchView_iconifiedByDefault
Amith Yamasani733cbd52010-09-03 12:21:39 -0700632 */
633 public void setIconifiedByDefault(boolean iconified) {
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -0700634 if (mIconifiedByDefault == iconified) return;
Amith Yamasani733cbd52010-09-03 12:21:39 -0700635 mIconifiedByDefault = iconified;
636 updateViewsVisibility(iconified);
Amith Yamasanib47c4fd2011-08-04 14:30:07 -0700637 updateQueryHint();
Amith Yamasani733cbd52010-09-03 12:21:39 -0700638 }
639
Amith Yamasani93227752010-09-14 10:10:54 -0700640 /**
641 * Returns the default iconified state of the search field.
642 * @return
Amith Yamasani3d3e7a52012-05-04 14:46:31 -0700643 *
644 * @attr ref android.R.styleable#SearchView_iconifiedByDefault
Amith Yamasani93227752010-09-14 10:10:54 -0700645 */
Amith Yamasani733cbd52010-09-03 12:21:39 -0700646 public boolean isIconfiedByDefault() {
647 return mIconifiedByDefault;
648 }
649
650 /**
Amith Yamasani93227752010-09-14 10:10:54 -0700651 * Iconifies or expands the SearchView. Any query text is cleared when iconified. This is
652 * a temporary state and does not override the default iconified state set by
653 * {@link #setIconifiedByDefault(boolean)}. If the default state is iconified, then
654 * a false here will only be valid until the user closes the field. And if the default
655 * state is expanded, then a true here will only clear the text field and not close it.
656 *
657 * @param iconify a true value will collapse the SearchView to an icon, while a false will
658 * expand it.
659 */
660 public void setIconified(boolean iconify) {
661 if (iconify) {
662 onCloseClicked();
663 } else {
664 onSearchClicked();
665 }
666 }
667
668 /**
669 * Returns the current iconified state of the SearchView.
670 *
671 * @return true if the SearchView is currently iconified, false if the search field is
672 * fully visible.
673 */
674 public boolean isIconified() {
675 return mIconified;
676 }
677
678 /**
Amith Yamasani733cbd52010-09-03 12:21:39 -0700679 * Enables showing a submit button when the query is non-empty. In cases where the SearchView
680 * is being used to filter the contents of the current activity and doesn't launch a separate
681 * results activity, then the submit button should be disabled.
682 *
683 * @param enabled true to show a submit button for submitting queries, false if a submit
684 * button is not required.
685 */
686 public void setSubmitButtonEnabled(boolean enabled) {
Amith Yamasani733cbd52010-09-03 12:21:39 -0700687 mSubmitButtonEnabled = enabled;
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -0700688 updateViewsVisibility(isIconified());
Amith Yamasani733cbd52010-09-03 12:21:39 -0700689 }
690
691 /**
692 * Returns whether the submit button is enabled when necessary or never displayed.
693 *
694 * @return whether the submit button is enabled automatically when necessary
695 */
696 public boolean isSubmitButtonEnabled() {
697 return mSubmitButtonEnabled;
698 }
699
Amith Yamasanie678f462010-09-15 16:13:43 -0700700 /**
701 * Specifies if a query refinement button should be displayed alongside each suggestion
702 * or if it should depend on the flags set in the individual items retrieved from the
703 * suggestions provider. Clicking on the query refinement button will replace the text
704 * in the query text field with the text from the suggestion. This flag only takes effect
705 * if a SearchableInfo has been specified with {@link #setSearchableInfo(SearchableInfo)}
706 * and not when using a custom adapter.
707 *
708 * @param enable true if all items should have a query refinement button, false if only
709 * those items that have a query refinement flag set should have the button.
710 *
711 * @see SearchManager#SUGGEST_COLUMN_FLAGS
712 * @see SearchManager#FLAG_QUERY_REFINEMENT
713 */
714 public void setQueryRefinementEnabled(boolean enable) {
715 mQueryRefinement = enable;
716 if (mSuggestionsAdapter instanceof SuggestionsAdapter) {
717 ((SuggestionsAdapter) mSuggestionsAdapter).setQueryRefinement(
718 enable ? SuggestionsAdapter.REFINE_ALL : SuggestionsAdapter.REFINE_BY_ENTRY);
719 }
720 }
721
722 /**
723 * Returns whether query refinement is enabled for all items or only specific ones.
724 * @return true if enabled for all items, false otherwise.
725 */
726 public boolean isQueryRefinementEnabled() {
727 return mQueryRefinement;
Amith Yamasani733cbd52010-09-03 12:21:39 -0700728 }
729
730 /**
731 * You can set a custom adapter if you wish. Otherwise the default adapter is used to
732 * display the suggestions from the suggestions provider associated with the SearchableInfo.
733 *
734 * @see #setSearchableInfo(SearchableInfo)
735 */
736 public void setSuggestionsAdapter(CursorAdapter adapter) {
737 mSuggestionsAdapter = adapter;
738
Alan Viverettecb8ed372014-11-18 17:05:35 -0800739 mSearchSrcTextView.setAdapter(mSuggestionsAdapter);
Amith Yamasani733cbd52010-09-03 12:21:39 -0700740 }
741
742 /**
743 * Returns the adapter used for suggestions, if any.
744 * @return the suggestions adapter
745 */
746 public CursorAdapter getSuggestionsAdapter() {
747 return mSuggestionsAdapter;
748 }
749
Amith Yamasani5931b1f2010-10-18 16:13:14 -0700750 /**
751 * Makes the view at most this many pixels wide
752 *
753 * @attr ref android.R.styleable#SearchView_maxWidth
754 */
755 public void setMaxWidth(int maxpixels) {
756 mMaxWidth = maxpixels;
757
758 requestLayout();
759 }
760
Amith Yamasanieca59d32012-04-25 18:57:18 -0700761 /**
762 * Gets the specified maximum width in pixels, if set. Returns zero if
763 * no maximum width was specified.
764 * @return the maximum width of the view
Amith Yamasani3d3e7a52012-05-04 14:46:31 -0700765 *
766 * @attr ref android.R.styleable#SearchView_maxWidth
Amith Yamasanieca59d32012-04-25 18:57:18 -0700767 */
768 public int getMaxWidth() {
769 return mMaxWidth;
770 }
771
Amith Yamasani5931b1f2010-10-18 16:13:14 -0700772 @Override
773 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Amith Yamasania95e4882011-08-17 11:41:37 -0700774 // Let the standard measurements take effect in iconified state.
775 if (isIconified()) {
776 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
777 return;
778 }
779
Amith Yamasani5931b1f2010-10-18 16:13:14 -0700780 int widthMode = MeasureSpec.getMode(widthMeasureSpec);
781 int width = MeasureSpec.getSize(widthMeasureSpec);
782
Amith Yamasani167d69a2011-08-12 19:28:37 -0700783 switch (widthMode) {
784 case MeasureSpec.AT_MOST:
785 // If there is an upper limit, don't exceed maximum width (explicit or implicit)
786 if (mMaxWidth > 0) {
787 width = Math.min(mMaxWidth, width);
788 } else {
789 width = Math.min(getPreferredWidth(), width);
790 }
791 break;
792 case MeasureSpec.EXACTLY:
793 // If an exact width is specified, still don't exceed any specified maximum width
794 if (mMaxWidth > 0) {
795 width = Math.min(mMaxWidth, width);
796 }
797 break;
798 case MeasureSpec.UNSPECIFIED:
799 // Use maximum width, if specified, else preferred width
800 width = mMaxWidth > 0 ? mMaxWidth : getPreferredWidth();
801 break;
Amith Yamasani5931b1f2010-10-18 16:13:14 -0700802 }
Amith Yamasani167d69a2011-08-12 19:28:37 -0700803 widthMode = MeasureSpec.EXACTLY;
804 super.onMeasure(MeasureSpec.makeMeasureSpec(width, widthMode), heightMeasureSpec);
805 }
806
807 private int getPreferredWidth() {
808 return getContext().getResources()
809 .getDimensionPixelSize(R.dimen.search_view_preferred_width);
Amith Yamasani5931b1f2010-10-18 16:13:14 -0700810 }
811
Amith Yamasani93227752010-09-14 10:10:54 -0700812 private void updateViewsVisibility(final boolean collapsed) {
813 mIconified = collapsed;
Amith Yamasani733cbd52010-09-03 12:21:39 -0700814 // Visibility of views that are visible when collapsed
Amith Yamasani93227752010-09-14 10:10:54 -0700815 final int visCollapsed = collapsed ? VISIBLE : GONE;
Amith Yamasani05944762010-10-08 13:52:38 -0700816 // Is there text in the query
Alan Viverettecb8ed372014-11-18 17:05:35 -0800817 final boolean hasText = !TextUtils.isEmpty(mSearchSrcTextView.getText());
Amith Yamasani733cbd52010-09-03 12:21:39 -0700818
819 mSearchButton.setVisibility(visCollapsed);
Amith Yamasani9b2e3022011-01-14 11:34:12 -0800820 updateSubmitButton(hasText);
821 mSearchEditFrame.setVisibility(collapsed ? GONE : VISIBLE);
Alan Viverette53b165e2015-08-25 12:33:01 -0400822
823 final int iconVisibility;
824 if (mCollapsedIcon.getDrawable() == null || mIconifiedByDefault) {
825 iconVisibility = GONE;
826 } else {
827 iconVisibility = VISIBLE;
828 }
829 mCollapsedIcon.setVisibility(iconVisibility);
830
Amith Yamasani4aedb392010-12-15 16:04:57 -0800831 updateCloseButton();
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -0700832 updateVoiceButton(!hasText);
Amith Yamasani9b2e3022011-01-14 11:34:12 -0800833 updateSubmitArea();
Amith Yamasani9b2e3022011-01-14 11:34:12 -0800834 }
835
836 private boolean hasVoiceSearch() {
837 if (mSearchable != null && mSearchable.getVoiceSearchEnabled()) {
838 Intent testIntent = null;
839 if (mSearchable.getVoiceSearchLaunchWebSearch()) {
840 testIntent = mVoiceWebSearchIntent;
841 } else if (mSearchable.getVoiceSearchLaunchRecognizer()) {
842 testIntent = mVoiceAppSearchIntent;
843 }
844 if (testIntent != null) {
845 ResolveInfo ri = getContext().getPackageManager().resolveActivity(testIntent,
846 PackageManager.MATCH_DEFAULT_ONLY);
847 return ri != null;
848 }
849 }
850 return false;
851 }
852
853 private boolean isSubmitAreaEnabled() {
854 return (mSubmitButtonEnabled || mVoiceButtonEnabled) && !isIconified();
855 }
856
857 private void updateSubmitButton(boolean hasText) {
Amith Yamasani79f74302011-03-08 14:16:35 -0800858 int visibility = GONE;
Amith Yamasanicf72ab42011-11-04 13:49:28 -0700859 if (mSubmitButtonEnabled && isSubmitAreaEnabled() && hasFocus()
860 && (hasText || !mVoiceButtonEnabled)) {
Amith Yamasani79f74302011-03-08 14:16:35 -0800861 visibility = VISIBLE;
862 }
Alan Viverettecb8ed372014-11-18 17:05:35 -0800863 mGoButton.setVisibility(visibility);
Amith Yamasani9b2e3022011-01-14 11:34:12 -0800864 }
865
866 private void updateSubmitArea() {
867 int visibility = GONE;
Amith Yamasani79f74302011-03-08 14:16:35 -0800868 if (isSubmitAreaEnabled()
Alan Viverettecb8ed372014-11-18 17:05:35 -0800869 && (mGoButton.getVisibility() == VISIBLE
Amith Yamasani79f74302011-03-08 14:16:35 -0800870 || mVoiceButton.getVisibility() == VISIBLE)) {
871 visibility = VISIBLE;
Amith Yamasani9b2e3022011-01-14 11:34:12 -0800872 }
873 mSubmitArea.setVisibility(visibility);
Amith Yamasani733cbd52010-09-03 12:21:39 -0700874 }
875
Amith Yamasani4aedb392010-12-15 16:04:57 -0800876 private void updateCloseButton() {
Alan Viverettecb8ed372014-11-18 17:05:35 -0800877 final boolean hasText = !TextUtils.isEmpty(mSearchSrcTextView.getText());
Amith Yamasani4aedb392010-12-15 16:04:57 -0800878 // Should we show the close button? It is not shown if there's no focus,
879 // field is not iconified by default and there is no text in it.
Amith Yamasani763bc072011-07-22 11:53:47 -0700880 final boolean showClose = hasText || (mIconifiedByDefault && !mExpandedInActionView);
Amith Yamasani167d69a2011-08-12 19:28:37 -0700881 mCloseButton.setVisibility(showClose ? VISIBLE : GONE);
Alan Viverettecb8ed372014-11-18 17:05:35 -0800882 final Drawable closeButtonImg = mCloseButton.getDrawable();
883 if (closeButtonImg != null){
884 closeButtonImg.setState(hasText ? ENABLED_STATE_SET : EMPTY_STATE_SET);
885 }
Amith Yamasani4aedb392010-12-15 16:04:57 -0800886 }
887
Amith Yamasania95e4882011-08-17 11:41:37 -0700888 private void postUpdateFocusedState() {
889 post(mUpdateDrawableStateRunnable);
890 }
891
892 private void updateFocusedState() {
Alan Viverettecb8ed372014-11-18 17:05:35 -0800893 final boolean focused = mSearchSrcTextView.hasFocus();
894 final int[] stateSet = focused ? FOCUSED_STATE_SET : EMPTY_STATE_SET;
895 final Drawable searchPlateBg = mSearchPlate.getBackground();
896 if (searchPlateBg != null) {
897 searchPlateBg.setState(stateSet);
898 }
899 final Drawable submitAreaBg = mSubmitArea.getBackground();
900 if (submitAreaBg != null) {
901 submitAreaBg.setState(stateSet);
902 }
Amith Yamasania95e4882011-08-17 11:41:37 -0700903 invalidate();
904 }
905
906 @Override
Amith Yamasania465b2d2011-08-19 13:01:22 -0700907 protected void onDetachedFromWindow() {
Amith Yamasania95e4882011-08-17 11:41:37 -0700908 removeCallbacks(mUpdateDrawableStateRunnable);
Amith Yamasani87907642011-11-03 11:32:44 -0700909 post(mReleaseCursorRunnable);
Amith Yamasania95e4882011-08-17 11:41:37 -0700910 super.onDetachedFromWindow();
Amith Yamasani79f74302011-03-08 14:16:35 -0800911 }
912
Adam Powellccdd4ee2011-07-27 20:05:14 -0700913 private void setImeVisibility(final boolean visible) {
914 if (visible) {
915 post(mShowImeRunnable);
916 } else {
917 removeCallbacks(mShowImeRunnable);
Yohei Yukawa777ef952015-11-25 20:32:24 -0800918 InputMethodManager imm = getContext().getSystemService(InputMethodManager.class);
repo sync6a81b822010-09-28 13:00:05 -0700919
Adam Powellccdd4ee2011-07-27 20:05:14 -0700920 if (imm != null) {
Amith Yamasani05944762010-10-08 13:52:38 -0700921 imm.hideSoftInputFromWindow(getWindowToken(), 0);
Amith Yamasani733cbd52010-09-03 12:21:39 -0700922 }
923 }
924 }
925
Amith Yamasanie678f462010-09-15 16:13:43 -0700926 /**
927 * Called by the SuggestionsAdapter
928 * @hide
929 */
930 /* package */void onQueryRefine(CharSequence queryText) {
931 setQuery(queryText);
932 }
933
Amith Yamasani733cbd52010-09-03 12:21:39 -0700934 private final OnClickListener mOnClickListener = new OnClickListener() {
935
936 public void onClick(View v) {
937 if (v == mSearchButton) {
938 onSearchClicked();
939 } else if (v == mCloseButton) {
940 onCloseClicked();
Alan Viverettecb8ed372014-11-18 17:05:35 -0800941 } else if (v == mGoButton) {
Amith Yamasani733cbd52010-09-03 12:21:39 -0700942 onSubmitQuery();
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -0700943 } else if (v == mVoiceButton) {
944 onVoiceClicked();
Alan Viverettecb8ed372014-11-18 17:05:35 -0800945 } else if (v == mSearchSrcTextView) {
Amith Yamasanif28d1872011-07-26 12:21:03 -0700946 forceSuggestionQuery();
Amith Yamasani733cbd52010-09-03 12:21:39 -0700947 }
948 }
949 };
950
951 /**
952 * Handles the key down event for dealing with action keys.
953 *
954 * @param keyCode This is the keycode of the typed key, and is the same value as
955 * found in the KeyEvent parameter.
956 * @param event The complete event record for the typed key
957 *
958 * @return true if the event was handled here, or false if not.
959 */
960 @Override
961 public boolean onKeyDown(int keyCode, KeyEvent event) {
962 if (mSearchable == null) {
963 return false;
964 }
965
966 // if it's an action specified by the searchable activity, launch the
967 // entered query with the action key
968 SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
969 if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) {
Alan Viverettecb8ed372014-11-18 17:05:35 -0800970 launchQuerySearch(keyCode, actionKey.getQueryActionMsg(), mSearchSrcTextView.getText()
Amith Yamasani93227752010-09-14 10:10:54 -0700971 .toString());
Amith Yamasani733cbd52010-09-03 12:21:39 -0700972 return true;
973 }
974
975 return super.onKeyDown(keyCode, event);
976 }
977
Amith Yamasani968ec932010-12-02 14:00:47 -0800978 /**
979 * React to the user typing "enter" or other hardwired keys while typing in
980 * the search box. This handles these special keys while the edit box has
981 * focus.
982 */
983 View.OnKeyListener mTextKeyListener = new View.OnKeyListener() {
984 public boolean onKey(View v, int keyCode, KeyEvent event) {
985 // guard against possible race conditions
986 if (mSearchable == null) {
987 return false;
988 }
989
990 if (DBG) {
991 Log.d(LOG_TAG, "mTextListener.onKey(" + keyCode + "," + event + "), selection: "
Alan Viverettecb8ed372014-11-18 17:05:35 -0800992 + mSearchSrcTextView.getListSelection());
Amith Yamasani968ec932010-12-02 14:00:47 -0800993 }
994
995 // If a suggestion is selected, handle enter, search key, and action keys
996 // as presses on the selected suggestion
Alan Viverettecb8ed372014-11-18 17:05:35 -0800997 if (mSearchSrcTextView.isPopupShowing()
998 && mSearchSrcTextView.getListSelection() != ListView.INVALID_POSITION) {
Amith Yamasani968ec932010-12-02 14:00:47 -0800999 return onSuggestionsKey(v, keyCode, event);
1000 }
1001
1002 // If there is text in the query box, handle enter, and action keys
1003 // The search key is handled by the dialog's onKeyDown().
Alan Viverettecb8ed372014-11-18 17:05:35 -08001004 if (!mSearchSrcTextView.isEmpty() && event.hasNoModifiers()) {
Jeff Brown4e6319b2010-12-13 10:36:51 -08001005 if (event.getAction() == KeyEvent.ACTION_UP) {
1006 if (keyCode == KeyEvent.KEYCODE_ENTER) {
1007 v.cancelLongPress();
Amith Yamasani968ec932010-12-02 14:00:47 -08001008
Jeff Brown4e6319b2010-12-13 10:36:51 -08001009 // Launch as a regular search.
Alan Viverettecb8ed372014-11-18 17:05:35 -08001010 launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null, mSearchSrcTextView.getText()
Jeff Brown4e6319b2010-12-13 10:36:51 -08001011 .toString());
1012 return true;
1013 }
Amith Yamasani968ec932010-12-02 14:00:47 -08001014 }
1015 if (event.getAction() == KeyEvent.ACTION_DOWN) {
1016 SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
1017 if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) {
Alan Viverettecb8ed372014-11-18 17:05:35 -08001018 launchQuerySearch(keyCode, actionKey.getQueryActionMsg(), mSearchSrcTextView
Amith Yamasani968ec932010-12-02 14:00:47 -08001019 .getText().toString());
1020 return true;
1021 }
1022 }
1023 }
1024 return false;
1025 }
1026 };
1027
1028 /**
1029 * React to the user typing while in the suggestions list. First, check for
1030 * action keys. If not handled, try refocusing regular characters into the
1031 * EditText.
1032 */
1033 private boolean onSuggestionsKey(View v, int keyCode, KeyEvent event) {
1034 // guard against possible race conditions (late arrival after dismiss)
1035 if (mSearchable == null) {
1036 return false;
1037 }
1038 if (mSuggestionsAdapter == null) {
1039 return false;
1040 }
Jeff Brown4e6319b2010-12-13 10:36:51 -08001041 if (event.getAction() == KeyEvent.ACTION_DOWN && event.hasNoModifiers()) {
Amith Yamasani968ec932010-12-02 14:00:47 -08001042 // First, check for enter or search (both of which we'll treat as a
1043 // "click")
Jeff Brown4e6319b2010-12-13 10:36:51 -08001044 if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_SEARCH
1045 || keyCode == KeyEvent.KEYCODE_TAB) {
Alan Viverettecb8ed372014-11-18 17:05:35 -08001046 int position = mSearchSrcTextView.getListSelection();
Amith Yamasani968ec932010-12-02 14:00:47 -08001047 return onItemClicked(position, KeyEvent.KEYCODE_UNKNOWN, null);
1048 }
1049
1050 // Next, check for left/right moves, which we use to "return" the
1051 // user to the edit view
1052 if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
1053 // give "focus" to text editor, with cursor at the beginning if
1054 // left key, at end if right key
1055 // TODO: Reverse left/right for right-to-left languages, e.g.
1056 // Arabic
Alan Viverettecb8ed372014-11-18 17:05:35 -08001057 int selPoint = (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) ? 0 : mSearchSrcTextView
Amith Yamasani968ec932010-12-02 14:00:47 -08001058 .length();
Alan Viverettecb8ed372014-11-18 17:05:35 -08001059 mSearchSrcTextView.setSelection(selPoint);
1060 mSearchSrcTextView.setListSelection(0);
1061 mSearchSrcTextView.clearListSelection();
1062 mSearchSrcTextView.ensureImeVisible(true);
Amith Yamasani968ec932010-12-02 14:00:47 -08001063
1064 return true;
1065 }
1066
1067 // Next, check for an "up and out" move
Alan Viverettecb8ed372014-11-18 17:05:35 -08001068 if (keyCode == KeyEvent.KEYCODE_DPAD_UP && 0 == mSearchSrcTextView.getListSelection()) {
Amith Yamasani968ec932010-12-02 14:00:47 -08001069 // TODO: restoreUserQuery();
1070 // let ACTV complete the move
1071 return false;
1072 }
1073
1074 // Next, check for an "action key"
1075 SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
1076 if ((actionKey != null)
1077 && ((actionKey.getSuggestActionMsg() != null) || (actionKey
1078 .getSuggestActionMsgColumn() != null))) {
1079 // launch suggestion using action key column
Alan Viverettecb8ed372014-11-18 17:05:35 -08001080 int position = mSearchSrcTextView.getListSelection();
Amith Yamasani968ec932010-12-02 14:00:47 -08001081 if (position != ListView.INVALID_POSITION) {
1082 Cursor c = mSuggestionsAdapter.getCursor();
1083 if (c.moveToPosition(position)) {
1084 final String actionMsg = getActionKeyMessage(c, actionKey);
1085 if (actionMsg != null && (actionMsg.length() > 0)) {
1086 return onItemClicked(position, keyCode, actionMsg);
1087 }
1088 }
1089 }
1090 }
1091 }
1092 return false;
1093 }
1094
1095 /**
1096 * For a given suggestion and a given cursor row, get the action message. If
1097 * not provided by the specific row/column, also check for a single
1098 * definition (for the action key).
1099 *
1100 * @param c The cursor providing suggestions
1101 * @param actionKey The actionkey record being examined
1102 *
1103 * @return Returns a string, or null if no action key message for this
1104 * suggestion
1105 */
1106 private static String getActionKeyMessage(Cursor c, SearchableInfo.ActionKeyInfo actionKey) {
1107 String result = null;
1108 // check first in the cursor data, for a suggestion-specific message
1109 final String column = actionKey.getSuggestActionMsgColumn();
1110 if (column != null) {
1111 result = SuggestionsAdapter.getColumnString(c, column);
1112 }
1113 // If the cursor didn't give us a message, see if there's a single
1114 // message defined
1115 // for the actionkey (for all suggestions)
1116 if (result == null) {
1117 result = actionKey.getSuggestActionMsg();
1118 }
1119 return result;
1120 }
1121
Amith Yamasanib4569fb2011-07-08 15:25:39 -07001122 private CharSequence getDecoratedHint(CharSequence hintText) {
Alan Viverettecb8ed372014-11-18 17:05:35 -08001123 // If the field is always expanded or we don't have a search hint icon,
1124 // then don't add the search icon to the hint.
1125 if (!mIconifiedByDefault || mSearchHintIcon == null) {
Alan Viverette5dddb702014-07-02 15:46:04 -07001126 return hintText;
1127 }
Amith Yamasanib4569fb2011-07-08 15:25:39 -07001128
Alan Viverettecb8ed372014-11-18 17:05:35 -08001129 final int textSize = (int) (mSearchSrcTextView.getTextSize() * 1.25);
1130 mSearchHintIcon.setBounds(0, 0, textSize, textSize);
Alan Viverette5dddb702014-07-02 15:46:04 -07001131
Alan Viverettecb8ed372014-11-18 17:05:35 -08001132 final SpannableStringBuilder ssb = new SpannableStringBuilder(" ");
1133 ssb.setSpan(new ImageSpan(mSearchHintIcon), 1, 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
Alan Viverette5dddb702014-07-02 15:46:04 -07001134 ssb.append(hintText);
Amith Yamasanib4569fb2011-07-08 15:25:39 -07001135 return ssb;
1136 }
1137
Amith Yamasani733cbd52010-09-03 12:21:39 -07001138 private void updateQueryHint() {
Alan Viveretteb4004df2015-04-29 16:55:42 -07001139 final CharSequence hint = getQueryHint();
1140 mSearchSrcTextView.setHint(getDecoratedHint(hint == null ? "" : hint));
Amith Yamasani733cbd52010-09-03 12:21:39 -07001141 }
1142
1143 /**
1144 * Updates the auto-complete text view.
1145 */
1146 private void updateSearchAutoComplete() {
Alan Viverettecb8ed372014-11-18 17:05:35 -08001147 mSearchSrcTextView.setDropDownAnimationStyle(0); // no animation
1148 mSearchSrcTextView.setThreshold(mSearchable.getSuggestThreshold());
1149 mSearchSrcTextView.setImeOptions(mSearchable.getImeOptions());
Amith Yamasani5607a382011-08-09 14:16:37 -07001150 int inputType = mSearchable.getInputType();
1151 // We only touch this if the input type is set up for text (which it almost certainly
1152 // should be, in the case of search!)
1153 if ((inputType & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_TEXT) {
1154 // The existence of a suggestions authority is the proxy for "suggestions
1155 // are available here"
1156 inputType &= ~InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;
1157 if (mSearchable.getSuggestAuthority() != null) {
1158 inputType |= InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;
Satoshi Kataoka9ce11162012-06-04 17:12:08 +09001159 // TYPE_TEXT_FLAG_AUTO_COMPLETE means that the text editor is performing
1160 // auto-completion based on its own semantics, which it will present to the user
1161 // as they type. This generally means that the input method should not show its
1162 // own candidates, and the spell checker should not be in action. The text editor
1163 // supplies its candidates by calling InputMethodManager.displayCompletions(),
1164 // which in turn will call InputMethodSession.displayCompletions().
1165 inputType |= InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
Amith Yamasani5607a382011-08-09 14:16:37 -07001166 }
1167 }
Alan Viverettecb8ed372014-11-18 17:05:35 -08001168 mSearchSrcTextView.setInputType(inputType);
Amith Yamasani87907642011-11-03 11:32:44 -07001169 if (mSuggestionsAdapter != null) {
1170 mSuggestionsAdapter.changeCursor(null);
1171 }
Amith Yamasani733cbd52010-09-03 12:21:39 -07001172 // attach the suggestions adapter, if suggestions are available
1173 // The existence of a suggestions authority is the proxy for "suggestions available here"
1174 if (mSearchable.getSuggestAuthority() != null) {
1175 mSuggestionsAdapter = new SuggestionsAdapter(getContext(),
1176 this, mSearchable, mOutsideDrawablesCache);
Alan Viverettecb8ed372014-11-18 17:05:35 -08001177 mSearchSrcTextView.setAdapter(mSuggestionsAdapter);
Amith Yamasanie678f462010-09-15 16:13:43 -07001178 ((SuggestionsAdapter) mSuggestionsAdapter).setQueryRefinement(
1179 mQueryRefinement ? SuggestionsAdapter.REFINE_ALL
1180 : SuggestionsAdapter.REFINE_BY_ENTRY);
Amith Yamasani733cbd52010-09-03 12:21:39 -07001181 }
1182 }
1183
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001184 /**
1185 * Update the visibility of the voice button. There are actually two voice search modes,
1186 * either of which will activate the button.
1187 * @param empty whether the search query text field is empty. If it is, then the other
Amith Yamasani79f74302011-03-08 14:16:35 -08001188 * criteria apply to make the voice button visible.
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001189 */
1190 private void updateVoiceButton(boolean empty) {
Amith Yamasani79f74302011-03-08 14:16:35 -08001191 int visibility = GONE;
Amith Yamasani167d69a2011-08-12 19:28:37 -07001192 if (mVoiceButtonEnabled && !isIconified() && empty) {
Amith Yamasani9b2e3022011-01-14 11:34:12 -08001193 visibility = VISIBLE;
Alan Viverettecb8ed372014-11-18 17:05:35 -08001194 mGoButton.setVisibility(GONE);
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001195 }
1196 mVoiceButton.setVisibility(visibility);
1197 }
1198
Amith Yamasani733cbd52010-09-03 12:21:39 -07001199 private final OnEditorActionListener mOnEditorActionListener = new OnEditorActionListener() {
1200
1201 /**
1202 * Called when the input method default action key is pressed.
1203 */
1204 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
1205 onSubmitQuery();
1206 return true;
1207 }
1208 };
1209
1210 private void onTextChanged(CharSequence newText) {
Alan Viverettecb8ed372014-11-18 17:05:35 -08001211 CharSequence text = mSearchSrcTextView.getText();
Amith Yamasani068d73c2011-05-27 15:15:14 -07001212 mUserQuery = text;
Amith Yamasani733cbd52010-09-03 12:21:39 -07001213 boolean hasText = !TextUtils.isEmpty(text);
Amith Yamasanicf72ab42011-11-04 13:49:28 -07001214 updateSubmitButton(hasText);
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001215 updateVoiceButton(!hasText);
Amith Yamasani73e00df2010-12-16 16:31:29 -08001216 updateCloseButton();
Amith Yamasani9b2e3022011-01-14 11:34:12 -08001217 updateSubmitArea();
Amith Yamasanib47c4fd2011-08-04 14:30:07 -07001218 if (mOnQueryChangeListener != null && !TextUtils.equals(newText, mOldQueryText)) {
Adam Powell01f21352011-01-20 18:30:10 -08001219 mOnQueryChangeListener.onQueryTextChange(newText.toString());
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001220 }
Amith Yamasanib47c4fd2011-08-04 14:30:07 -07001221 mOldQueryText = newText.toString();
Amith Yamasani733cbd52010-09-03 12:21:39 -07001222 }
1223
1224 private void onSubmitQuery() {
Alan Viverettecb8ed372014-11-18 17:05:35 -08001225 CharSequence query = mSearchSrcTextView.getText();
Amith Yamasani6a7421b2011-07-27 11:55:53 -07001226 if (query != null && TextUtils.getTrimmedLength(query) > 0) {
Amith Yamasani733cbd52010-09-03 12:21:39 -07001227 if (mOnQueryChangeListener == null
Adam Powell01f21352011-01-20 18:30:10 -08001228 || !mOnQueryChangeListener.onQueryTextSubmit(query.toString())) {
Amith Yamasani733cbd52010-09-03 12:21:39 -07001229 if (mSearchable != null) {
1230 launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null, query.toString());
1231 }
Fabrice Di Meglio2182f2a2014-03-25 14:03:33 -07001232 setImeVisibility(false);
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001233 dismissSuggestions();
Amith Yamasani733cbd52010-09-03 12:21:39 -07001234 }
1235 }
1236 }
1237
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001238 private void dismissSuggestions() {
Alan Viverettecb8ed372014-11-18 17:05:35 -08001239 mSearchSrcTextView.dismissDropDown();
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001240 }
1241
Amith Yamasani733cbd52010-09-03 12:21:39 -07001242 private void onCloseClicked() {
Alan Viverettecb8ed372014-11-18 17:05:35 -08001243 CharSequence text = mSearchSrcTextView.getText();
Amith Yamasani24652982011-06-23 16:16:05 -07001244 if (TextUtils.isEmpty(text)) {
1245 if (mIconifiedByDefault) {
Amith Yamasanib4569fb2011-07-08 15:25:39 -07001246 // If the app doesn't override the close behavior
1247 if (mOnCloseListener == null || !mOnCloseListener.onClose()) {
1248 // hide the keyboard and remove focus
1249 clearFocus();
1250 // collapse the search field
1251 updateViewsVisibility(true);
1252 }
Amith Yamasani05944762010-10-08 13:52:38 -07001253 }
Amith Yamasani24652982011-06-23 16:16:05 -07001254 } else {
Alan Viverettecb8ed372014-11-18 17:05:35 -08001255 mSearchSrcTextView.setText("");
1256 mSearchSrcTextView.requestFocus();
Amith Yamasani24652982011-06-23 16:16:05 -07001257 setImeVisibility(true);
1258 }
1259
Amith Yamasani733cbd52010-09-03 12:21:39 -07001260 }
1261
1262 private void onSearchClicked() {
Amith Yamasani733cbd52010-09-03 12:21:39 -07001263 updateViewsVisibility(false);
Alan Viverettecb8ed372014-11-18 17:05:35 -08001264 mSearchSrcTextView.requestFocus();
Amith Yamasani05944762010-10-08 13:52:38 -07001265 setImeVisibility(true);
Amith Yamasani48385482010-12-03 14:43:52 -08001266 if (mOnSearchClickListener != null) {
1267 mOnSearchClickListener.onClick(this);
1268 }
Amith Yamasani733cbd52010-09-03 12:21:39 -07001269 }
1270
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001271 private void onVoiceClicked() {
1272 // guard against possible race conditions
1273 if (mSearchable == null) {
1274 return;
1275 }
1276 SearchableInfo searchable = mSearchable;
1277 try {
1278 if (searchable.getVoiceSearchLaunchWebSearch()) {
1279 Intent webSearchIntent = createVoiceWebSearchIntent(mVoiceWebSearchIntent,
1280 searchable);
1281 getContext().startActivity(webSearchIntent);
1282 } else if (searchable.getVoiceSearchLaunchRecognizer()) {
1283 Intent appSearchIntent = createVoiceAppSearchIntent(mVoiceAppSearchIntent,
1284 searchable);
1285 getContext().startActivity(appSearchIntent);
1286 }
1287 } catch (ActivityNotFoundException e) {
1288 // Should not happen, since we check the availability of
1289 // voice search before showing the button. But just in case...
1290 Log.w(LOG_TAG, "Could not find voice search activity");
1291 }
1292 }
1293
Amith Yamasani4aedb392010-12-15 16:04:57 -08001294 void onTextFocusChanged() {
Amith Yamasani79f74302011-03-08 14:16:35 -08001295 updateViewsVisibility(isIconified());
Amith Yamasania95e4882011-08-17 11:41:37 -07001296 // Delayed update to make sure that the focus has settled down and window focus changes
1297 // don't affect it. A synchronous update was not working.
1298 postUpdateFocusedState();
Alan Viverettecb8ed372014-11-18 17:05:35 -08001299 if (mSearchSrcTextView.hasFocus()) {
Amith Yamasanif28d1872011-07-26 12:21:03 -07001300 forceSuggestionQuery();
1301 }
Amith Yamasani4aedb392010-12-15 16:04:57 -08001302 }
1303
Amith Yamasanib4569fb2011-07-08 15:25:39 -07001304 @Override
Amith Yamasania95e4882011-08-17 11:41:37 -07001305 public void onWindowFocusChanged(boolean hasWindowFocus) {
1306 super.onWindowFocusChanged(hasWindowFocus);
1307
1308 postUpdateFocusedState();
Amith Yamasanib4569fb2011-07-08 15:25:39 -07001309 }
1310
Amith Yamasani763bc072011-07-22 11:53:47 -07001311 /**
1312 * {@inheritDoc}
1313 */
1314 @Override
1315 public void onActionViewCollapsed() {
Adam Powell99d0ce92012-12-10 13:11:47 -08001316 setQuery("", false);
Amith Yamasani10da5902011-07-26 16:14:26 -07001317 clearFocus();
1318 updateViewsVisibility(true);
Alan Viverettecb8ed372014-11-18 17:05:35 -08001319 mSearchSrcTextView.setImeOptions(mCollapsedImeOptions);
Amith Yamasani763bc072011-07-22 11:53:47 -07001320 mExpandedInActionView = false;
1321 }
1322
1323 /**
1324 * {@inheritDoc}
1325 */
1326 @Override
1327 public void onActionViewExpanded() {
Amith Yamasani434c73f2011-11-01 11:44:50 -07001328 if (mExpandedInActionView) return;
1329
Amith Yamasani763bc072011-07-22 11:53:47 -07001330 mExpandedInActionView = true;
Alan Viverettecb8ed372014-11-18 17:05:35 -08001331 mCollapsedImeOptions = mSearchSrcTextView.getImeOptions();
1332 mSearchSrcTextView.setImeOptions(mCollapsedImeOptions | EditorInfo.IME_FLAG_NO_FULLSCREEN);
1333 mSearchSrcTextView.setText("");
Amith Yamasani763bc072011-07-22 11:53:47 -07001334 setIconified(false);
1335 }
1336
Aurimas Liutikas13fdea02016-02-11 10:10:10 -08001337 static class SavedState extends BaseSavedState {
1338 boolean isIconified;
1339
1340 SavedState(Parcelable superState) {
1341 super(superState);
1342 }
1343
1344 public SavedState(Parcel source) {
1345 super(source);
1346 isIconified = (Boolean) source.readValue(null);
1347 }
1348
1349 @Override
1350 public void writeToParcel(Parcel dest, int flags) {
1351 super.writeToParcel(dest, flags);
1352 dest.writeValue(isIconified);
1353 }
1354
1355 @Override
1356 public String toString() {
1357 return "SearchView.SavedState{"
1358 + Integer.toHexString(System.identityHashCode(this))
1359 + " isIconified=" + isIconified + "}";
1360 }
Aurimas Liutikas7849a3d2016-02-26 15:27:31 -08001361
1362 public static final Parcelable.Creator<SavedState> CREATOR =
1363 new Parcelable.Creator<SavedState>() {
1364 public SavedState createFromParcel(Parcel in) {
1365 return new SavedState(in);
1366 }
1367
1368 public SavedState[] newArray(int size) {
1369 return new SavedState[size];
1370 }
1371 };
Aurimas Liutikas13fdea02016-02-11 10:10:10 -08001372 }
1373
1374 @Override
1375 protected Parcelable onSaveInstanceState() {
1376 Parcelable superState = super.onSaveInstanceState();
1377 SavedState ss = new SavedState(superState);
1378 ss.isIconified = isIconified();
1379 return ss;
1380 }
1381
1382 @Override
1383 protected void onRestoreInstanceState(Parcelable state) {
1384 SavedState ss = (SavedState) state;
1385 super.onRestoreInstanceState(ss.getSuperState());
1386 updateViewsVisibility(ss.isIconified);
1387 requestLayout();
1388 }
1389
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001390 @Override
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -08001391 public CharSequence getAccessibilityClassName() {
1392 return SearchView.class.getName();
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -08001393 }
1394
Amith Yamasanib4569fb2011-07-08 15:25:39 -07001395 private void adjustDropDownSizeAndPosition() {
1396 if (mDropDownAnchor.getWidth() > 1) {
1397 Resources res = getContext().getResources();
1398 int anchorPadding = mSearchPlate.getPaddingLeft();
1399 Rect dropDownPadding = new Rect();
Fabrice Di Meglio3a69bea2012-07-26 13:50:10 -07001400 final boolean isLayoutRtl = isLayoutRtl();
Amith Yamasanib4569fb2011-07-08 15:25:39 -07001401 int iconOffset = mIconifiedByDefault
1402 ? res.getDimensionPixelSize(R.dimen.dropdownitem_icon_width)
1403 + res.getDimensionPixelSize(R.dimen.dropdownitem_text_padding_left)
1404 : 0;
Alan Viverettecb8ed372014-11-18 17:05:35 -08001405 mSearchSrcTextView.getDropDownBackground().getPadding(dropDownPadding);
Fabrice Di Meglio3a69bea2012-07-26 13:50:10 -07001406 int offset;
1407 if (isLayoutRtl) {
1408 offset = - dropDownPadding.left;
1409 } else {
1410 offset = anchorPadding - (dropDownPadding.left + iconOffset);
1411 }
Alan Viverettecb8ed372014-11-18 17:05:35 -08001412 mSearchSrcTextView.setDropDownHorizontalOffset(offset);
Fabrice Di Meglio3a69bea2012-07-26 13:50:10 -07001413 final int width = mDropDownAnchor.getWidth() + dropDownPadding.left
1414 + dropDownPadding.right + iconOffset - anchorPadding;
Alan Viverettecb8ed372014-11-18 17:05:35 -08001415 mSearchSrcTextView.setDropDownWidth(width);
Amith Yamasanib4569fb2011-07-08 15:25:39 -07001416 }
1417 }
1418
Amith Yamasani968ec932010-12-02 14:00:47 -08001419 private boolean onItemClicked(int position, int actionKey, String actionMsg) {
1420 if (mOnSuggestionListener == null
Adam Powell01f21352011-01-20 18:30:10 -08001421 || !mOnSuggestionListener.onSuggestionClick(position)) {
Amith Yamasani968ec932010-12-02 14:00:47 -08001422 launchSuggestion(position, KeyEvent.KEYCODE_UNKNOWN, null);
1423 setImeVisibility(false);
1424 dismissSuggestions();
1425 return true;
1426 }
1427 return false;
1428 }
1429
1430 private boolean onItemSelected(int position) {
1431 if (mOnSuggestionListener == null
Adam Powell01f21352011-01-20 18:30:10 -08001432 || !mOnSuggestionListener.onSuggestionSelect(position)) {
Amith Yamasani968ec932010-12-02 14:00:47 -08001433 rewriteQueryFromSuggestion(position);
1434 return true;
1435 }
1436 return false;
1437 }
1438
Amith Yamasani733cbd52010-09-03 12:21:39 -07001439 private final OnItemClickListener mOnItemClickListener = new OnItemClickListener() {
1440
1441 /**
1442 * Implements OnItemClickListener
1443 */
1444 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Amith Yamasani968ec932010-12-02 14:00:47 -08001445 if (DBG) Log.d(LOG_TAG, "onItemClick() position " + position);
1446 onItemClicked(position, KeyEvent.KEYCODE_UNKNOWN, null);
Amith Yamasani733cbd52010-09-03 12:21:39 -07001447 }
1448 };
1449
1450 private final OnItemSelectedListener mOnItemSelectedListener = new OnItemSelectedListener() {
1451
1452 /**
1453 * Implements OnItemSelectedListener
1454 */
1455 public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
Amith Yamasani968ec932010-12-02 14:00:47 -08001456 if (DBG) Log.d(LOG_TAG, "onItemSelected() position " + position);
1457 SearchView.this.onItemSelected(position);
Amith Yamasani733cbd52010-09-03 12:21:39 -07001458 }
1459
1460 /**
1461 * Implements OnItemSelectedListener
1462 */
1463 public void onNothingSelected(AdapterView<?> parent) {
1464 if (DBG)
1465 Log.d(LOG_TAG, "onNothingSelected()");
1466 }
1467 };
1468
1469 /**
1470 * Query rewriting.
1471 */
1472 private void rewriteQueryFromSuggestion(int position) {
Alan Viverettecb8ed372014-11-18 17:05:35 -08001473 CharSequence oldQuery = mSearchSrcTextView.getText();
Amith Yamasani733cbd52010-09-03 12:21:39 -07001474 Cursor c = mSuggestionsAdapter.getCursor();
1475 if (c == null) {
1476 return;
1477 }
1478 if (c.moveToPosition(position)) {
1479 // Get the new query from the suggestion.
1480 CharSequence newQuery = mSuggestionsAdapter.convertToString(c);
1481 if (newQuery != null) {
1482 // The suggestion rewrites the query.
1483 // Update the text field, without getting new suggestions.
1484 setQuery(newQuery);
1485 } else {
1486 // The suggestion does not rewrite the query, restore the user's query.
1487 setQuery(oldQuery);
1488 }
1489 } else {
1490 // We got a bad position, restore the user's query.
1491 setQuery(oldQuery);
1492 }
1493 }
1494
1495 /**
1496 * Launches an intent based on a suggestion.
1497 *
1498 * @param position The index of the suggestion to create the intent from.
1499 * @param actionKey The key code of the action key that was pressed,
1500 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
1501 * @param actionMsg The message for the action key that was pressed,
1502 * or <code>null</code> if none.
1503 * @return true if a successful launch, false if could not (e.g. bad position).
1504 */
1505 private boolean launchSuggestion(int position, int actionKey, String actionMsg) {
1506 Cursor c = mSuggestionsAdapter.getCursor();
1507 if ((c != null) && c.moveToPosition(position)) {
1508
1509 Intent intent = createIntentFromSuggestion(c, actionKey, actionMsg);
1510
1511 // launch the intent
1512 launchIntent(intent);
1513
1514 return true;
1515 }
1516 return false;
1517 }
1518
1519 /**
1520 * Launches an intent, including any special intent handling.
1521 */
1522 private void launchIntent(Intent intent) {
1523 if (intent == null) {
1524 return;
1525 }
1526 try {
1527 // If the intent was created from a suggestion, it will always have an explicit
1528 // component here.
1529 getContext().startActivity(intent);
1530 } catch (RuntimeException ex) {
1531 Log.e(LOG_TAG, "Failed launch activity: " + intent, ex);
1532 }
1533 }
1534
1535 /**
1536 * Sets the text in the query box, without updating the suggestions.
1537 */
1538 private void setQuery(CharSequence query) {
Alan Viverettecb8ed372014-11-18 17:05:35 -08001539 mSearchSrcTextView.setText(query, true);
Amith Yamasanie678f462010-09-15 16:13:43 -07001540 // Move the cursor to the end
Alan Viverettecb8ed372014-11-18 17:05:35 -08001541 mSearchSrcTextView.setSelection(TextUtils.isEmpty(query) ? 0 : query.length());
Amith Yamasani733cbd52010-09-03 12:21:39 -07001542 }
1543
1544 private void launchQuerySearch(int actionKey, String actionMsg, String query) {
1545 String action = Intent.ACTION_SEARCH;
Amith Yamasanie678f462010-09-15 16:13:43 -07001546 Intent intent = createIntent(action, null, null, query, actionKey, actionMsg);
Amith Yamasani733cbd52010-09-03 12:21:39 -07001547 getContext().startActivity(intent);
1548 }
1549
1550 /**
1551 * Constructs an intent from the given information and the search dialog state.
1552 *
1553 * @param action Intent action.
1554 * @param data Intent data, or <code>null</code>.
1555 * @param extraData Data for {@link SearchManager#EXTRA_DATA_KEY} or <code>null</code>.
1556 * @param query Intent query, or <code>null</code>.
Amith Yamasani733cbd52010-09-03 12:21:39 -07001557 * @param actionKey The key code of the action key that was pressed,
1558 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
1559 * @param actionMsg The message for the action key that was pressed,
1560 * or <code>null</code> if none.
1561 * @param mode The search mode, one of the acceptable values for
1562 * {@link SearchManager#SEARCH_MODE}, or {@code null}.
1563 * @return The intent.
1564 */
1565 private Intent createIntent(String action, Uri data, String extraData, String query,
Amith Yamasanie678f462010-09-15 16:13:43 -07001566 int actionKey, String actionMsg) {
Amith Yamasani733cbd52010-09-03 12:21:39 -07001567 // Now build the Intent
1568 Intent intent = new Intent(action);
1569 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
1570 // We need CLEAR_TOP to avoid reusing an old task that has other activities
1571 // on top of the one we want. We don't want to do this in in-app search though,
1572 // as it can be destructive to the activity stack.
1573 if (data != null) {
1574 intent.setData(data);
1575 }
Amith Yamasani068d73c2011-05-27 15:15:14 -07001576 intent.putExtra(SearchManager.USER_QUERY, mUserQuery);
Amith Yamasani733cbd52010-09-03 12:21:39 -07001577 if (query != null) {
1578 intent.putExtra(SearchManager.QUERY, query);
1579 }
1580 if (extraData != null) {
1581 intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData);
1582 }
Amith Yamasani940ef382011-03-02 18:43:23 -08001583 if (mAppSearchData != null) {
1584 intent.putExtra(SearchManager.APP_DATA, mAppSearchData);
1585 }
Amith Yamasani733cbd52010-09-03 12:21:39 -07001586 if (actionKey != KeyEvent.KEYCODE_UNKNOWN) {
1587 intent.putExtra(SearchManager.ACTION_KEY, actionKey);
1588 intent.putExtra(SearchManager.ACTION_MSG, actionMsg);
1589 }
1590 intent.setComponent(mSearchable.getSearchActivity());
1591 return intent;
1592 }
1593
1594 /**
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001595 * Create and return an Intent that can launch the voice search activity for web search.
1596 */
1597 private Intent createVoiceWebSearchIntent(Intent baseIntent, SearchableInfo searchable) {
1598 Intent voiceIntent = new Intent(baseIntent);
1599 ComponentName searchActivity = searchable.getSearchActivity();
1600 voiceIntent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, searchActivity == null ? null
1601 : searchActivity.flattenToShortString());
1602 return voiceIntent;
1603 }
1604
1605 /**
1606 * Create and return an Intent that can launch the voice search activity, perform a specific
1607 * voice transcription, and forward the results to the searchable activity.
1608 *
1609 * @param baseIntent The voice app search intent to start from
1610 * @return A completely-configured intent ready to send to the voice search activity
1611 */
1612 private Intent createVoiceAppSearchIntent(Intent baseIntent, SearchableInfo searchable) {
1613 ComponentName searchActivity = searchable.getSearchActivity();
1614
1615 // create the necessary intent to set up a search-and-forward operation
1616 // in the voice search system. We have to keep the bundle separate,
1617 // because it becomes immutable once it enters the PendingIntent
1618 Intent queryIntent = new Intent(Intent.ACTION_SEARCH);
1619 queryIntent.setComponent(searchActivity);
1620 PendingIntent pending = PendingIntent.getActivity(getContext(), 0, queryIntent,
1621 PendingIntent.FLAG_ONE_SHOT);
1622
1623 // Now set up the bundle that will be inserted into the pending intent
1624 // when it's time to do the search. We always build it here (even if empty)
1625 // because the voice search activity will always need to insert "QUERY" into
1626 // it anyway.
1627 Bundle queryExtras = new Bundle();
Jorge Ruesga1bcfe842012-09-03 01:26:59 +02001628 if (mAppSearchData != null) {
1629 queryExtras.putParcelable(SearchManager.APP_DATA, mAppSearchData);
1630 }
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001631
1632 // Now build the intent to launch the voice search. Add all necessary
1633 // extras to launch the voice recognizer, and then all the necessary extras
1634 // to forward the results to the searchable activity
1635 Intent voiceIntent = new Intent(baseIntent);
1636
1637 // Add all of the configuration options supplied by the searchable's metadata
1638 String languageModel = RecognizerIntent.LANGUAGE_MODEL_FREE_FORM;
1639 String prompt = null;
1640 String language = null;
1641 int maxResults = 1;
1642
1643 Resources resources = getResources();
1644 if (searchable.getVoiceLanguageModeId() != 0) {
1645 languageModel = resources.getString(searchable.getVoiceLanguageModeId());
1646 }
1647 if (searchable.getVoicePromptTextId() != 0) {
1648 prompt = resources.getString(searchable.getVoicePromptTextId());
1649 }
1650 if (searchable.getVoiceLanguageId() != 0) {
1651 language = resources.getString(searchable.getVoiceLanguageId());
1652 }
1653 if (searchable.getVoiceMaxResults() != 0) {
1654 maxResults = searchable.getVoiceMaxResults();
1655 }
1656 voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, languageModel);
1657 voiceIntent.putExtra(RecognizerIntent.EXTRA_PROMPT, prompt);
1658 voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, language);
1659 voiceIntent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, maxResults);
1660 voiceIntent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, searchActivity == null ? null
1661 : searchActivity.flattenToShortString());
1662
1663 // Add the values that configure forwarding the results
1664 voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT, pending);
1665 voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT_BUNDLE, queryExtras);
1666
1667 return voiceIntent;
1668 }
1669
1670 /**
Amith Yamasani733cbd52010-09-03 12:21:39 -07001671 * When a particular suggestion has been selected, perform the various lookups required
1672 * to use the suggestion. This includes checking the cursor for suggestion-specific data,
1673 * and/or falling back to the XML for defaults; It also creates REST style Uri data when
1674 * the suggestion includes a data id.
1675 *
1676 * @param c The suggestions cursor, moved to the row of the user's selection
1677 * @param actionKey The key code of the action key that was pressed,
1678 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
1679 * @param actionMsg The message for the action key that was pressed,
1680 * or <code>null</code> if none.
1681 * @return An intent for the suggestion at the cursor's position.
1682 */
1683 private Intent createIntentFromSuggestion(Cursor c, int actionKey, String actionMsg) {
1684 try {
1685 // use specific action if supplied, or default action if supplied, or fixed default
1686 String action = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_ACTION);
1687
Amith Yamasani733cbd52010-09-03 12:21:39 -07001688 if (action == null) {
1689 action = mSearchable.getSuggestIntentAction();
1690 }
1691 if (action == null) {
1692 action = Intent.ACTION_SEARCH;
1693 }
1694
1695 // use specific data if supplied, or default data if supplied
1696 String data = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA);
1697 if (data == null) {
1698 data = mSearchable.getSuggestIntentData();
1699 }
1700 // then, if an ID was provided, append it.
1701 if (data != null) {
1702 String id = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID);
1703 if (id != null) {
1704 data = data + "/" + Uri.encode(id);
1705 }
1706 }
1707 Uri dataUri = (data == null) ? null : Uri.parse(data);
1708
Amith Yamasani733cbd52010-09-03 12:21:39 -07001709 String query = getColumnString(c, SearchManager.SUGGEST_COLUMN_QUERY);
1710 String extraData = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA);
1711
Amith Yamasanie678f462010-09-15 16:13:43 -07001712 return createIntent(action, dataUri, extraData, query, actionKey, actionMsg);
Amith Yamasani733cbd52010-09-03 12:21:39 -07001713 } catch (RuntimeException e ) {
1714 int rowNum;
1715 try { // be really paranoid now
1716 rowNum = c.getPosition();
1717 } catch (RuntimeException e2 ) {
1718 rowNum = -1;
1719 }
Jake Wharton73af4512012-07-06 23:15:44 -07001720 Log.w(LOG_TAG, "Search suggestions cursor at row " + rowNum +
1721 " returned exception.", e);
Amith Yamasani733cbd52010-09-03 12:21:39 -07001722 return null;
1723 }
1724 }
1725
Amith Yamasanif28d1872011-07-26 12:21:03 -07001726 private void forceSuggestionQuery() {
Alan Viverettecb8ed372014-11-18 17:05:35 -08001727 mSearchSrcTextView.doBeforeTextChanged();
1728 mSearchSrcTextView.doAfterTextChanged();
Amith Yamasanif28d1872011-07-26 12:21:03 -07001729 }
1730
Amith Yamasani968ec932010-12-02 14:00:47 -08001731 static boolean isLandscapeMode(Context context) {
1732 return context.getResources().getConfiguration().orientation
1733 == Configuration.ORIENTATION_LANDSCAPE;
1734 }
1735
Amith Yamasani733cbd52010-09-03 12:21:39 -07001736 /**
1737 * Callback to watch the text field for empty/non-empty
1738 */
1739 private TextWatcher mTextWatcher = new TextWatcher() {
1740
1741 public void beforeTextChanged(CharSequence s, int start, int before, int after) { }
1742
1743 public void onTextChanged(CharSequence s, int start,
1744 int before, int after) {
1745 SearchView.this.onTextChanged(s);
1746 }
1747
1748 public void afterTextChanged(Editable s) {
1749 }
1750 };
Amith Yamasani968ec932010-12-02 14:00:47 -08001751
1752 /**
1753 * Local subclass for AutoCompleteTextView.
1754 * @hide
1755 */
1756 public static class SearchAutoComplete extends AutoCompleteTextView {
1757
1758 private int mThreshold;
1759 private SearchView mSearchView;
1760
1761 public SearchAutoComplete(Context context) {
1762 super(context);
1763 mThreshold = getThreshold();
1764 }
1765
1766 public SearchAutoComplete(Context context, AttributeSet attrs) {
1767 super(context, attrs);
1768 mThreshold = getThreshold();
1769 }
1770
Alan Viverette617feb92013-09-09 18:09:13 -07001771 public SearchAutoComplete(Context context, AttributeSet attrs, int defStyleAttrs) {
1772 super(context, attrs, defStyleAttrs);
1773 mThreshold = getThreshold();
1774 }
1775
1776 public SearchAutoComplete(
1777 Context context, AttributeSet attrs, int defStyleAttrs, int defStyleRes) {
1778 super(context, attrs, defStyleAttrs, defStyleRes);
Amith Yamasani968ec932010-12-02 14:00:47 -08001779 mThreshold = getThreshold();
1780 }
1781
Filip Gruszczynskib635fda2015-12-03 18:37:38 -08001782 @Override
1783 protected void onFinishInflate() {
1784 super.onFinishInflate();
1785 DisplayMetrics metrics = getResources().getDisplayMetrics();
1786 setMinWidth((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
1787 getSearchViewTextMinWidthDp(), metrics));
1788 }
1789
Amith Yamasani968ec932010-12-02 14:00:47 -08001790 void setSearchView(SearchView searchView) {
1791 mSearchView = searchView;
1792 }
1793
1794 @Override
1795 public void setThreshold(int threshold) {
1796 super.setThreshold(threshold);
1797 mThreshold = threshold;
1798 }
1799
1800 /**
1801 * Returns true if the text field is empty, or contains only whitespace.
1802 */
1803 private boolean isEmpty() {
1804 return TextUtils.getTrimmedLength(getText()) == 0;
1805 }
1806
1807 /**
1808 * We override this method to avoid replacing the query box text when a
1809 * suggestion is clicked.
1810 */
1811 @Override
1812 protected void replaceText(CharSequence text) {
1813 }
1814
1815 /**
1816 * We override this method to avoid an extra onItemClick being called on
1817 * the drop-down's OnItemClickListener by
1818 * {@link AutoCompleteTextView#onKeyUp(int, KeyEvent)} when an item is
1819 * clicked with the trackball.
1820 */
1821 @Override
1822 public void performCompletion() {
1823 }
1824
1825 /**
1826 * We override this method to be sure and show the soft keyboard if
1827 * appropriate when the TextView has focus.
1828 */
1829 @Override
1830 public void onWindowFocusChanged(boolean hasWindowFocus) {
1831 super.onWindowFocusChanged(hasWindowFocus);
1832
Amith Yamasaniacd8d2d2010-12-06 15:50:23 -08001833 if (hasWindowFocus && mSearchView.hasFocus() && getVisibility() == VISIBLE) {
Yohei Yukawa777ef952015-11-25 20:32:24 -08001834 InputMethodManager inputManager =
1835 getContext().getSystemService(InputMethodManager.class);
Amith Yamasani968ec932010-12-02 14:00:47 -08001836 inputManager.showSoftInput(this, 0);
1837 // If in landscape mode, then make sure that
1838 // the ime is in front of the dropdown.
1839 if (isLandscapeMode(getContext())) {
1840 ensureImeVisible(true);
1841 }
1842 }
1843 }
1844
Amith Yamasani4aedb392010-12-15 16:04:57 -08001845 @Override
1846 protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
1847 super.onFocusChanged(focused, direction, previouslyFocusedRect);
1848 mSearchView.onTextFocusChanged();
1849 }
1850
Amith Yamasani968ec932010-12-02 14:00:47 -08001851 /**
1852 * We override this method so that we can allow a threshold of zero,
1853 * which ACTV does not.
1854 */
1855 @Override
1856 public boolean enoughToFilter() {
1857 return mThreshold <= 0 || super.enoughToFilter();
1858 }
Amith Yamasanib4569fb2011-07-08 15:25:39 -07001859
1860 @Override
1861 public boolean onKeyPreIme(int keyCode, KeyEvent event) {
1862 if (keyCode == KeyEvent.KEYCODE_BACK) {
1863 // special case for the back key, we do not even try to send it
1864 // to the drop down list but instead, consume it immediately
1865 if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
1866 KeyEvent.DispatcherState state = getKeyDispatcherState();
1867 if (state != null) {
1868 state.startTracking(event, this);
1869 }
1870 return true;
1871 } else if (event.getAction() == KeyEvent.ACTION_UP) {
1872 KeyEvent.DispatcherState state = getKeyDispatcherState();
1873 if (state != null) {
1874 state.handleUpEvent(event);
1875 }
1876 if (event.isTracking() && !event.isCanceled()) {
1877 mSearchView.clearFocus();
1878 mSearchView.setImeVisibility(false);
1879 return true;
1880 }
1881 }
1882 }
1883 return super.onKeyPreIme(keyCode, event);
1884 }
1885
Filip Gruszczynskib635fda2015-12-03 18:37:38 -08001886 /**
1887 * Get minimum width of the search view text entry area.
1888 */
1889 private int getSearchViewTextMinWidthDp() {
1890 final Configuration configuration = getResources().getConfiguration();
1891 final int width = configuration.screenWidthDp;
1892 final int height = configuration.screenHeightDp;
1893 final int orientation = configuration.orientation;
1894 if (width >= 960 && height >= 720
1895 && orientation == Configuration.ORIENTATION_LANDSCAPE) {
1896 return 256;
1897 } else if (width >= 600 || (width >= 640 && height >= 480)) {
1898 return 192;
1899 };
1900 return 160;
1901 }
Amith Yamasani968ec932010-12-02 14:00:47 -08001902 }
Amith Yamasani05944762010-10-08 13:52:38 -07001903}