blob: f524ef0f1d0f9e27cdca5fbd7163a51f3768a535 [file] [log] [blame]
Amith Yamasani733cbd52010-09-03 12:21:39 -07001/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.widget;
18
19import static android.widget.SuggestionsAdapter.getColumnString;
20
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -070021import android.app.PendingIntent;
Amith Yamasani733cbd52010-09-03 12:21:39 -070022import android.app.SearchManager;
23import android.app.SearchableInfo;
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -070024import android.content.ActivityNotFoundException;
25import android.content.ComponentName;
Amith Yamasani733cbd52010-09-03 12:21:39 -070026import android.content.Context;
27import android.content.Intent;
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -070028import android.content.pm.PackageManager;
29import android.content.pm.ResolveInfo;
Amith Yamasani968ec932010-12-02 14:00:47 -080030import android.content.res.Configuration;
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -070031import android.content.res.Resources;
Amith Yamasani733cbd52010-09-03 12:21:39 -070032import android.content.res.TypedArray;
33import android.database.Cursor;
repo sync6a81b822010-09-28 13:00:05 -070034import android.graphics.Rect;
Amith Yamasani733cbd52010-09-03 12:21:39 -070035import android.graphics.drawable.Drawable;
36import android.net.Uri;
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -070037import android.os.Bundle;
Amith Yamasania95e4882011-08-17 11:41:37 -070038import android.os.Handler;
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -070039import android.speech.RecognizerIntent;
Amith Yamasani733cbd52010-09-03 12:21:39 -070040import android.text.Editable;
Amith Yamasani5607a382011-08-09 14:16:37 -070041import android.text.InputType;
Amith Yamasanib4569fb2011-07-08 15:25:39 -070042import android.text.Spannable;
43import android.text.SpannableStringBuilder;
Amith Yamasani733cbd52010-09-03 12:21:39 -070044import android.text.TextUtils;
45import android.text.TextWatcher;
Amith Yamasanib4569fb2011-07-08 15:25:39 -070046import android.text.style.ImageSpan;
Amith Yamasani733cbd52010-09-03 12:21:39 -070047import android.util.AttributeSet;
48import android.util.Log;
Amith Yamasanib4569fb2011-07-08 15:25:39 -070049import android.util.TypedValue;
Amith Yamasani763bc072011-07-22 11:53:47 -070050import android.view.CollapsibleActionView;
Amith Yamasani733cbd52010-09-03 12:21:39 -070051import android.view.KeyEvent;
52import android.view.LayoutInflater;
53import android.view.View;
Amith Yamasani5607a382011-08-09 14:16:37 -070054import android.view.inputmethod.EditorInfo;
Amith Yamasani733cbd52010-09-03 12:21:39 -070055import android.view.inputmethod.InputMethodManager;
56import android.widget.AdapterView.OnItemClickListener;
57import android.widget.AdapterView.OnItemSelectedListener;
58import android.widget.TextView.OnEditorActionListener;
59
Amith Yamasanib4569fb2011-07-08 15:25:39 -070060import com.android.internal.R;
61
Amith Yamasani733cbd52010-09-03 12:21:39 -070062import java.util.WeakHashMap;
63
64/**
Amith Yamasani763bc072011-07-22 11:53:47 -070065 * 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 Yamasani5931b1f2010-10-18 16:13:14 -070068 *
Amith Yamasani763bc072011-07-22 11:53:47 -070069 * <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 Yamasani5931b1f2010-10-18 16:13:14 -070077 *
Amith Yamasani763bc072011-07-22 11:53:47 -070078 * <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 Yamasani5931b1f2010-10-18 16:13:14 -070084 * @attr ref android.R.styleable#SearchView_iconifiedByDefault
Amith Yamasani5607a382011-08-09 14:16:37 -070085 * @attr ref android.R.styleable#SearchView_imeOptions
86 * @attr ref android.R.styleable#SearchView_inputType
Amith Yamasani5931b1f2010-10-18 16:13:14 -070087 * @attr ref android.R.styleable#SearchView_maxWidth
Scott Mainabdf0d52011-02-08 10:20:27 -080088 * @attr ref android.R.styleable#SearchView_queryHint
Amith Yamasani733cbd52010-09-03 12:21:39 -070089 */
Amith Yamasani763bc072011-07-22 11:53:47 -070090public class SearchView extends LinearLayout implements CollapsibleActionView {
Amith Yamasani733cbd52010-09-03 12:21:39 -070091
92 private static final boolean DBG = false;
93 private static final String LOG_TAG = "SearchView";
94
Luca Zanolin535698c2011-10-06 13:36:15 +010095 /**
96 * Private constant for removing the microphone in the keyboard.
97 */
98 private static final String IME_OPTION_NO_MICROPHONE = "nm";
99
Adam Powell01f21352011-01-20 18:30:10 -0800100 private OnQueryTextListener mOnQueryChangeListener;
Amith Yamasani733cbd52010-09-03 12:21:39 -0700101 private OnCloseListener mOnCloseListener;
Amith Yamasani05944762010-10-08 13:52:38 -0700102 private OnFocusChangeListener mOnQueryTextFocusChangeListener;
Adam Powell01f21352011-01-20 18:30:10 -0800103 private OnSuggestionListener mOnSuggestionListener;
Amith Yamasani48385482010-12-03 14:43:52 -0800104 private OnClickListener mOnSearchClickListener;
Amith Yamasani733cbd52010-09-03 12:21:39 -0700105
106 private boolean mIconifiedByDefault;
Amith Yamasani93227752010-09-14 10:10:54 -0700107 private boolean mIconified;
Amith Yamasani733cbd52010-09-03 12:21:39 -0700108 private CursorAdapter mSuggestionsAdapter;
109 private View mSearchButton;
110 private View mSubmitButton;
Amith Yamasani79f74302011-03-08 14:16:35 -0800111 private View mSearchPlate;
Amith Yamasani9b2e3022011-01-14 11:34:12 -0800112 private View mSubmitArea;
Amith Yamasani4aedb392010-12-15 16:04:57 -0800113 private ImageView mCloseButton;
Amith Yamasani733cbd52010-09-03 12:21:39 -0700114 private View mSearchEditFrame;
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -0700115 private View mVoiceButton;
Amith Yamasani968ec932010-12-02 14:00:47 -0800116 private SearchAutoComplete mQueryTextView;
Amith Yamasanib4569fb2011-07-08 15:25:39 -0700117 private View mDropDownAnchor;
118 private ImageView mSearchHintIcon;
Amith Yamasani733cbd52010-09-03 12:21:39 -0700119 private boolean mSubmitButtonEnabled;
120 private CharSequence mQueryHint;
Amith Yamasanie678f462010-09-15 16:13:43 -0700121 private boolean mQueryRefinement;
Amith Yamasani05944762010-10-08 13:52:38 -0700122 private boolean mClearingFocus;
Amith Yamasani5931b1f2010-10-18 16:13:14 -0700123 private int mMaxWidth;
Amith Yamasani9b2e3022011-01-14 11:34:12 -0800124 private boolean mVoiceButtonEnabled;
Amith Yamasanib47c4fd2011-08-04 14:30:07 -0700125 private CharSequence mOldQueryText;
Amith Yamasani068d73c2011-05-27 15:15:14 -0700126 private CharSequence mUserQuery;
Amith Yamasani763bc072011-07-22 11:53:47 -0700127 private boolean mExpandedInActionView;
Adam Powell53f56c42011-09-25 13:46:15 -0700128 private int mCollapsedImeOptions;
Amith Yamasani733cbd52010-09-03 12:21:39 -0700129
130 private SearchableInfo mSearchable;
Amith Yamasani940ef382011-03-02 18:43:23 -0800131 private Bundle mAppSearchData;
Amith Yamasani733cbd52010-09-03 12:21:39 -0700132
Adam Powellccdd4ee2011-07-27 20:05:14 -0700133 /*
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 Yamasania95e4882011-08-17 11:41:37 -0700148 private Runnable mUpdateDrawableStateRunnable = new Runnable() {
149 public void run() {
150 updateFocusedState();
151 }
152 };
153
Amith Yamasani87907642011-11-03 11:32:44 -0700154 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 Yamasaniebcf5a3a2010-10-13 11:35:24 -0700162 // For voice searching
163 private final Intent mVoiceWebSearchIntent;
164 private final Intent mVoiceAppSearchIntent;
165
Amith Yamasani733cbd52010-09-03 12:21:39 -0700166 // 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 Powell01f21352011-01-20 18:30:10 -0800174 public interface OnQueryTextListener {
Amith Yamasani733cbd52010-09-03 12:21:39 -0700175
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 Powell01f21352011-01-20 18:30:10 -0800188 boolean onQueryTextSubmit(String query);
Amith Yamasani733cbd52010-09-03 12:21:39 -0700189
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 Powell01f21352011-01-20 18:30:10 -0800198 boolean onQueryTextChange(String newText);
Amith Yamasani733cbd52010-09-03 12:21:39 -0700199 }
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 Yamasani05944762010-10-08 13:52:38 -0700212 /**
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 Powell01f21352011-01-20 18:30:10 -0800216 public interface OnSuggestionListener {
Amith Yamasani05944762010-10-08 13:52:38 -0700217
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 Powell01f21352011-01-20 18:30:10 -0800225 boolean onSuggestionSelect(int position);
Amith Yamasani05944762010-10-08 13:52:38 -0700226
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 Powell01f21352011-01-20 18:30:10 -0800235 boolean onSuggestionClick(int position);
Amith Yamasani05944762010-10-08 13:52:38 -0700236 }
237
Amith Yamasani733cbd52010-09-03 12:21:39 -0700238 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 Yamasani968ec932010-12-02 14:00:47 -0800250 mQueryTextView = (SearchAutoComplete) findViewById(R.id.search_src_text);
251 mQueryTextView.setSearchView(this);
252
Amith Yamasani733cbd52010-09-03 12:21:39 -0700253 mSearchEditFrame = findViewById(R.id.search_edit_frame);
Amith Yamasani79f74302011-03-08 14:16:35 -0800254 mSearchPlate = findViewById(R.id.search_plate);
Amith Yamasani9b2e3022011-01-14 11:34:12 -0800255 mSubmitArea = findViewById(R.id.submit_area);
Amith Yamasani733cbd52010-09-03 12:21:39 -0700256 mSubmitButton = findViewById(R.id.search_go_btn);
Amith Yamasani4aedb392010-12-15 16:04:57 -0800257 mCloseButton = (ImageView) findViewById(R.id.search_close_btn);
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -0700258 mVoiceButton = findViewById(R.id.search_voice_btn);
Amith Yamasanib4569fb2011-07-08 15:25:39 -0700259 mSearchHintIcon = (ImageView) findViewById(R.id.search_mag_icon);
Amith Yamasani733cbd52010-09-03 12:21:39 -0700260
261 mSearchButton.setOnClickListener(mOnClickListener);
262 mCloseButton.setOnClickListener(mOnClickListener);
263 mSubmitButton.setOnClickListener(mOnClickListener);
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -0700264 mVoiceButton.setOnClickListener(mOnClickListener);
Amith Yamasanif28d1872011-07-26 12:21:03 -0700265 mQueryTextView.setOnClickListener(mOnClickListener);
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -0700266
Amith Yamasani733cbd52010-09-03 12:21:39 -0700267 mQueryTextView.addTextChangedListener(mTextWatcher);
268 mQueryTextView.setOnEditorActionListener(mOnEditorActionListener);
269 mQueryTextView.setOnItemClickListener(mOnItemClickListener);
270 mQueryTextView.setOnItemSelectedListener(mOnItemSelectedListener);
Amith Yamasani968ec932010-12-02 14:00:47 -0800271 mQueryTextView.setOnKeyListener(mTextKeyListener);
Luca Zanolin535698c2011-10-06 13:36:15 +0100272 // Inform any listener of focus changes
Amith Yamasani05944762010-10-08 13:52:38 -0700273 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 Yamasani733cbd52010-09-03 12:21:39 -0700281
Amith Yamasani733cbd52010-09-03 12:21:39 -0700282 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SearchView, 0, 0);
283 setIconifiedByDefault(a.getBoolean(R.styleable.SearchView_iconifiedByDefault, true));
Amith Yamasani5931b1f2010-10-18 16:13:14 -0700284 int maxWidth = a.getDimensionPixelSize(R.styleable.SearchView_maxWidth, -1);
285 if (maxWidth != -1) {
286 setMaxWidth(maxWidth);
287 }
Adam Powellc0171d52011-01-13 14:31:17 -0800288 CharSequence queryHint = a.getText(R.styleable.SearchView_queryHint);
289 if (!TextUtils.isEmpty(queryHint)) {
290 setQueryHint(queryHint);
291 }
Amith Yamasani5607a382011-08-09 14:16:37 -0700292 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 Yamasani733cbd52010-09-03 12:21:39 -0700301 a.recycle();
302
Amith Yamasani5607a382011-08-09 14:16:37 -0700303 boolean focusable = true;
304
Amith Yamasani7f8aef62011-01-25 11:58:09 -0800305 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 Yamasaniebcf5a3a2010-10-13 11:35:24 -0700310 // 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 Yamasanib4569fb2011-07-08 15:25:39 -0700319 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 Yamasani733cbd52010-09-03 12:21:39 -0700331 updateViewsVisibility(mIconifiedByDefault);
Amith Yamasanib4569fb2011-07-08 15:25:39 -0700332 updateQueryHint();
Amith Yamasani733cbd52010-09-03 12:21:39 -0700333 }
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 Yamasani79f74302011-03-08 14:16:35 -0800347 updateQueryHint();
Amith Yamasani733cbd52010-09-03 12:21:39 -0700348 }
Amith Yamasani9b2e3022011-01-14 11:34:12 -0800349 // Cache the voice search capability
350 mVoiceButtonEnabled = hasVoiceSearch();
Luca Zanolin535698c2011-10-06 13:36:15 +0100351
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 Yamasanib4569fb2011-07-08 15:25:39 -0700357 updateViewsVisibility(isIconified());
Amith Yamasani733cbd52010-09-03 12:21:39 -0700358 }
359
Amith Yamasani940ef382011-03-02 18:43:23 -0800360 /**
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 Yamasani5607a382011-08-09 14:16:37 -0700369 /**
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 Yamasani05944762010-10-08 13:52:38 -0700393 /** @hide */
394 @Override
395 public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
Amith Yamasani7f8aef62011-01-25 11:58:09 -0800396 // 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 Yamasanif28d1872011-07-26 12:21:03 -0700403 if (result) {
404 updateViewsVisibility(false);
405 }
Amith Yamasani7f8aef62011-01-25 11:58:09 -0800406 return result;
407 } else {
408 return super.requestFocus(direction, previouslyFocusedRect);
409 }
Amith Yamasani05944762010-10-08 13:52:38 -0700410 }
411
412 /** @hide */
413 @Override
414 public void clearFocus() {
415 mClearingFocus = true;
Amith Yamasani10da5902011-07-26 16:14:26 -0700416 setImeVisibility(false);
Amith Yamasani05944762010-10-08 13:52:38 -0700417 super.clearFocus();
418 mQueryTextView.clearFocus();
Amith Yamasani05944762010-10-08 13:52:38 -0700419 mClearingFocus = false;
420 }
421
Amith Yamasani733cbd52010-09-03 12:21:39 -0700422 /**
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 Powell01f21352011-01-20 18:30:10 -0800428 public void setOnQueryTextListener(OnQueryTextListener listener) {
Amith Yamasani733cbd52010-09-03 12:21:39 -0700429 mOnQueryChangeListener = listener;
430 }
431
432 /**
Amith Yamasani93227752010-09-14 10:10:54 -0700433 * 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 Yamasani05944762010-10-08 13:52:38 -0700442 * 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 Powell01f21352011-01-20 18:30:10 -0800455 public void setOnSuggestionListener(OnSuggestionListener listener) {
Amith Yamasani05944762010-10-08 13:52:38 -0700456 mOnSuggestionListener = listener;
457 }
458
459 /**
Amith Yamasani48385482010-12-03 14:43:52 -0800460 * Sets a listener to inform when the search button is pressed. This is only
Scott Maincccdbe92011-02-06 15:51:47 -0800461 * 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 Yamasani48385482010-12-03 14:43:52 -0800463 *
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 Yamasani733cbd52010-09-03 12:21:39 -0700481 * 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 Plotnikov87c50252010-10-21 21:16:42 -0700490 if (query != null) {
491 mQueryTextView.setSelection(query.length());
Amith Yamasani068d73c2011-05-27 15:15:14 -0700492 mUserQuery = query;
Dmitri Plotnikov87c50252010-10-21 21:16:42 -0700493 }
494
Amith Yamasani733cbd52010-09-03 12:21:39 -0700495 // 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 Mainabdf0d52011-02-08 10:20:27 -0800506 *
507 * @attr ref android.R.styleable#SearchView_queryHint
Amith Yamasani733cbd52010-09-03 12:21:39 -0700508 */
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 Yamasani93227752010-09-14 10:10:54 -0700518 * is pressed. Changes to this property will take effect immediately.
Amith Yamasani733cbd52010-09-03 12:21:39 -0700519 *
Scott Maincccdbe92011-02-06 15:51:47 -0800520 * <p>The default value is true.</p>
Amith Yamasani93227752010-09-14 10:10:54 -0700521 *
522 * @param iconified whether the search field should be iconified by default
Scott Mainabdf0d52011-02-08 10:20:27 -0800523 *
524 * @attr ref android.R.styleable#SearchView_iconifiedByDefault
Amith Yamasani733cbd52010-09-03 12:21:39 -0700525 */
526 public void setIconifiedByDefault(boolean iconified) {
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -0700527 if (mIconifiedByDefault == iconified) return;
Amith Yamasani733cbd52010-09-03 12:21:39 -0700528 mIconifiedByDefault = iconified;
529 updateViewsVisibility(iconified);
Amith Yamasanib47c4fd2011-08-04 14:30:07 -0700530 updateQueryHint();
Amith Yamasani733cbd52010-09-03 12:21:39 -0700531 }
532
Amith Yamasani93227752010-09-14 10:10:54 -0700533 /**
534 * Returns the default iconified state of the search field.
535 * @return
536 */
Amith Yamasani733cbd52010-09-03 12:21:39 -0700537 public boolean isIconfiedByDefault() {
538 return mIconifiedByDefault;
539 }
540
541 /**
Amith Yamasani93227752010-09-14 10:10:54 -0700542 * 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 Yamasani733cbd52010-09-03 12:21:39 -0700570 * 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 Yamasani733cbd52010-09-03 12:21:39 -0700578 mSubmitButtonEnabled = enabled;
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -0700579 updateViewsVisibility(isIconified());
Amith Yamasani733cbd52010-09-03 12:21:39 -0700580 }
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 Yamasanie678f462010-09-15 16:13:43 -0700591 /**
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 Yamasani733cbd52010-09-03 12:21:39 -0700619 }
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 Yamasani5931b1f2010-10-18 16:13:14 -0700641 /**
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 Yamasania95e4882011-08-17 11:41:37 -0700654 // Let the standard measurements take effect in iconified state.
655 if (isIconified()) {
656 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
657 return;
658 }
659
Amith Yamasani5931b1f2010-10-18 16:13:14 -0700660 int widthMode = MeasureSpec.getMode(widthMeasureSpec);
661 int width = MeasureSpec.getSize(widthMeasureSpec);
662
Amith Yamasani167d69a2011-08-12 19:28:37 -0700663 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 Yamasani5931b1f2010-10-18 16:13:14 -0700682 }
Amith Yamasani167d69a2011-08-12 19:28:37 -0700683 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 Yamasani5931b1f2010-10-18 16:13:14 -0700690 }
691
Amith Yamasani93227752010-09-14 10:10:54 -0700692 private void updateViewsVisibility(final boolean collapsed) {
693 mIconified = collapsed;
Amith Yamasani733cbd52010-09-03 12:21:39 -0700694 // Visibility of views that are visible when collapsed
Amith Yamasani93227752010-09-14 10:10:54 -0700695 final int visCollapsed = collapsed ? VISIBLE : GONE;
Amith Yamasani05944762010-10-08 13:52:38 -0700696 // Is there text in the query
697 final boolean hasText = !TextUtils.isEmpty(mQueryTextView.getText());
Amith Yamasani733cbd52010-09-03 12:21:39 -0700698
699 mSearchButton.setVisibility(visCollapsed);
Amith Yamasani9b2e3022011-01-14 11:34:12 -0800700 updateSubmitButton(hasText);
701 mSearchEditFrame.setVisibility(collapsed ? GONE : VISIBLE);
Amith Yamasanib4569fb2011-07-08 15:25:39 -0700702 mSearchHintIcon.setVisibility(mIconifiedByDefault ? GONE : VISIBLE);
Amith Yamasani4aedb392010-12-15 16:04:57 -0800703 updateCloseButton();
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -0700704 updateVoiceButton(!hasText);
Amith Yamasani9b2e3022011-01-14 11:34:12 -0800705 updateSubmitArea();
Amith Yamasani9b2e3022011-01-14 11:34:12 -0800706 }
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 Yamasani79f74302011-03-08 14:16:35 -0800730 int visibility = GONE;
Amith Yamasanicf72ab42011-11-04 13:49:28 -0700731 if (mSubmitButtonEnabled && isSubmitAreaEnabled() && hasFocus()
732 && (hasText || !mVoiceButtonEnabled)) {
Amith Yamasani79f74302011-03-08 14:16:35 -0800733 visibility = VISIBLE;
734 }
735 mSubmitButton.setVisibility(visibility);
Amith Yamasani9b2e3022011-01-14 11:34:12 -0800736 }
737
738 private void updateSubmitArea() {
739 int visibility = GONE;
Amith Yamasani79f74302011-03-08 14:16:35 -0800740 if (isSubmitAreaEnabled()
741 && (mSubmitButton.getVisibility() == VISIBLE
742 || mVoiceButton.getVisibility() == VISIBLE)) {
743 visibility = VISIBLE;
Amith Yamasani9b2e3022011-01-14 11:34:12 -0800744 }
745 mSubmitArea.setVisibility(visibility);
Amith Yamasani733cbd52010-09-03 12:21:39 -0700746 }
747
Amith Yamasani4aedb392010-12-15 16:04:57 -0800748 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 Yamasani763bc072011-07-22 11:53:47 -0700752 final boolean showClose = hasText || (mIconifiedByDefault && !mExpandedInActionView);
Amith Yamasani167d69a2011-08-12 19:28:37 -0700753 mCloseButton.setVisibility(showClose ? VISIBLE : GONE);
Amith Yamasani4aedb392010-12-15 16:04:57 -0800754 mCloseButton.getDrawable().setState(hasText ? ENABLED_STATE_SET : EMPTY_STATE_SET);
755 }
756
Amith Yamasania95e4882011-08-17 11:41:37 -0700757 private void postUpdateFocusedState() {
758 post(mUpdateDrawableStateRunnable);
759 }
760
761 private void updateFocusedState() {
762 boolean focused = mQueryTextView.hasFocus();
Amith Yamasani79f74302011-03-08 14:16:35 -0800763 mSearchPlate.getBackground().setState(focused ? FOCUSED_STATE_SET : EMPTY_STATE_SET);
764 mSubmitArea.getBackground().setState(focused ? FOCUSED_STATE_SET : EMPTY_STATE_SET);
Amith Yamasania95e4882011-08-17 11:41:37 -0700765 invalidate();
766 }
767
768 @Override
Amith Yamasania465b2d2011-08-19 13:01:22 -0700769 protected void onDetachedFromWindow() {
Amith Yamasania95e4882011-08-17 11:41:37 -0700770 removeCallbacks(mUpdateDrawableStateRunnable);
Amith Yamasani87907642011-11-03 11:32:44 -0700771 post(mReleaseCursorRunnable);
Amith Yamasania95e4882011-08-17 11:41:37 -0700772 super.onDetachedFromWindow();
Amith Yamasani79f74302011-03-08 14:16:35 -0800773 }
774
Adam Powellccdd4ee2011-07-27 20:05:14 -0700775 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 sync6a81b822010-09-28 13:00:05 -0700782
Adam Powellccdd4ee2011-07-27 20:05:14 -0700783 if (imm != null) {
Amith Yamasani05944762010-10-08 13:52:38 -0700784 imm.hideSoftInputFromWindow(getWindowToken(), 0);
Amith Yamasani733cbd52010-09-03 12:21:39 -0700785 }
786 }
787 }
788
Amith Yamasanie678f462010-09-15 16:13:43 -0700789 /**
790 * Called by the SuggestionsAdapter
791 * @hide
792 */
793 /* package */void onQueryRefine(CharSequence queryText) {
794 setQuery(queryText);
795 }
796
Amith Yamasani733cbd52010-09-03 12:21:39 -0700797 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 Yamasaniebcf5a3a2010-10-13 11:35:24 -0700806 } else if (v == mVoiceButton) {
807 onVoiceClicked();
Amith Yamasanif28d1872011-07-26 12:21:03 -0700808 } else if (v == mQueryTextView) {
809 forceSuggestionQuery();
Amith Yamasani733cbd52010-09-03 12:21:39 -0700810 }
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 Yamasani93227752010-09-14 10:10:54 -0700833 launchQuerySearch(keyCode, actionKey.getQueryActionMsg(), mQueryTextView.getText()
834 .toString());
Amith Yamasani733cbd52010-09-03 12:21:39 -0700835 return true;
836 }
837
838 return super.onKeyDown(keyCode, event);
839 }
840
Amith Yamasani968ec932010-12-02 14:00:47 -0800841 /**
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 Brown4e6319b2010-12-13 10:36:51 -0800867 if (!mQueryTextView.isEmpty() && event.hasNoModifiers()) {
868 if (event.getAction() == KeyEvent.ACTION_UP) {
869 if (keyCode == KeyEvent.KEYCODE_ENTER) {
870 v.cancelLongPress();
Amith Yamasani968ec932010-12-02 14:00:47 -0800871
Jeff Brown4e6319b2010-12-13 10:36:51 -0800872 // Launch as a regular search.
873 launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null, mQueryTextView.getText()
874 .toString());
875 return true;
876 }
Amith Yamasani968ec932010-12-02 14:00:47 -0800877 }
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 Brown4e6319b2010-12-13 10:36:51 -0800904 if (event.getAction() == KeyEvent.ACTION_DOWN && event.hasNoModifiers()) {
Amith Yamasani968ec932010-12-02 14:00:47 -0800905 // First, check for enter or search (both of which we'll treat as a
906 // "click")
Jeff Brown4e6319b2010-12-13 10:36:51 -0800907 if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_SEARCH
908 || keyCode == KeyEvent.KEYCODE_TAB) {
Amith Yamasani968ec932010-12-02 14:00:47 -0800909 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 Yamasanib4569fb2011-07-08 15:25:39 -0700985 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 Yamasani733cbd52010-09-03 12:21:39 -07001005 private void updateQueryHint() {
1006 if (mQueryHint != null) {
Amith Yamasanib4569fb2011-07-08 15:25:39 -07001007 mQueryTextView.setHint(getDecoratedHint(mQueryHint));
Amith Yamasani733cbd52010-09-03 12:21:39 -07001008 } 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 Yamasanib4569fb2011-07-08 15:25:39 -07001015 mQueryTextView.setHint(getDecoratedHint(hint));
Amith Yamasani733cbd52010-09-03 12:21:39 -07001016 }
Amith Yamasanib4569fb2011-07-08 15:25:39 -07001017 } else {
1018 mQueryTextView.setHint(getDecoratedHint(""));
Amith Yamasani733cbd52010-09-03 12:21:39 -07001019 }
1020 }
1021
1022 /**
1023 * Updates the auto-complete text view.
1024 */
1025 private void updateSearchAutoComplete() {
Amith Yamasani733cbd52010-09-03 12:21:39 -07001026 mQueryTextView.setDropDownAnimationStyle(0); // no animation
Amith Yamasani5931b1f2010-10-18 16:13:14 -07001027 mQueryTextView.setThreshold(mSearchable.getSuggestThreshold());
Amith Yamasani5607a382011-08-09 14:16:37 -07001028 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 Yamasani87907642011-11-03 11:32:44 -07001041 if (mSuggestionsAdapter != null) {
1042 mSuggestionsAdapter.changeCursor(null);
1043 }
Amith Yamasani733cbd52010-09-03 12:21:39 -07001044 // 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 Yamasanie678f462010-09-15 16:13:43 -07001050 ((SuggestionsAdapter) mSuggestionsAdapter).setQueryRefinement(
1051 mQueryRefinement ? SuggestionsAdapter.REFINE_ALL
1052 : SuggestionsAdapter.REFINE_BY_ENTRY);
Amith Yamasani733cbd52010-09-03 12:21:39 -07001053 }
1054 }
1055
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001056 /**
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 Yamasani79f74302011-03-08 14:16:35 -08001060 * criteria apply to make the voice button visible.
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001061 */
1062 private void updateVoiceButton(boolean empty) {
Amith Yamasani79f74302011-03-08 14:16:35 -08001063 int visibility = GONE;
Amith Yamasani167d69a2011-08-12 19:28:37 -07001064 if (mVoiceButtonEnabled && !isIconified() && empty) {
Amith Yamasani9b2e3022011-01-14 11:34:12 -08001065 visibility = VISIBLE;
1066 mSubmitButton.setVisibility(GONE);
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001067 }
1068 mVoiceButton.setVisibility(visibility);
1069 }
1070
Amith Yamasani733cbd52010-09-03 12:21:39 -07001071 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 Yamasani068d73c2011-05-27 15:15:14 -07001084 mUserQuery = text;
Amith Yamasani733cbd52010-09-03 12:21:39 -07001085 boolean hasText = !TextUtils.isEmpty(text);
Amith Yamasanicf72ab42011-11-04 13:49:28 -07001086 updateSubmitButton(hasText);
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001087 updateVoiceButton(!hasText);
Amith Yamasani73e00df2010-12-16 16:31:29 -08001088 updateCloseButton();
Amith Yamasani9b2e3022011-01-14 11:34:12 -08001089 updateSubmitArea();
Amith Yamasanib47c4fd2011-08-04 14:30:07 -07001090 if (mOnQueryChangeListener != null && !TextUtils.equals(newText, mOldQueryText)) {
Adam Powell01f21352011-01-20 18:30:10 -08001091 mOnQueryChangeListener.onQueryTextChange(newText.toString());
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001092 }
Amith Yamasanib47c4fd2011-08-04 14:30:07 -07001093 mOldQueryText = newText.toString();
Amith Yamasani733cbd52010-09-03 12:21:39 -07001094 }
1095
1096 private void onSubmitQuery() {
1097 CharSequence query = mQueryTextView.getText();
Amith Yamasani6a7421b2011-07-27 11:55:53 -07001098 if (query != null && TextUtils.getTrimmedLength(query) > 0) {
Amith Yamasani733cbd52010-09-03 12:21:39 -07001099 if (mOnQueryChangeListener == null
Adam Powell01f21352011-01-20 18:30:10 -08001100 || !mOnQueryChangeListener.onQueryTextSubmit(query.toString())) {
Amith Yamasani733cbd52010-09-03 12:21:39 -07001101 if (mSearchable != null) {
1102 launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null, query.toString());
Amith Yamasani05944762010-10-08 13:52:38 -07001103 setImeVisibility(false);
Amith Yamasani733cbd52010-09-03 12:21:39 -07001104 }
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001105 dismissSuggestions();
Amith Yamasani733cbd52010-09-03 12:21:39 -07001106 }
1107 }
1108 }
1109
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001110 private void dismissSuggestions() {
1111 mQueryTextView.dismissDropDown();
1112 }
1113
Amith Yamasani733cbd52010-09-03 12:21:39 -07001114 private void onCloseClicked() {
Amith Yamasani24652982011-06-23 16:16:05 -07001115 CharSequence text = mQueryTextView.getText();
1116 if (TextUtils.isEmpty(text)) {
1117 if (mIconifiedByDefault) {
Amith Yamasanib4569fb2011-07-08 15:25:39 -07001118 // 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 Yamasani05944762010-10-08 13:52:38 -07001125 }
Amith Yamasani24652982011-06-23 16:16:05 -07001126 } else {
1127 mQueryTextView.setText("");
1128 mQueryTextView.requestFocus();
1129 setImeVisibility(true);
1130 }
1131
Amith Yamasani733cbd52010-09-03 12:21:39 -07001132 }
1133
1134 private void onSearchClicked() {
Amith Yamasani733cbd52010-09-03 12:21:39 -07001135 updateViewsVisibility(false);
Amith Yamasani7f8aef62011-01-25 11:58:09 -08001136 mQueryTextView.requestFocus();
Amith Yamasani05944762010-10-08 13:52:38 -07001137 setImeVisibility(true);
Amith Yamasani48385482010-12-03 14:43:52 -08001138 if (mOnSearchClickListener != null) {
1139 mOnSearchClickListener.onClick(this);
1140 }
Amith Yamasani733cbd52010-09-03 12:21:39 -07001141 }
1142
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001143 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 Yamasani4aedb392010-12-15 16:04:57 -08001166 void onTextFocusChanged() {
Amith Yamasani79f74302011-03-08 14:16:35 -08001167 updateViewsVisibility(isIconified());
Amith Yamasania95e4882011-08-17 11:41:37 -07001168 // 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 Yamasanif28d1872011-07-26 12:21:03 -07001171 if (mQueryTextView.hasFocus()) {
1172 forceSuggestionQuery();
1173 }
Amith Yamasani4aedb392010-12-15 16:04:57 -08001174 }
1175
Amith Yamasanib4569fb2011-07-08 15:25:39 -07001176 @Override
Amith Yamasania95e4882011-08-17 11:41:37 -07001177 public void onWindowFocusChanged(boolean hasWindowFocus) {
1178 super.onWindowFocusChanged(hasWindowFocus);
1179
1180 postUpdateFocusedState();
Amith Yamasanib4569fb2011-07-08 15:25:39 -07001181 }
1182
Amith Yamasani763bc072011-07-22 11:53:47 -07001183 /**
1184 * {@inheritDoc}
1185 */
1186 @Override
1187 public void onActionViewCollapsed() {
Amith Yamasani10da5902011-07-26 16:14:26 -07001188 clearFocus();
1189 updateViewsVisibility(true);
Adam Powell53f56c42011-09-25 13:46:15 -07001190 mQueryTextView.setImeOptions(mCollapsedImeOptions);
Amith Yamasani763bc072011-07-22 11:53:47 -07001191 mExpandedInActionView = false;
1192 }
1193
1194 /**
1195 * {@inheritDoc}
1196 */
1197 @Override
1198 public void onActionViewExpanded() {
Amith Yamasani434c73f2011-11-01 11:44:50 -07001199 if (mExpandedInActionView) return;
1200
Amith Yamasani763bc072011-07-22 11:53:47 -07001201 mExpandedInActionView = true;
Adam Powell53f56c42011-09-25 13:46:15 -07001202 mCollapsedImeOptions = mQueryTextView.getImeOptions();
1203 mQueryTextView.setImeOptions(mCollapsedImeOptions | EditorInfo.IME_FLAG_NO_FULLSCREEN);
Amith Yamasani87907642011-11-03 11:32:44 -07001204 mQueryTextView.setText("");
Amith Yamasani763bc072011-07-22 11:53:47 -07001205 setIconified(false);
1206 }
1207
Amith Yamasanib4569fb2011-07-08 15:25:39 -07001208 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 Yamasani968ec932010-12-02 14:00:47 -08001225 private boolean onItemClicked(int position, int actionKey, String actionMsg) {
1226 if (mOnSuggestionListener == null
Adam Powell01f21352011-01-20 18:30:10 -08001227 || !mOnSuggestionListener.onSuggestionClick(position)) {
Amith Yamasani968ec932010-12-02 14:00:47 -08001228 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 Powell01f21352011-01-20 18:30:10 -08001238 || !mOnSuggestionListener.onSuggestionSelect(position)) {
Amith Yamasani968ec932010-12-02 14:00:47 -08001239 rewriteQueryFromSuggestion(position);
1240 return true;
1241 }
1242 return false;
1243 }
1244
Amith Yamasani733cbd52010-09-03 12:21:39 -07001245 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 Yamasani968ec932010-12-02 14:00:47 -08001251 if (DBG) Log.d(LOG_TAG, "onItemClick() position " + position);
1252 onItemClicked(position, KeyEvent.KEYCODE_UNKNOWN, null);
Amith Yamasani733cbd52010-09-03 12:21:39 -07001253 }
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 Yamasani968ec932010-12-02 14:00:47 -08001262 if (DBG) Log.d(LOG_TAG, "onItemSelected() position " + position);
1263 SearchView.this.onItemSelected(position);
Amith Yamasani733cbd52010-09-03 12:21:39 -07001264 }
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 Yamasanie678f462010-09-15 16:13:43 -07001345 mQueryTextView.setText(query, true);
1346 // Move the cursor to the end
1347 mQueryTextView.setSelection(TextUtils.isEmpty(query) ? 0 : query.length());
Amith Yamasani733cbd52010-09-03 12:21:39 -07001348 }
1349
1350 private void launchQuerySearch(int actionKey, String actionMsg, String query) {
1351 String action = Intent.ACTION_SEARCH;
Amith Yamasanie678f462010-09-15 16:13:43 -07001352 Intent intent = createIntent(action, null, null, query, actionKey, actionMsg);
Amith Yamasani733cbd52010-09-03 12:21:39 -07001353 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 Yamasani733cbd52010-09-03 12:21:39 -07001363 * @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 Yamasanie678f462010-09-15 16:13:43 -07001372 int actionKey, String actionMsg) {
Amith Yamasani733cbd52010-09-03 12:21:39 -07001373 // 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 Yamasani068d73c2011-05-27 15:15:14 -07001382 intent.putExtra(SearchManager.USER_QUERY, mUserQuery);
Amith Yamasani733cbd52010-09-03 12:21:39 -07001383 if (query != null) {
1384 intent.putExtra(SearchManager.QUERY, query);
1385 }
1386 if (extraData != null) {
1387 intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData);
1388 }
Amith Yamasani940ef382011-03-02 18:43:23 -08001389 if (mAppSearchData != null) {
1390 intent.putExtra(SearchManager.APP_DATA, mAppSearchData);
1391 }
Amith Yamasani733cbd52010-09-03 12:21:39 -07001392 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 Yamasaniebcf5a3a2010-10-13 11:35:24 -07001401 * 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 Yamasani733cbd52010-09-03 12:21:39 -07001474 * 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 Yamasani733cbd52010-09-03 12:21:39 -07001491 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 Yamasani733cbd52010-09-03 12:21:39 -07001512 String query = getColumnString(c, SearchManager.SUGGEST_COLUMN_QUERY);
1513 String extraData = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA);
1514
Amith Yamasanie678f462010-09-15 16:13:43 -07001515 return createIntent(action, dataUri, extraData, query, actionKey, actionMsg);
Amith Yamasani733cbd52010-09-03 12:21:39 -07001516 } 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 Yamasanif28d1872011-07-26 12:21:03 -07001529 private void forceSuggestionQuery() {
1530 mQueryTextView.doBeforeTextChanged();
1531 mQueryTextView.doAfterTextChanged();
1532 }
1533
Amith Yamasani968ec932010-12-02 14:00:47 -08001534 static boolean isLandscapeMode(Context context) {
1535 return context.getResources().getConfiguration().orientation
1536 == Configuration.ORIENTATION_LANDSCAPE;
1537 }
1538
Amith Yamasani733cbd52010-09-03 12:21:39 -07001539 /**
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 Yamasani968ec932010-12-02 14:00:47 -08001554
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 Yamasaniacd8d2d2010-12-06 15:50:23 -08001622 if (hasWindowFocus && mSearchView.hasFocus() && getVisibility() == VISIBLE) {
Amith Yamasani968ec932010-12-02 14:00:47 -08001623 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 Yamasani4aedb392010-12-15 16:04:57 -08001634 @Override
1635 protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
1636 super.onFocusChanged(focused, direction, previouslyFocusedRect);
1637 mSearchView.onTextFocusChanged();
1638 }
1639
Amith Yamasani968ec932010-12-02 14:00:47 -08001640 /**
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 Yamasanib4569fb2011-07-08 15:25:39 -07001648
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 Yamasani968ec932010-12-02 14:00:47 -08001675 }
Amith Yamasani05944762010-10-08 13:52:38 -07001676}