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