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