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 | |
| 21 | import com.android.internal.R; |
| 22 | |
Amith Yamasani | ebcf5a3a | 2010-10-13 11:35:24 -0700 | [diff] [blame] | 23 | import android.app.PendingIntent; |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 24 | import android.app.SearchManager; |
| 25 | import android.app.SearchableInfo; |
Amith Yamasani | ebcf5a3a | 2010-10-13 11:35:24 -0700 | [diff] [blame] | 26 | import android.content.ActivityNotFoundException; |
| 27 | import android.content.ComponentName; |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 28 | import android.content.Context; |
| 29 | import android.content.Intent; |
Amith Yamasani | ebcf5a3a | 2010-10-13 11:35:24 -0700 | [diff] [blame] | 30 | import android.content.pm.PackageManager; |
| 31 | import android.content.pm.ResolveInfo; |
| 32 | import android.content.res.Resources; |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 33 | import android.content.res.TypedArray; |
| 34 | import android.database.Cursor; |
repo sync | 6a81b82 | 2010-09-28 13:00:05 -0700 | [diff] [blame] | 35 | import android.graphics.Rect; |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 36 | import android.graphics.drawable.Drawable; |
| 37 | import android.net.Uri; |
Amith Yamasani | ebcf5a3a | 2010-10-13 11:35:24 -0700 | [diff] [blame] | 38 | import android.os.Bundle; |
| 39 | import android.speech.RecognizerIntent; |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 40 | import android.text.Editable; |
| 41 | import android.text.TextUtils; |
| 42 | import android.text.TextWatcher; |
| 43 | import android.util.AttributeSet; |
| 44 | import android.util.Log; |
| 45 | import android.view.KeyEvent; |
| 46 | import android.view.LayoutInflater; |
| 47 | import android.view.View; |
| 48 | import android.view.inputmethod.InputMethodManager; |
| 49 | import android.widget.AdapterView.OnItemClickListener; |
| 50 | import android.widget.AdapterView.OnItemSelectedListener; |
| 51 | import android.widget.TextView.OnEditorActionListener; |
| 52 | |
| 53 | import java.util.WeakHashMap; |
| 54 | |
| 55 | /** |
| 56 | * Provides the user interface elements for the user to enter a search query and submit a |
| 57 | * request to a search provider. Shows a list of query suggestions or results, if |
| 58 | * available and allows the user to pick a suggestion or result to launch into. |
Amith Yamasani | 5931b1f | 2010-10-18 16:13:14 -0700 | [diff] [blame] | 59 | * |
| 60 | * <p> |
| 61 | * <b>XML attributes</b> |
| 62 | * <p> |
| 63 | * See {@link android.R.styleable#SearchView SearchView Attributes}, |
| 64 | * {@link android.R.styleable#View View Attributes} |
| 65 | * |
| 66 | * @attr ref android.R.styleable#SearchView_iconifiedByDefault |
| 67 | * @attr ref android.R.styleable#SearchView_maxWidth |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 68 | */ |
| 69 | public class SearchView extends LinearLayout { |
| 70 | |
| 71 | private static final boolean DBG = false; |
| 72 | private static final String LOG_TAG = "SearchView"; |
| 73 | |
| 74 | private OnQueryChangeListener mOnQueryChangeListener; |
| 75 | private OnCloseListener mOnCloseListener; |
Amith Yamasani | 0594476 | 2010-10-08 13:52:38 -0700 | [diff] [blame] | 76 | private OnFocusChangeListener mOnQueryTextFocusChangeListener; |
| 77 | private OnSuggestionSelectionListener mOnSuggestionListener; |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 78 | |
| 79 | private boolean mIconifiedByDefault; |
Amith Yamasani | 9322775 | 2010-09-14 10:10:54 -0700 | [diff] [blame] | 80 | private boolean mIconified; |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 81 | private CursorAdapter mSuggestionsAdapter; |
| 82 | private View mSearchButton; |
| 83 | private View mSubmitButton; |
| 84 | private View mCloseButton; |
| 85 | private View mSearchEditFrame; |
Amith Yamasani | ebcf5a3a | 2010-10-13 11:35:24 -0700 | [diff] [blame] | 86 | private View mVoiceButton; |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 87 | private AutoCompleteTextView mQueryTextView; |
| 88 | private boolean mSubmitButtonEnabled; |
| 89 | private CharSequence mQueryHint; |
Amith Yamasani | e678f46 | 2010-09-15 16:13:43 -0700 | [diff] [blame] | 90 | private boolean mQueryRefinement; |
Amith Yamasani | 0594476 | 2010-10-08 13:52:38 -0700 | [diff] [blame] | 91 | private boolean mClearingFocus; |
Amith Yamasani | 5931b1f | 2010-10-18 16:13:14 -0700 | [diff] [blame] | 92 | private int mMaxWidth; |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 93 | |
| 94 | private SearchableInfo mSearchable; |
| 95 | |
Amith Yamasani | ebcf5a3a | 2010-10-13 11:35:24 -0700 | [diff] [blame] | 96 | // For voice searching |
| 97 | private final Intent mVoiceWebSearchIntent; |
| 98 | private final Intent mVoiceAppSearchIntent; |
| 99 | |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 100 | // A weak map of drawables we've gotten from other packages, so we don't load them |
| 101 | // more than once. |
| 102 | private final WeakHashMap<String, Drawable.ConstantState> mOutsideDrawablesCache = |
| 103 | new WeakHashMap<String, Drawable.ConstantState>(); |
| 104 | |
| 105 | /** |
| 106 | * Callbacks for changes to the query text. |
| 107 | */ |
| 108 | public interface OnQueryChangeListener { |
| 109 | |
| 110 | /** |
| 111 | * Called when the user submits the query. This could be due to a key press on the |
| 112 | * keyboard or due to pressing a submit button. |
| 113 | * The listener can override the standard behavior by returning true |
| 114 | * to indicate that it has handled the submit request. Otherwise return false to |
| 115 | * let the SearchView handle the submission by launching any associated intent. |
| 116 | * |
| 117 | * @param query the query text that is to be submitted |
| 118 | * |
| 119 | * @return true if the query has been handled by the listener, false to let the |
| 120 | * SearchView perform the default action. |
| 121 | */ |
| 122 | boolean onSubmitQuery(String query); |
| 123 | |
| 124 | /** |
| 125 | * Called when the query text is changed by the user. |
| 126 | * |
| 127 | * @param newText the new content of the query text field. |
| 128 | * |
| 129 | * @return false if the SearchView should perform the default action of showing any |
| 130 | * suggestions if available, true if the action was handled by the listener. |
| 131 | */ |
| 132 | boolean onQueryTextChanged(String newText); |
| 133 | } |
| 134 | |
| 135 | public interface OnCloseListener { |
| 136 | |
| 137 | /** |
| 138 | * The user is attempting to close the SearchView. |
| 139 | * |
| 140 | * @return true if the listener wants to override the default behavior of clearing the |
| 141 | * text field and dismissing it, false otherwise. |
| 142 | */ |
| 143 | boolean onClose(); |
| 144 | } |
| 145 | |
Amith Yamasani | 0594476 | 2010-10-08 13:52:38 -0700 | [diff] [blame] | 146 | /** |
| 147 | * Callback interface for selection events on suggestions. These callbacks |
| 148 | * are only relevant when a SearchableInfo has been specified by {@link #setSearchableInfo}. |
| 149 | */ |
| 150 | public interface OnSuggestionSelectionListener { |
| 151 | |
| 152 | /** |
| 153 | * Called when a suggestion was selected by navigating to it. |
| 154 | * @param position the absolute position in the list of suggestions. |
| 155 | * |
| 156 | * @return true if the listener handles the event and wants to override the default |
| 157 | * behavior of possibly rewriting the query based on the selected item, false otherwise. |
| 158 | */ |
| 159 | boolean onSuggestionSelected(int position); |
| 160 | |
| 161 | /** |
| 162 | * Called when a suggestion was clicked. |
| 163 | * @param position the absolute position of the clicked item in the list of suggestions. |
| 164 | * |
| 165 | * @return true if the listener handles the event and wants to override the default |
| 166 | * behavior of launching any intent or submitting a search query specified on that item. |
| 167 | * Return false otherwise. |
| 168 | */ |
| 169 | boolean onSuggestionClicked(int position); |
| 170 | } |
| 171 | |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 172 | public SearchView(Context context) { |
| 173 | this(context, null); |
| 174 | } |
| 175 | |
| 176 | public SearchView(Context context, AttributeSet attrs) { |
| 177 | super(context, attrs); |
| 178 | |
| 179 | LayoutInflater inflater = (LayoutInflater) context |
| 180 | .getSystemService(Context.LAYOUT_INFLATER_SERVICE); |
| 181 | inflater.inflate(R.layout.search_view, this, true); |
| 182 | |
| 183 | mSearchButton = findViewById(R.id.search_button); |
| 184 | mQueryTextView = (AutoCompleteTextView) findViewById(R.id.search_src_text); |
| 185 | mSearchEditFrame = findViewById(R.id.search_edit_frame); |
| 186 | mSubmitButton = findViewById(R.id.search_go_btn); |
| 187 | mCloseButton = findViewById(R.id.search_close_btn); |
Amith Yamasani | ebcf5a3a | 2010-10-13 11:35:24 -0700 | [diff] [blame] | 188 | mVoiceButton = findViewById(R.id.search_voice_btn); |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 189 | |
| 190 | mSearchButton.setOnClickListener(mOnClickListener); |
| 191 | mCloseButton.setOnClickListener(mOnClickListener); |
| 192 | mSubmitButton.setOnClickListener(mOnClickListener); |
Amith Yamasani | ebcf5a3a | 2010-10-13 11:35:24 -0700 | [diff] [blame] | 193 | mVoiceButton.setOnClickListener(mOnClickListener); |
| 194 | |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 195 | mQueryTextView.addTextChangedListener(mTextWatcher); |
| 196 | mQueryTextView.setOnEditorActionListener(mOnEditorActionListener); |
| 197 | mQueryTextView.setOnItemClickListener(mOnItemClickListener); |
| 198 | mQueryTextView.setOnItemSelectedListener(mOnItemSelectedListener); |
Amith Yamasani | 0594476 | 2010-10-08 13:52:38 -0700 | [diff] [blame] | 199 | // Inform any listener of focus changes |
| 200 | mQueryTextView.setOnFocusChangeListener(new OnFocusChangeListener() { |
| 201 | |
| 202 | public void onFocusChange(View v, boolean hasFocus) { |
| 203 | if (mOnQueryTextFocusChangeListener != null) { |
| 204 | mOnQueryTextFocusChangeListener.onFocusChange(SearchView.this, hasFocus); |
| 205 | } |
| 206 | } |
| 207 | }); |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 208 | |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 209 | TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SearchView, 0, 0); |
| 210 | setIconifiedByDefault(a.getBoolean(R.styleable.SearchView_iconifiedByDefault, true)); |
Amith Yamasani | 5931b1f | 2010-10-18 16:13:14 -0700 | [diff] [blame] | 211 | int maxWidth = a.getDimensionPixelSize(R.styleable.SearchView_maxWidth, -1); |
| 212 | if (maxWidth != -1) { |
| 213 | setMaxWidth(maxWidth); |
| 214 | } |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 215 | a.recycle(); |
| 216 | |
Amith Yamasani | ebcf5a3a | 2010-10-13 11:35:24 -0700 | [diff] [blame] | 217 | // Save voice intent for later queries/launching |
| 218 | mVoiceWebSearchIntent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH); |
| 219 | mVoiceWebSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); |
| 220 | mVoiceWebSearchIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, |
| 221 | RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH); |
| 222 | |
| 223 | mVoiceAppSearchIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); |
| 224 | mVoiceAppSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); |
| 225 | |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 226 | updateViewsVisibility(mIconifiedByDefault); |
| 227 | } |
| 228 | |
| 229 | /** |
| 230 | * Sets the SearchableInfo for this SearchView. Properties in the SearchableInfo are used |
| 231 | * to display labels, hints, suggestions, create intents for launching search results screens |
| 232 | * and controlling other affordances such as a voice button. |
| 233 | * |
| 234 | * @param searchable a SearchableInfo can be retrieved from the SearchManager, for a specific |
| 235 | * activity or a global search provider. |
| 236 | */ |
| 237 | public void setSearchableInfo(SearchableInfo searchable) { |
| 238 | mSearchable = searchable; |
| 239 | if (mSearchable != null) { |
| 240 | updateSearchAutoComplete(); |
| 241 | } |
| 242 | updateViewsVisibility(mIconifiedByDefault); |
| 243 | } |
| 244 | |
Amith Yamasani | 0594476 | 2010-10-08 13:52:38 -0700 | [diff] [blame] | 245 | /** @hide */ |
| 246 | @Override |
| 247 | public boolean requestFocus(int direction, Rect previouslyFocusedRect) { |
Amith Yamasani | ebcf5a3a | 2010-10-13 11:35:24 -0700 | [diff] [blame] | 248 | if (mClearingFocus || isIconified()) return false; |
| 249 | return mQueryTextView.requestFocus(direction, previouslyFocusedRect); |
Amith Yamasani | 0594476 | 2010-10-08 13:52:38 -0700 | [diff] [blame] | 250 | } |
| 251 | |
| 252 | /** @hide */ |
| 253 | @Override |
| 254 | public void clearFocus() { |
| 255 | mClearingFocus = true; |
| 256 | super.clearFocus(); |
| 257 | mQueryTextView.clearFocus(); |
| 258 | setImeVisibility(false); |
| 259 | mClearingFocus = false; |
| 260 | } |
| 261 | |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 262 | /** |
| 263 | * Sets a listener for user actions within the SearchView. |
| 264 | * |
| 265 | * @param listener the listener object that receives callbacks when the user performs |
| 266 | * actions in the SearchView such as clicking on buttons or typing a query. |
| 267 | */ |
| 268 | public void setOnQueryChangeListener(OnQueryChangeListener listener) { |
| 269 | mOnQueryChangeListener = listener; |
| 270 | } |
| 271 | |
| 272 | /** |
Amith Yamasani | 9322775 | 2010-09-14 10:10:54 -0700 | [diff] [blame] | 273 | * Sets a listener to inform when the user closes the SearchView. |
| 274 | * |
| 275 | * @param listener the listener to call when the user closes the SearchView. |
| 276 | */ |
| 277 | public void setOnCloseListener(OnCloseListener listener) { |
| 278 | mOnCloseListener = listener; |
| 279 | } |
| 280 | |
| 281 | /** |
Amith Yamasani | 0594476 | 2010-10-08 13:52:38 -0700 | [diff] [blame] | 282 | * Sets a listener to inform when the focus of the query text field changes. |
| 283 | * |
| 284 | * @param listener the listener to inform of focus changes. |
| 285 | */ |
| 286 | public void setOnQueryTextFocusChangeListener(OnFocusChangeListener listener) { |
| 287 | mOnQueryTextFocusChangeListener = listener; |
| 288 | } |
| 289 | |
| 290 | /** |
| 291 | * Sets a listener to inform when a suggestion is focused or clicked. |
| 292 | * |
| 293 | * @param listener the listener to inform of suggestion selection events. |
| 294 | */ |
| 295 | public void setOnSuggestionSelectionListener(OnSuggestionSelectionListener listener) { |
| 296 | mOnSuggestionListener = listener; |
| 297 | } |
| 298 | |
| 299 | /** |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 300 | * Sets a query string in the text field and optionally submits the query as well. |
| 301 | * |
| 302 | * @param query the query string. This replaces any query text already present in the |
| 303 | * text field. |
| 304 | * @param submit whether to submit the query right now or only update the contents of |
| 305 | * text field. |
| 306 | */ |
| 307 | public void setQuery(CharSequence query, boolean submit) { |
| 308 | mQueryTextView.setText(query); |
| 309 | // If the query is not empty and submit is requested, submit the query |
| 310 | if (submit && !TextUtils.isEmpty(query)) { |
| 311 | onSubmitQuery(); |
| 312 | } |
| 313 | } |
| 314 | |
| 315 | /** |
| 316 | * Sets the hint text to display in the query text field. This overrides any hint specified |
| 317 | * in the SearchableInfo. |
| 318 | * |
| 319 | * @param hint the hint text to display |
| 320 | */ |
| 321 | public void setQueryHint(CharSequence hint) { |
| 322 | mQueryHint = hint; |
| 323 | updateQueryHint(); |
| 324 | } |
| 325 | |
| 326 | /** |
| 327 | * Sets the default or resting state of the search field. If true, a single search icon is |
| 328 | * shown by default and expands to show the text field and other buttons when pressed. Also, |
| 329 | * 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] | 330 | * is pressed. Changes to this property will take effect immediately. |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 331 | * |
Amith Yamasani | 9322775 | 2010-09-14 10:10:54 -0700 | [diff] [blame] | 332 | * <p>The default value is false.</p> |
| 333 | * |
| 334 | * @param iconified whether the search field should be iconified by default |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 335 | */ |
| 336 | public void setIconifiedByDefault(boolean iconified) { |
Amith Yamasani | ebcf5a3a | 2010-10-13 11:35:24 -0700 | [diff] [blame] | 337 | if (mIconifiedByDefault == iconified) return; |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 338 | mIconifiedByDefault = iconified; |
| 339 | updateViewsVisibility(iconified); |
Amith Yamasani | 0594476 | 2010-10-08 13:52:38 -0700 | [diff] [blame] | 340 | setImeVisibility(!iconified); |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 341 | } |
| 342 | |
Amith Yamasani | 9322775 | 2010-09-14 10:10:54 -0700 | [diff] [blame] | 343 | /** |
| 344 | * Returns the default iconified state of the search field. |
| 345 | * @return |
| 346 | */ |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 347 | public boolean isIconfiedByDefault() { |
| 348 | return mIconifiedByDefault; |
| 349 | } |
| 350 | |
| 351 | /** |
Amith Yamasani | 9322775 | 2010-09-14 10:10:54 -0700 | [diff] [blame] | 352 | * Iconifies or expands the SearchView. Any query text is cleared when iconified. This is |
| 353 | * a temporary state and does not override the default iconified state set by |
| 354 | * {@link #setIconifiedByDefault(boolean)}. If the default state is iconified, then |
| 355 | * a false here will only be valid until the user closes the field. And if the default |
| 356 | * state is expanded, then a true here will only clear the text field and not close it. |
| 357 | * |
| 358 | * @param iconify a true value will collapse the SearchView to an icon, while a false will |
| 359 | * expand it. |
| 360 | */ |
| 361 | public void setIconified(boolean iconify) { |
| 362 | if (iconify) { |
| 363 | onCloseClicked(); |
| 364 | } else { |
| 365 | onSearchClicked(); |
| 366 | } |
| 367 | } |
| 368 | |
| 369 | /** |
| 370 | * Returns the current iconified state of the SearchView. |
| 371 | * |
| 372 | * @return true if the SearchView is currently iconified, false if the search field is |
| 373 | * fully visible. |
| 374 | */ |
| 375 | public boolean isIconified() { |
| 376 | return mIconified; |
| 377 | } |
| 378 | |
| 379 | /** |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 380 | * Enables showing a submit button when the query is non-empty. In cases where the SearchView |
| 381 | * is being used to filter the contents of the current activity and doesn't launch a separate |
| 382 | * results activity, then the submit button should be disabled. |
| 383 | * |
| 384 | * @param enabled true to show a submit button for submitting queries, false if a submit |
| 385 | * button is not required. |
| 386 | */ |
| 387 | public void setSubmitButtonEnabled(boolean enabled) { |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 388 | mSubmitButtonEnabled = enabled; |
Amith Yamasani | ebcf5a3a | 2010-10-13 11:35:24 -0700 | [diff] [blame] | 389 | updateViewsVisibility(isIconified()); |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 390 | } |
| 391 | |
| 392 | /** |
| 393 | * Returns whether the submit button is enabled when necessary or never displayed. |
| 394 | * |
| 395 | * @return whether the submit button is enabled automatically when necessary |
| 396 | */ |
| 397 | public boolean isSubmitButtonEnabled() { |
| 398 | return mSubmitButtonEnabled; |
| 399 | } |
| 400 | |
Amith Yamasani | e678f46 | 2010-09-15 16:13:43 -0700 | [diff] [blame] | 401 | /** |
| 402 | * Specifies if a query refinement button should be displayed alongside each suggestion |
| 403 | * or if it should depend on the flags set in the individual items retrieved from the |
| 404 | * suggestions provider. Clicking on the query refinement button will replace the text |
| 405 | * in the query text field with the text from the suggestion. This flag only takes effect |
| 406 | * if a SearchableInfo has been specified with {@link #setSearchableInfo(SearchableInfo)} |
| 407 | * and not when using a custom adapter. |
| 408 | * |
| 409 | * @param enable true if all items should have a query refinement button, false if only |
| 410 | * those items that have a query refinement flag set should have the button. |
| 411 | * |
| 412 | * @see SearchManager#SUGGEST_COLUMN_FLAGS |
| 413 | * @see SearchManager#FLAG_QUERY_REFINEMENT |
| 414 | */ |
| 415 | public void setQueryRefinementEnabled(boolean enable) { |
| 416 | mQueryRefinement = enable; |
| 417 | if (mSuggestionsAdapter instanceof SuggestionsAdapter) { |
| 418 | ((SuggestionsAdapter) mSuggestionsAdapter).setQueryRefinement( |
| 419 | enable ? SuggestionsAdapter.REFINE_ALL : SuggestionsAdapter.REFINE_BY_ENTRY); |
| 420 | } |
| 421 | } |
| 422 | |
| 423 | /** |
| 424 | * Returns whether query refinement is enabled for all items or only specific ones. |
| 425 | * @return true if enabled for all items, false otherwise. |
| 426 | */ |
| 427 | public boolean isQueryRefinementEnabled() { |
| 428 | return mQueryRefinement; |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 429 | } |
| 430 | |
| 431 | /** |
| 432 | * You can set a custom adapter if you wish. Otherwise the default adapter is used to |
| 433 | * display the suggestions from the suggestions provider associated with the SearchableInfo. |
| 434 | * |
| 435 | * @see #setSearchableInfo(SearchableInfo) |
| 436 | */ |
| 437 | public void setSuggestionsAdapter(CursorAdapter adapter) { |
| 438 | mSuggestionsAdapter = adapter; |
| 439 | |
| 440 | mQueryTextView.setAdapter(mSuggestionsAdapter); |
| 441 | } |
| 442 | |
| 443 | /** |
| 444 | * Returns the adapter used for suggestions, if any. |
| 445 | * @return the suggestions adapter |
| 446 | */ |
| 447 | public CursorAdapter getSuggestionsAdapter() { |
| 448 | return mSuggestionsAdapter; |
| 449 | } |
| 450 | |
Amith Yamasani | 5931b1f | 2010-10-18 16:13:14 -0700 | [diff] [blame] | 451 | /** |
| 452 | * Makes the view at most this many pixels wide |
| 453 | * |
| 454 | * @attr ref android.R.styleable#SearchView_maxWidth |
| 455 | */ |
| 456 | public void setMaxWidth(int maxpixels) { |
| 457 | mMaxWidth = maxpixels; |
| 458 | |
| 459 | requestLayout(); |
| 460 | } |
| 461 | |
| 462 | @Override |
| 463 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
| 464 | int widthMode = MeasureSpec.getMode(widthMeasureSpec); |
| 465 | int width = MeasureSpec.getSize(widthMeasureSpec); |
| 466 | |
| 467 | if ((widthMode == MeasureSpec.AT_MOST || widthMode == MeasureSpec.EXACTLY) && mMaxWidth > 0 |
| 468 | && width > mMaxWidth) { |
| 469 | super.onMeasure(MeasureSpec.makeMeasureSpec(mMaxWidth, widthMode), heightMeasureSpec); |
| 470 | } else { |
| 471 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); |
| 472 | } |
| 473 | } |
| 474 | |
Amith Yamasani | 9322775 | 2010-09-14 10:10:54 -0700 | [diff] [blame] | 475 | private void updateViewsVisibility(final boolean collapsed) { |
| 476 | mIconified = collapsed; |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 477 | // Visibility of views that are visible when collapsed |
Amith Yamasani | 9322775 | 2010-09-14 10:10:54 -0700 | [diff] [blame] | 478 | final int visCollapsed = collapsed ? VISIBLE : GONE; |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 479 | // Visibility of views that are visible when expanded |
Amith Yamasani | 9322775 | 2010-09-14 10:10:54 -0700 | [diff] [blame] | 480 | final int visExpanded = collapsed ? GONE : VISIBLE; |
Amith Yamasani | 0594476 | 2010-10-08 13:52:38 -0700 | [diff] [blame] | 481 | // Is there text in the query |
| 482 | final boolean hasText = !TextUtils.isEmpty(mQueryTextView.getText()); |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 483 | |
| 484 | mSearchButton.setVisibility(visCollapsed); |
Amith Yamasani | 0594476 | 2010-10-08 13:52:38 -0700 | [diff] [blame] | 485 | mSubmitButton.setVisibility(mSubmitButtonEnabled && hasText ? visExpanded : GONE); |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 486 | mSearchEditFrame.setVisibility(visExpanded); |
Amith Yamasani | ebcf5a3a | 2010-10-13 11:35:24 -0700 | [diff] [blame] | 487 | updateVoiceButton(!hasText); |
Amith Yamasani | 5931b1f | 2010-10-18 16:13:14 -0700 | [diff] [blame] | 488 | requestLayout(); |
| 489 | invalidate(); |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 490 | } |
| 491 | |
| 492 | private void setImeVisibility(boolean visible) { |
Amith Yamasani | 0594476 | 2010-10-08 13:52:38 -0700 | [diff] [blame] | 493 | InputMethodManager imm = (InputMethodManager) |
| 494 | getContext().getSystemService(Context.INPUT_METHOD_SERVICE); |
repo sync | 6a81b82 | 2010-09-28 13:00:05 -0700 | [diff] [blame] | 495 | |
Amith Yamasani | 0594476 | 2010-10-08 13:52:38 -0700 | [diff] [blame] | 496 | // We made sure the IME was displayed, so also make sure it is closed |
| 497 | // when we go away. |
| 498 | if (imm != null) { |
| 499 | if (visible) { |
| 500 | imm.showSoftInputUnchecked(0, null); |
| 501 | } else { |
| 502 | imm.hideSoftInputFromWindow(getWindowToken(), 0); |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 503 | } |
| 504 | } |
| 505 | } |
| 506 | |
Amith Yamasani | e678f46 | 2010-09-15 16:13:43 -0700 | [diff] [blame] | 507 | /** |
| 508 | * Called by the SuggestionsAdapter |
| 509 | * @hide |
| 510 | */ |
| 511 | /* package */void onQueryRefine(CharSequence queryText) { |
| 512 | setQuery(queryText); |
| 513 | } |
| 514 | |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 515 | private final OnClickListener mOnClickListener = new OnClickListener() { |
| 516 | |
| 517 | public void onClick(View v) { |
| 518 | if (v == mSearchButton) { |
| 519 | onSearchClicked(); |
| 520 | } else if (v == mCloseButton) { |
| 521 | onCloseClicked(); |
| 522 | } else if (v == mSubmitButton) { |
| 523 | onSubmitQuery(); |
Amith Yamasani | ebcf5a3a | 2010-10-13 11:35:24 -0700 | [diff] [blame] | 524 | } else if (v == mVoiceButton) { |
| 525 | onVoiceClicked(); |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 526 | } |
| 527 | } |
| 528 | }; |
| 529 | |
| 530 | /** |
| 531 | * Handles the key down event for dealing with action keys. |
| 532 | * |
| 533 | * @param keyCode This is the keycode of the typed key, and is the same value as |
| 534 | * found in the KeyEvent parameter. |
| 535 | * @param event The complete event record for the typed key |
| 536 | * |
| 537 | * @return true if the event was handled here, or false if not. |
| 538 | */ |
| 539 | @Override |
| 540 | public boolean onKeyDown(int keyCode, KeyEvent event) { |
| 541 | if (mSearchable == null) { |
| 542 | return false; |
| 543 | } |
| 544 | |
| 545 | // if it's an action specified by the searchable activity, launch the |
| 546 | // entered query with the action key |
| 547 | SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode); |
| 548 | if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) { |
Amith Yamasani | 9322775 | 2010-09-14 10:10:54 -0700 | [diff] [blame] | 549 | launchQuerySearch(keyCode, actionKey.getQueryActionMsg(), mQueryTextView.getText() |
| 550 | .toString()); |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 551 | return true; |
| 552 | } |
| 553 | |
| 554 | return super.onKeyDown(keyCode, event); |
| 555 | } |
| 556 | |
| 557 | private void updateQueryHint() { |
| 558 | if (mQueryHint != null) { |
| 559 | mQueryTextView.setHint(mQueryHint); |
| 560 | } else if (mSearchable != null) { |
| 561 | CharSequence hint = null; |
| 562 | int hintId = mSearchable.getHintId(); |
| 563 | if (hintId != 0) { |
| 564 | hint = getContext().getString(hintId); |
| 565 | } |
| 566 | if (hint != null) { |
| 567 | mQueryTextView.setHint(hint); |
| 568 | } |
| 569 | } |
| 570 | } |
| 571 | |
| 572 | /** |
| 573 | * Updates the auto-complete text view. |
| 574 | */ |
| 575 | private void updateSearchAutoComplete() { |
| 576 | // close any existing suggestions adapter |
| 577 | //closeSuggestionsAdapter(); |
| 578 | |
| 579 | mQueryTextView.setDropDownAnimationStyle(0); // no animation |
Amith Yamasani | 5931b1f | 2010-10-18 16:13:14 -0700 | [diff] [blame] | 580 | mQueryTextView.setThreshold(mSearchable.getSuggestThreshold()); |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 581 | |
| 582 | // attach the suggestions adapter, if suggestions are available |
| 583 | // The existence of a suggestions authority is the proxy for "suggestions available here" |
| 584 | if (mSearchable.getSuggestAuthority() != null) { |
| 585 | mSuggestionsAdapter = new SuggestionsAdapter(getContext(), |
| 586 | this, mSearchable, mOutsideDrawablesCache); |
| 587 | mQueryTextView.setAdapter(mSuggestionsAdapter); |
Amith Yamasani | e678f46 | 2010-09-15 16:13:43 -0700 | [diff] [blame] | 588 | ((SuggestionsAdapter) mSuggestionsAdapter).setQueryRefinement( |
| 589 | mQueryRefinement ? SuggestionsAdapter.REFINE_ALL |
| 590 | : SuggestionsAdapter.REFINE_BY_ENTRY); |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 591 | } |
| 592 | } |
| 593 | |
Amith Yamasani | ebcf5a3a | 2010-10-13 11:35:24 -0700 | [diff] [blame] | 594 | /** |
| 595 | * Update the visibility of the voice button. There are actually two voice search modes, |
| 596 | * either of which will activate the button. |
| 597 | * @param empty whether the search query text field is empty. If it is, then the other |
| 598 | * criteria apply to make the voice button visible. Otherwise the voice button will not |
| 599 | * be visible - i.e., if the user has typed a query, remove the voice button. |
| 600 | */ |
| 601 | private void updateVoiceButton(boolean empty) { |
| 602 | int visibility = View.GONE; |
| 603 | if (mSearchable != null && mSearchable.getVoiceSearchEnabled() && empty |
| 604 | && !isIconified()) { |
| 605 | Intent testIntent = null; |
| 606 | if (mSearchable.getVoiceSearchLaunchWebSearch()) { |
| 607 | testIntent = mVoiceWebSearchIntent; |
| 608 | } else if (mSearchable.getVoiceSearchLaunchRecognizer()) { |
| 609 | testIntent = mVoiceAppSearchIntent; |
| 610 | } |
| 611 | if (testIntent != null) { |
| 612 | ResolveInfo ri = getContext().getPackageManager().resolveActivity(testIntent, |
| 613 | PackageManager.MATCH_DEFAULT_ONLY); |
| 614 | if (ri != null) { |
| 615 | visibility = View.VISIBLE; |
| 616 | } |
| 617 | } |
| 618 | } |
| 619 | mVoiceButton.setVisibility(visibility); |
| 620 | } |
| 621 | |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 622 | private final OnEditorActionListener mOnEditorActionListener = new OnEditorActionListener() { |
| 623 | |
| 624 | /** |
| 625 | * Called when the input method default action key is pressed. |
| 626 | */ |
| 627 | public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { |
| 628 | onSubmitQuery(); |
| 629 | return true; |
| 630 | } |
| 631 | }; |
| 632 | |
| 633 | private void onTextChanged(CharSequence newText) { |
| 634 | CharSequence text = mQueryTextView.getText(); |
| 635 | boolean hasText = !TextUtils.isEmpty(text); |
| 636 | if (isSubmitButtonEnabled()) { |
| 637 | mSubmitButton.setVisibility(hasText ? VISIBLE : GONE); |
Amith Yamasani | 5931b1f | 2010-10-18 16:13:14 -0700 | [diff] [blame] | 638 | requestLayout(); |
| 639 | invalidate(); |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 640 | } |
Amith Yamasani | ebcf5a3a | 2010-10-13 11:35:24 -0700 | [diff] [blame] | 641 | updateVoiceButton(!hasText); |
| 642 | if (mOnQueryChangeListener != null) { |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 643 | mOnQueryChangeListener.onQueryTextChanged(newText.toString()); |
Amith Yamasani | ebcf5a3a | 2010-10-13 11:35:24 -0700 | [diff] [blame] | 644 | } |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 645 | } |
| 646 | |
| 647 | private void onSubmitQuery() { |
| 648 | CharSequence query = mQueryTextView.getText(); |
| 649 | if (!TextUtils.isEmpty(query)) { |
| 650 | if (mOnQueryChangeListener == null |
| 651 | || !mOnQueryChangeListener.onSubmitQuery(query.toString())) { |
| 652 | if (mSearchable != null) { |
| 653 | launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null, query.toString()); |
Amith Yamasani | 0594476 | 2010-10-08 13:52:38 -0700 | [diff] [blame] | 654 | setImeVisibility(false); |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 655 | } |
Amith Yamasani | ebcf5a3a | 2010-10-13 11:35:24 -0700 | [diff] [blame] | 656 | dismissSuggestions(); |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 657 | } |
| 658 | } |
| 659 | } |
| 660 | |
Amith Yamasani | ebcf5a3a | 2010-10-13 11:35:24 -0700 | [diff] [blame] | 661 | private void dismissSuggestions() { |
| 662 | mQueryTextView.dismissDropDown(); |
| 663 | } |
| 664 | |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 665 | private void onCloseClicked() { |
| 666 | if (mOnCloseListener == null || !mOnCloseListener.onClose()) { |
Amith Yamasani | 0594476 | 2010-10-08 13:52:38 -0700 | [diff] [blame] | 667 | CharSequence text = mQueryTextView.getText(); |
| 668 | if (TextUtils.isEmpty(text)) { |
| 669 | // query field already empty, hide the keyboard and remove focus |
Amith Yamasani | ebcf5a3a | 2010-10-13 11:35:24 -0700 | [diff] [blame] | 670 | clearFocus(); |
Amith Yamasani | 0594476 | 2010-10-08 13:52:38 -0700 | [diff] [blame] | 671 | setImeVisibility(false); |
| 672 | } else { |
| 673 | mQueryTextView.setText(""); |
| 674 | } |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 675 | updateViewsVisibility(mIconifiedByDefault); |
Amith Yamasani | ebcf5a3a | 2010-10-13 11:35:24 -0700 | [diff] [blame] | 676 | if (mIconifiedByDefault) setImeVisibility(false); |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 677 | } |
| 678 | } |
| 679 | |
| 680 | private void onSearchClicked() { |
| 681 | mQueryTextView.requestFocus(); |
| 682 | updateViewsVisibility(false); |
Amith Yamasani | 0594476 | 2010-10-08 13:52:38 -0700 | [diff] [blame] | 683 | setImeVisibility(true); |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 684 | } |
| 685 | |
Amith Yamasani | ebcf5a3a | 2010-10-13 11:35:24 -0700 | [diff] [blame] | 686 | private void onVoiceClicked() { |
| 687 | // guard against possible race conditions |
| 688 | if (mSearchable == null) { |
| 689 | return; |
| 690 | } |
| 691 | SearchableInfo searchable = mSearchable; |
| 692 | try { |
| 693 | if (searchable.getVoiceSearchLaunchWebSearch()) { |
| 694 | Intent webSearchIntent = createVoiceWebSearchIntent(mVoiceWebSearchIntent, |
| 695 | searchable); |
| 696 | getContext().startActivity(webSearchIntent); |
| 697 | } else if (searchable.getVoiceSearchLaunchRecognizer()) { |
| 698 | Intent appSearchIntent = createVoiceAppSearchIntent(mVoiceAppSearchIntent, |
| 699 | searchable); |
| 700 | getContext().startActivity(appSearchIntent); |
| 701 | } |
| 702 | } catch (ActivityNotFoundException e) { |
| 703 | // Should not happen, since we check the availability of |
| 704 | // voice search before showing the button. But just in case... |
| 705 | Log.w(LOG_TAG, "Could not find voice search activity"); |
| 706 | } |
| 707 | } |
| 708 | |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 709 | private final OnItemClickListener mOnItemClickListener = new OnItemClickListener() { |
| 710 | |
| 711 | /** |
| 712 | * Implements OnItemClickListener |
| 713 | */ |
| 714 | public void onItemClick(AdapterView<?> parent, View view, int position, long id) { |
| 715 | if (DBG) |
| 716 | Log.d(LOG_TAG, "onItemClick() position " + position); |
Amith Yamasani | 0594476 | 2010-10-08 13:52:38 -0700 | [diff] [blame] | 717 | if (mOnSuggestionListener == null |
| 718 | || !mOnSuggestionListener.onSuggestionClicked(position)) { |
| 719 | launchSuggestion(position, KeyEvent.KEYCODE_UNKNOWN, null); |
Amith Yamasani | b1818e8 | 2010-10-20 10:06:08 -0700 | [diff] [blame] | 720 | setImeVisibility(false); |
Amith Yamasani | ebcf5a3a | 2010-10-13 11:35:24 -0700 | [diff] [blame] | 721 | dismissSuggestions(); |
Amith Yamasani | 0594476 | 2010-10-08 13:52:38 -0700 | [diff] [blame] | 722 | } |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 723 | } |
| 724 | }; |
| 725 | |
| 726 | private final OnItemSelectedListener mOnItemSelectedListener = new OnItemSelectedListener() { |
| 727 | |
| 728 | /** |
| 729 | * Implements OnItemSelectedListener |
| 730 | */ |
| 731 | public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { |
| 732 | if (DBG) |
| 733 | Log.d(LOG_TAG, "onItemSelected() position " + position); |
| 734 | // A suggestion has been selected, rewrite the query if possible, |
| 735 | // otherwise the restore the original query. |
Amith Yamasani | 0594476 | 2010-10-08 13:52:38 -0700 | [diff] [blame] | 736 | if (mOnSuggestionListener == null |
| 737 | || !mOnSuggestionListener.onSuggestionSelected(position)) { |
| 738 | rewriteQueryFromSuggestion(position); |
| 739 | } |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 740 | } |
| 741 | |
| 742 | /** |
| 743 | * Implements OnItemSelectedListener |
| 744 | */ |
| 745 | public void onNothingSelected(AdapterView<?> parent) { |
| 746 | if (DBG) |
| 747 | Log.d(LOG_TAG, "onNothingSelected()"); |
| 748 | } |
| 749 | }; |
| 750 | |
| 751 | /** |
| 752 | * Query rewriting. |
| 753 | */ |
| 754 | private void rewriteQueryFromSuggestion(int position) { |
| 755 | CharSequence oldQuery = mQueryTextView.getText(); |
| 756 | Cursor c = mSuggestionsAdapter.getCursor(); |
| 757 | if (c == null) { |
| 758 | return; |
| 759 | } |
| 760 | if (c.moveToPosition(position)) { |
| 761 | // Get the new query from the suggestion. |
| 762 | CharSequence newQuery = mSuggestionsAdapter.convertToString(c); |
| 763 | if (newQuery != null) { |
| 764 | // The suggestion rewrites the query. |
| 765 | // Update the text field, without getting new suggestions. |
| 766 | setQuery(newQuery); |
| 767 | } else { |
| 768 | // The suggestion does not rewrite the query, restore the user's query. |
| 769 | setQuery(oldQuery); |
| 770 | } |
| 771 | } else { |
| 772 | // We got a bad position, restore the user's query. |
| 773 | setQuery(oldQuery); |
| 774 | } |
| 775 | } |
| 776 | |
| 777 | /** |
| 778 | * Launches an intent based on a suggestion. |
| 779 | * |
| 780 | * @param position The index of the suggestion to create the intent from. |
| 781 | * @param actionKey The key code of the action key that was pressed, |
| 782 | * or {@link KeyEvent#KEYCODE_UNKNOWN} if none. |
| 783 | * @param actionMsg The message for the action key that was pressed, |
| 784 | * or <code>null</code> if none. |
| 785 | * @return true if a successful launch, false if could not (e.g. bad position). |
| 786 | */ |
| 787 | private boolean launchSuggestion(int position, int actionKey, String actionMsg) { |
| 788 | Cursor c = mSuggestionsAdapter.getCursor(); |
| 789 | if ((c != null) && c.moveToPosition(position)) { |
| 790 | |
| 791 | Intent intent = createIntentFromSuggestion(c, actionKey, actionMsg); |
| 792 | |
| 793 | // launch the intent |
| 794 | launchIntent(intent); |
| 795 | |
| 796 | return true; |
| 797 | } |
| 798 | return false; |
| 799 | } |
| 800 | |
| 801 | /** |
| 802 | * Launches an intent, including any special intent handling. |
| 803 | */ |
| 804 | private void launchIntent(Intent intent) { |
| 805 | if (intent == null) { |
| 806 | return; |
| 807 | } |
| 808 | try { |
| 809 | // If the intent was created from a suggestion, it will always have an explicit |
| 810 | // component here. |
| 811 | getContext().startActivity(intent); |
| 812 | } catch (RuntimeException ex) { |
| 813 | Log.e(LOG_TAG, "Failed launch activity: " + intent, ex); |
| 814 | } |
| 815 | } |
| 816 | |
| 817 | /** |
| 818 | * Sets the text in the query box, without updating the suggestions. |
| 819 | */ |
| 820 | private void setQuery(CharSequence query) { |
Amith Yamasani | e678f46 | 2010-09-15 16:13:43 -0700 | [diff] [blame] | 821 | mQueryTextView.setText(query, true); |
| 822 | // Move the cursor to the end |
| 823 | mQueryTextView.setSelection(TextUtils.isEmpty(query) ? 0 : query.length()); |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 824 | } |
| 825 | |
| 826 | private void launchQuerySearch(int actionKey, String actionMsg, String query) { |
| 827 | String action = Intent.ACTION_SEARCH; |
Amith Yamasani | e678f46 | 2010-09-15 16:13:43 -0700 | [diff] [blame] | 828 | Intent intent = createIntent(action, null, null, query, actionKey, actionMsg); |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 829 | getContext().startActivity(intent); |
| 830 | } |
| 831 | |
| 832 | /** |
| 833 | * Constructs an intent from the given information and the search dialog state. |
| 834 | * |
| 835 | * @param action Intent action. |
| 836 | * @param data Intent data, or <code>null</code>. |
| 837 | * @param extraData Data for {@link SearchManager#EXTRA_DATA_KEY} or <code>null</code>. |
| 838 | * @param query Intent query, or <code>null</code>. |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 839 | * @param actionKey The key code of the action key that was pressed, |
| 840 | * or {@link KeyEvent#KEYCODE_UNKNOWN} if none. |
| 841 | * @param actionMsg The message for the action key that was pressed, |
| 842 | * or <code>null</code> if none. |
| 843 | * @param mode The search mode, one of the acceptable values for |
| 844 | * {@link SearchManager#SEARCH_MODE}, or {@code null}. |
| 845 | * @return The intent. |
| 846 | */ |
| 847 | private Intent createIntent(String action, Uri data, String extraData, String query, |
Amith Yamasani | e678f46 | 2010-09-15 16:13:43 -0700 | [diff] [blame] | 848 | int actionKey, String actionMsg) { |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 849 | // Now build the Intent |
| 850 | Intent intent = new Intent(action); |
| 851 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); |
| 852 | // We need CLEAR_TOP to avoid reusing an old task that has other activities |
| 853 | // on top of the one we want. We don't want to do this in in-app search though, |
| 854 | // as it can be destructive to the activity stack. |
| 855 | if (data != null) { |
| 856 | intent.setData(data); |
| 857 | } |
| 858 | intent.putExtra(SearchManager.USER_QUERY, query); |
| 859 | if (query != null) { |
| 860 | intent.putExtra(SearchManager.QUERY, query); |
| 861 | } |
| 862 | if (extraData != null) { |
| 863 | intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData); |
| 864 | } |
| 865 | if (actionKey != KeyEvent.KEYCODE_UNKNOWN) { |
| 866 | intent.putExtra(SearchManager.ACTION_KEY, actionKey); |
| 867 | intent.putExtra(SearchManager.ACTION_MSG, actionMsg); |
| 868 | } |
| 869 | intent.setComponent(mSearchable.getSearchActivity()); |
| 870 | return intent; |
| 871 | } |
| 872 | |
| 873 | /** |
Amith Yamasani | ebcf5a3a | 2010-10-13 11:35:24 -0700 | [diff] [blame] | 874 | * Create and return an Intent that can launch the voice search activity for web search. |
| 875 | */ |
| 876 | private Intent createVoiceWebSearchIntent(Intent baseIntent, SearchableInfo searchable) { |
| 877 | Intent voiceIntent = new Intent(baseIntent); |
| 878 | ComponentName searchActivity = searchable.getSearchActivity(); |
| 879 | voiceIntent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, searchActivity == null ? null |
| 880 | : searchActivity.flattenToShortString()); |
| 881 | return voiceIntent; |
| 882 | } |
| 883 | |
| 884 | /** |
| 885 | * Create and return an Intent that can launch the voice search activity, perform a specific |
| 886 | * voice transcription, and forward the results to the searchable activity. |
| 887 | * |
| 888 | * @param baseIntent The voice app search intent to start from |
| 889 | * @return A completely-configured intent ready to send to the voice search activity |
| 890 | */ |
| 891 | private Intent createVoiceAppSearchIntent(Intent baseIntent, SearchableInfo searchable) { |
| 892 | ComponentName searchActivity = searchable.getSearchActivity(); |
| 893 | |
| 894 | // create the necessary intent to set up a search-and-forward operation |
| 895 | // in the voice search system. We have to keep the bundle separate, |
| 896 | // because it becomes immutable once it enters the PendingIntent |
| 897 | Intent queryIntent = new Intent(Intent.ACTION_SEARCH); |
| 898 | queryIntent.setComponent(searchActivity); |
| 899 | PendingIntent pending = PendingIntent.getActivity(getContext(), 0, queryIntent, |
| 900 | PendingIntent.FLAG_ONE_SHOT); |
| 901 | |
| 902 | // Now set up the bundle that will be inserted into the pending intent |
| 903 | // when it's time to do the search. We always build it here (even if empty) |
| 904 | // because the voice search activity will always need to insert "QUERY" into |
| 905 | // it anyway. |
| 906 | Bundle queryExtras = new Bundle(); |
| 907 | |
| 908 | // Now build the intent to launch the voice search. Add all necessary |
| 909 | // extras to launch the voice recognizer, and then all the necessary extras |
| 910 | // to forward the results to the searchable activity |
| 911 | Intent voiceIntent = new Intent(baseIntent); |
| 912 | |
| 913 | // Add all of the configuration options supplied by the searchable's metadata |
| 914 | String languageModel = RecognizerIntent.LANGUAGE_MODEL_FREE_FORM; |
| 915 | String prompt = null; |
| 916 | String language = null; |
| 917 | int maxResults = 1; |
| 918 | |
| 919 | Resources resources = getResources(); |
| 920 | if (searchable.getVoiceLanguageModeId() != 0) { |
| 921 | languageModel = resources.getString(searchable.getVoiceLanguageModeId()); |
| 922 | } |
| 923 | if (searchable.getVoicePromptTextId() != 0) { |
| 924 | prompt = resources.getString(searchable.getVoicePromptTextId()); |
| 925 | } |
| 926 | if (searchable.getVoiceLanguageId() != 0) { |
| 927 | language = resources.getString(searchable.getVoiceLanguageId()); |
| 928 | } |
| 929 | if (searchable.getVoiceMaxResults() != 0) { |
| 930 | maxResults = searchable.getVoiceMaxResults(); |
| 931 | } |
| 932 | voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, languageModel); |
| 933 | voiceIntent.putExtra(RecognizerIntent.EXTRA_PROMPT, prompt); |
| 934 | voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, language); |
| 935 | voiceIntent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, maxResults); |
| 936 | voiceIntent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, searchActivity == null ? null |
| 937 | : searchActivity.flattenToShortString()); |
| 938 | |
| 939 | // Add the values that configure forwarding the results |
| 940 | voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT, pending); |
| 941 | voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT_BUNDLE, queryExtras); |
| 942 | |
| 943 | return voiceIntent; |
| 944 | } |
| 945 | |
| 946 | /** |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 947 | * When a particular suggestion has been selected, perform the various lookups required |
| 948 | * to use the suggestion. This includes checking the cursor for suggestion-specific data, |
| 949 | * and/or falling back to the XML for defaults; It also creates REST style Uri data when |
| 950 | * the suggestion includes a data id. |
| 951 | * |
| 952 | * @param c The suggestions cursor, moved to the row of the user's selection |
| 953 | * @param actionKey The key code of the action key that was pressed, |
| 954 | * or {@link KeyEvent#KEYCODE_UNKNOWN} if none. |
| 955 | * @param actionMsg The message for the action key that was pressed, |
| 956 | * or <code>null</code> if none. |
| 957 | * @return An intent for the suggestion at the cursor's position. |
| 958 | */ |
| 959 | private Intent createIntentFromSuggestion(Cursor c, int actionKey, String actionMsg) { |
| 960 | try { |
| 961 | // use specific action if supplied, or default action if supplied, or fixed default |
| 962 | String action = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_ACTION); |
| 963 | |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 964 | if (action == null) { |
| 965 | action = mSearchable.getSuggestIntentAction(); |
| 966 | } |
| 967 | if (action == null) { |
| 968 | action = Intent.ACTION_SEARCH; |
| 969 | } |
| 970 | |
| 971 | // use specific data if supplied, or default data if supplied |
| 972 | String data = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA); |
| 973 | if (data == null) { |
| 974 | data = mSearchable.getSuggestIntentData(); |
| 975 | } |
| 976 | // then, if an ID was provided, append it. |
| 977 | if (data != null) { |
| 978 | String id = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID); |
| 979 | if (id != null) { |
| 980 | data = data + "/" + Uri.encode(id); |
| 981 | } |
| 982 | } |
| 983 | Uri dataUri = (data == null) ? null : Uri.parse(data); |
| 984 | |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 985 | String query = getColumnString(c, SearchManager.SUGGEST_COLUMN_QUERY); |
| 986 | String extraData = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA); |
| 987 | |
Amith Yamasani | e678f46 | 2010-09-15 16:13:43 -0700 | [diff] [blame] | 988 | return createIntent(action, dataUri, extraData, query, actionKey, actionMsg); |
Amith Yamasani | 733cbd5 | 2010-09-03 12:21:39 -0700 | [diff] [blame] | 989 | } catch (RuntimeException e ) { |
| 990 | int rowNum; |
| 991 | try { // be really paranoid now |
| 992 | rowNum = c.getPosition(); |
| 993 | } catch (RuntimeException e2 ) { |
| 994 | rowNum = -1; |
| 995 | } |
| 996 | Log.w(LOG_TAG, "Search Suggestions cursor at row " + rowNum + |
| 997 | " returned exception" + e.toString()); |
| 998 | return null; |
| 999 | } |
| 1000 | } |
| 1001 | |
| 1002 | /** |
| 1003 | * Callback to watch the text field for empty/non-empty |
| 1004 | */ |
| 1005 | private TextWatcher mTextWatcher = new TextWatcher() { |
| 1006 | |
| 1007 | public void beforeTextChanged(CharSequence s, int start, int before, int after) { } |
| 1008 | |
| 1009 | public void onTextChanged(CharSequence s, int start, |
| 1010 | int before, int after) { |
| 1011 | SearchView.this.onTextChanged(s); |
| 1012 | } |
| 1013 | |
| 1014 | public void afterTextChanged(Editable s) { |
| 1015 | } |
| 1016 | }; |
Amith Yamasani | 0594476 | 2010-10-08 13:52:38 -0700 | [diff] [blame] | 1017 | } |