blob: 9d2ff2efa3ae755a43f9a8211e0083cfc1539a5d [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 *
Joe Fernandez3aef8e1d2011-12-20 10:38:34 -080078 * <div class="special reference">
79 * <h3>Developer Guides</h3>
80 * <p>For information about using {@code SearchView}, read the
81 * <a href="{@docRoot}guide/topics/search/index.html">Search</a> developer guide.</p>
82 * </div>
Amith Yamasani763bc072011-07-22 11:53:47 -070083 *
84 * @see android.view.MenuItem#SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW
Amith Yamasani5931b1f2010-10-18 16:13:14 -070085 * @attr ref android.R.styleable#SearchView_iconifiedByDefault
Amith Yamasani5607a382011-08-09 14:16:37 -070086 * @attr ref android.R.styleable#SearchView_imeOptions
87 * @attr ref android.R.styleable#SearchView_inputType
Amith Yamasani5931b1f2010-10-18 16:13:14 -070088 * @attr ref android.R.styleable#SearchView_maxWidth
Scott Mainabdf0d52011-02-08 10:20:27 -080089 * @attr ref android.R.styleable#SearchView_queryHint
Amith Yamasani733cbd52010-09-03 12:21:39 -070090 */
Amith Yamasani763bc072011-07-22 11:53:47 -070091public class SearchView extends LinearLayout implements CollapsibleActionView {
Amith Yamasani733cbd52010-09-03 12:21:39 -070092
93 private static final boolean DBG = false;
94 private static final String LOG_TAG = "SearchView";
95
Luca Zanolin535698c2011-10-06 13:36:15 +010096 /**
97 * Private constant for removing the microphone in the keyboard.
98 */
99 private static final String IME_OPTION_NO_MICROPHONE = "nm";
100
Adam Powell01f21352011-01-20 18:30:10 -0800101 private OnQueryTextListener mOnQueryChangeListener;
Amith Yamasani733cbd52010-09-03 12:21:39 -0700102 private OnCloseListener mOnCloseListener;
Amith Yamasani05944762010-10-08 13:52:38 -0700103 private OnFocusChangeListener mOnQueryTextFocusChangeListener;
Adam Powell01f21352011-01-20 18:30:10 -0800104 private OnSuggestionListener mOnSuggestionListener;
Amith Yamasani48385482010-12-03 14:43:52 -0800105 private OnClickListener mOnSearchClickListener;
Amith Yamasani733cbd52010-09-03 12:21:39 -0700106
107 private boolean mIconifiedByDefault;
Amith Yamasani93227752010-09-14 10:10:54 -0700108 private boolean mIconified;
Amith Yamasani733cbd52010-09-03 12:21:39 -0700109 private CursorAdapter mSuggestionsAdapter;
110 private View mSearchButton;
111 private View mSubmitButton;
Amith Yamasani79f74302011-03-08 14:16:35 -0800112 private View mSearchPlate;
Amith Yamasani9b2e3022011-01-14 11:34:12 -0800113 private View mSubmitArea;
Amith Yamasani4aedb392010-12-15 16:04:57 -0800114 private ImageView mCloseButton;
Amith Yamasani733cbd52010-09-03 12:21:39 -0700115 private View mSearchEditFrame;
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -0700116 private View mVoiceButton;
Amith Yamasani968ec932010-12-02 14:00:47 -0800117 private SearchAutoComplete mQueryTextView;
Amith Yamasanib4569fb2011-07-08 15:25:39 -0700118 private View mDropDownAnchor;
119 private ImageView mSearchHintIcon;
Amith Yamasani733cbd52010-09-03 12:21:39 -0700120 private boolean mSubmitButtonEnabled;
121 private CharSequence mQueryHint;
Amith Yamasanie678f462010-09-15 16:13:43 -0700122 private boolean mQueryRefinement;
Amith Yamasani05944762010-10-08 13:52:38 -0700123 private boolean mClearingFocus;
Amith Yamasani5931b1f2010-10-18 16:13:14 -0700124 private int mMaxWidth;
Amith Yamasani9b2e3022011-01-14 11:34:12 -0800125 private boolean mVoiceButtonEnabled;
Amith Yamasanib47c4fd2011-08-04 14:30:07 -0700126 private CharSequence mOldQueryText;
Amith Yamasani068d73c2011-05-27 15:15:14 -0700127 private CharSequence mUserQuery;
Amith Yamasani763bc072011-07-22 11:53:47 -0700128 private boolean mExpandedInActionView;
Adam Powell53f56c42011-09-25 13:46:15 -0700129 private int mCollapsedImeOptions;
Amith Yamasani733cbd52010-09-03 12:21:39 -0700130
131 private SearchableInfo mSearchable;
Amith Yamasani940ef382011-03-02 18:43:23 -0800132 private Bundle mAppSearchData;
Amith Yamasani733cbd52010-09-03 12:21:39 -0700133
Adam Powellccdd4ee2011-07-27 20:05:14 -0700134 /*
135 * SearchView can be set expanded before the IME is ready to be shown during
136 * initial UI setup. The show operation is asynchronous to account for this.
137 */
138 private Runnable mShowImeRunnable = new Runnable() {
139 public void run() {
140 InputMethodManager imm = (InputMethodManager)
141 getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
142
143 if (imm != null) {
144 imm.showSoftInputUnchecked(0, null);
145 }
146 }
147 };
148
Amith Yamasania95e4882011-08-17 11:41:37 -0700149 private Runnable mUpdateDrawableStateRunnable = new Runnable() {
150 public void run() {
151 updateFocusedState();
152 }
153 };
154
Amith Yamasani87907642011-11-03 11:32:44 -0700155 private Runnable mReleaseCursorRunnable = new Runnable() {
156 public void run() {
157 if (mSuggestionsAdapter != null && mSuggestionsAdapter instanceof SuggestionsAdapter) {
158 mSuggestionsAdapter.changeCursor(null);
159 }
160 }
161 };
162
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -0700163 // For voice searching
164 private final Intent mVoiceWebSearchIntent;
165 private final Intent mVoiceAppSearchIntent;
166
Amith Yamasani733cbd52010-09-03 12:21:39 -0700167 // A weak map of drawables we've gotten from other packages, so we don't load them
168 // more than once.
169 private final WeakHashMap<String, Drawable.ConstantState> mOutsideDrawablesCache =
170 new WeakHashMap<String, Drawable.ConstantState>();
171
172 /**
173 * Callbacks for changes to the query text.
174 */
Adam Powell01f21352011-01-20 18:30:10 -0800175 public interface OnQueryTextListener {
Amith Yamasani733cbd52010-09-03 12:21:39 -0700176
177 /**
178 * Called when the user submits the query. This could be due to a key press on the
179 * keyboard or due to pressing a submit button.
180 * The listener can override the standard behavior by returning true
181 * to indicate that it has handled the submit request. Otherwise return false to
182 * let the SearchView handle the submission by launching any associated intent.
183 *
184 * @param query the query text that is to be submitted
185 *
186 * @return true if the query has been handled by the listener, false to let the
187 * SearchView perform the default action.
188 */
Adam Powell01f21352011-01-20 18:30:10 -0800189 boolean onQueryTextSubmit(String query);
Amith Yamasani733cbd52010-09-03 12:21:39 -0700190
191 /**
192 * Called when the query text is changed by the user.
193 *
194 * @param newText the new content of the query text field.
195 *
196 * @return false if the SearchView should perform the default action of showing any
197 * suggestions if available, true if the action was handled by the listener.
198 */
Adam Powell01f21352011-01-20 18:30:10 -0800199 boolean onQueryTextChange(String newText);
Amith Yamasani733cbd52010-09-03 12:21:39 -0700200 }
201
202 public interface OnCloseListener {
203
204 /**
205 * The user is attempting to close the SearchView.
206 *
207 * @return true if the listener wants to override the default behavior of clearing the
208 * text field and dismissing it, false otherwise.
209 */
210 boolean onClose();
211 }
212
Amith Yamasani05944762010-10-08 13:52:38 -0700213 /**
214 * Callback interface for selection events on suggestions. These callbacks
215 * are only relevant when a SearchableInfo has been specified by {@link #setSearchableInfo}.
216 */
Adam Powell01f21352011-01-20 18:30:10 -0800217 public interface OnSuggestionListener {
Amith Yamasani05944762010-10-08 13:52:38 -0700218
219 /**
220 * Called when a suggestion was selected by navigating to it.
221 * @param position the absolute position in the list of suggestions.
222 *
223 * @return true if the listener handles the event and wants to override the default
224 * behavior of possibly rewriting the query based on the selected item, false otherwise.
225 */
Adam Powell01f21352011-01-20 18:30:10 -0800226 boolean onSuggestionSelect(int position);
Amith Yamasani05944762010-10-08 13:52:38 -0700227
228 /**
229 * Called when a suggestion was clicked.
230 * @param position the absolute position of the clicked item in the list of suggestions.
231 *
232 * @return true if the listener handles the event and wants to override the default
233 * behavior of launching any intent or submitting a search query specified on that item.
234 * Return false otherwise.
235 */
Adam Powell01f21352011-01-20 18:30:10 -0800236 boolean onSuggestionClick(int position);
Amith Yamasani05944762010-10-08 13:52:38 -0700237 }
238
Amith Yamasani733cbd52010-09-03 12:21:39 -0700239 public SearchView(Context context) {
240 this(context, null);
241 }
242
243 public SearchView(Context context, AttributeSet attrs) {
244 super(context, attrs);
245
246 LayoutInflater inflater = (LayoutInflater) context
247 .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
248 inflater.inflate(R.layout.search_view, this, true);
249
250 mSearchButton = findViewById(R.id.search_button);
Amith Yamasani968ec932010-12-02 14:00:47 -0800251 mQueryTextView = (SearchAutoComplete) findViewById(R.id.search_src_text);
252 mQueryTextView.setSearchView(this);
253
Amith Yamasani733cbd52010-09-03 12:21:39 -0700254 mSearchEditFrame = findViewById(R.id.search_edit_frame);
Amith Yamasani79f74302011-03-08 14:16:35 -0800255 mSearchPlate = findViewById(R.id.search_plate);
Amith Yamasani9b2e3022011-01-14 11:34:12 -0800256 mSubmitArea = findViewById(R.id.submit_area);
Amith Yamasani733cbd52010-09-03 12:21:39 -0700257 mSubmitButton = findViewById(R.id.search_go_btn);
Amith Yamasani4aedb392010-12-15 16:04:57 -0800258 mCloseButton = (ImageView) findViewById(R.id.search_close_btn);
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -0700259 mVoiceButton = findViewById(R.id.search_voice_btn);
Amith Yamasanib4569fb2011-07-08 15:25:39 -0700260 mSearchHintIcon = (ImageView) findViewById(R.id.search_mag_icon);
Amith Yamasani733cbd52010-09-03 12:21:39 -0700261
262 mSearchButton.setOnClickListener(mOnClickListener);
263 mCloseButton.setOnClickListener(mOnClickListener);
264 mSubmitButton.setOnClickListener(mOnClickListener);
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -0700265 mVoiceButton.setOnClickListener(mOnClickListener);
Amith Yamasanif28d1872011-07-26 12:21:03 -0700266 mQueryTextView.setOnClickListener(mOnClickListener);
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -0700267
Amith Yamasani733cbd52010-09-03 12:21:39 -0700268 mQueryTextView.addTextChangedListener(mTextWatcher);
269 mQueryTextView.setOnEditorActionListener(mOnEditorActionListener);
270 mQueryTextView.setOnItemClickListener(mOnItemClickListener);
271 mQueryTextView.setOnItemSelectedListener(mOnItemSelectedListener);
Amith Yamasani968ec932010-12-02 14:00:47 -0800272 mQueryTextView.setOnKeyListener(mTextKeyListener);
Luca Zanolin535698c2011-10-06 13:36:15 +0100273 // Inform any listener of focus changes
Amith Yamasani05944762010-10-08 13:52:38 -0700274 mQueryTextView.setOnFocusChangeListener(new OnFocusChangeListener() {
275
276 public void onFocusChange(View v, boolean hasFocus) {
277 if (mOnQueryTextFocusChangeListener != null) {
278 mOnQueryTextFocusChangeListener.onFocusChange(SearchView.this, hasFocus);
279 }
280 }
281 });
Amith Yamasani733cbd52010-09-03 12:21:39 -0700282
Amith Yamasani733cbd52010-09-03 12:21:39 -0700283 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SearchView, 0, 0);
284 setIconifiedByDefault(a.getBoolean(R.styleable.SearchView_iconifiedByDefault, true));
Amith Yamasani5931b1f2010-10-18 16:13:14 -0700285 int maxWidth = a.getDimensionPixelSize(R.styleable.SearchView_maxWidth, -1);
286 if (maxWidth != -1) {
287 setMaxWidth(maxWidth);
288 }
Adam Powellc0171d52011-01-13 14:31:17 -0800289 CharSequence queryHint = a.getText(R.styleable.SearchView_queryHint);
290 if (!TextUtils.isEmpty(queryHint)) {
291 setQueryHint(queryHint);
292 }
Amith Yamasani5607a382011-08-09 14:16:37 -0700293 int imeOptions = a.getInt(R.styleable.SearchView_imeOptions, -1);
294 if (imeOptions != -1) {
295 setImeOptions(imeOptions);
296 }
297 int inputType = a.getInt(R.styleable.SearchView_inputType, -1);
298 if (inputType != -1) {
299 setInputType(inputType);
300 }
301
Amith Yamasani733cbd52010-09-03 12:21:39 -0700302 a.recycle();
303
Amith Yamasani5607a382011-08-09 14:16:37 -0700304 boolean focusable = true;
305
Amith Yamasani7f8aef62011-01-25 11:58:09 -0800306 a = context.obtainStyledAttributes(attrs, R.styleable.View, 0, 0);
307 focusable = a.getBoolean(R.styleable.View_focusable, focusable);
308 a.recycle();
309 setFocusable(focusable);
310
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -0700311 // Save voice intent for later queries/launching
312 mVoiceWebSearchIntent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH);
313 mVoiceWebSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
314 mVoiceWebSearchIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
315 RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH);
316
317 mVoiceAppSearchIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
318 mVoiceAppSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
319
Amith Yamasanib4569fb2011-07-08 15:25:39 -0700320 mDropDownAnchor = findViewById(mQueryTextView.getDropDownAnchor());
321 if (mDropDownAnchor != null) {
322 mDropDownAnchor.addOnLayoutChangeListener(new OnLayoutChangeListener() {
323 @Override
324 public void onLayoutChange(View v, int left, int top, int right, int bottom,
325 int oldLeft, int oldTop, int oldRight, int oldBottom) {
326 adjustDropDownSizeAndPosition();
327 }
328
329 });
330 }
331
Amith Yamasani733cbd52010-09-03 12:21:39 -0700332 updateViewsVisibility(mIconifiedByDefault);
Amith Yamasanib4569fb2011-07-08 15:25:39 -0700333 updateQueryHint();
Amith Yamasani733cbd52010-09-03 12:21:39 -0700334 }
335
336 /**
337 * Sets the SearchableInfo for this SearchView. Properties in the SearchableInfo are used
338 * to display labels, hints, suggestions, create intents for launching search results screens
339 * and controlling other affordances such as a voice button.
340 *
341 * @param searchable a SearchableInfo can be retrieved from the SearchManager, for a specific
342 * activity or a global search provider.
343 */
344 public void setSearchableInfo(SearchableInfo searchable) {
345 mSearchable = searchable;
346 if (mSearchable != null) {
347 updateSearchAutoComplete();
Amith Yamasani79f74302011-03-08 14:16:35 -0800348 updateQueryHint();
Amith Yamasani733cbd52010-09-03 12:21:39 -0700349 }
Amith Yamasani9b2e3022011-01-14 11:34:12 -0800350 // Cache the voice search capability
351 mVoiceButtonEnabled = hasVoiceSearch();
Luca Zanolin535698c2011-10-06 13:36:15 +0100352
353 if (mVoiceButtonEnabled) {
354 // Disable the microphone on the keyboard, as a mic is displayed near the text box
355 // TODO: use imeOptions to disable voice input when the new API will be available
356 mQueryTextView.setPrivateImeOptions(IME_OPTION_NO_MICROPHONE);
357 }
Amith Yamasanib4569fb2011-07-08 15:25:39 -0700358 updateViewsVisibility(isIconified());
Amith Yamasani733cbd52010-09-03 12:21:39 -0700359 }
360
Amith Yamasani940ef382011-03-02 18:43:23 -0800361 /**
362 * Sets the APP_DATA for legacy SearchDialog use.
363 * @param appSearchData bundle provided by the app when launching the search dialog
364 * @hide
365 */
366 public void setAppSearchData(Bundle appSearchData) {
367 mAppSearchData = appSearchData;
368 }
369
Amith Yamasani5607a382011-08-09 14:16:37 -0700370 /**
371 * Sets the IME options on the query text field.
372 *
373 * @see TextView#setImeOptions(int)
374 * @param imeOptions the options to set on the query text field
375 *
376 * @attr ref android.R.styleable#SearchView_imeOptions
377 */
378 public void setImeOptions(int imeOptions) {
379 mQueryTextView.setImeOptions(imeOptions);
380 }
381
382 /**
383 * Sets the input type on the query text field.
384 *
385 * @see TextView#setInputType(int)
386 * @param inputType the input type to set on the query text field
387 *
388 * @attr ref android.R.styleable#SearchView_inputType
389 */
390 public void setInputType(int inputType) {
391 mQueryTextView.setInputType(inputType);
392 }
393
Amith Yamasani05944762010-10-08 13:52:38 -0700394 /** @hide */
395 @Override
396 public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
Amith Yamasani7f8aef62011-01-25 11:58:09 -0800397 // Don't accept focus if in the middle of clearing focus
398 if (mClearingFocus) return false;
399 // Check if SearchView is focusable.
400 if (!isFocusable()) return false;
401 // If it is not iconified, then give the focus to the text field
402 if (!isIconified()) {
403 boolean result = mQueryTextView.requestFocus(direction, previouslyFocusedRect);
Amith Yamasanif28d1872011-07-26 12:21:03 -0700404 if (result) {
405 updateViewsVisibility(false);
406 }
Amith Yamasani7f8aef62011-01-25 11:58:09 -0800407 return result;
408 } else {
409 return super.requestFocus(direction, previouslyFocusedRect);
410 }
Amith Yamasani05944762010-10-08 13:52:38 -0700411 }
412
413 /** @hide */
414 @Override
415 public void clearFocus() {
416 mClearingFocus = true;
Amith Yamasani10da5902011-07-26 16:14:26 -0700417 setImeVisibility(false);
Amith Yamasani05944762010-10-08 13:52:38 -0700418 super.clearFocus();
419 mQueryTextView.clearFocus();
Amith Yamasani05944762010-10-08 13:52:38 -0700420 mClearingFocus = false;
421 }
422
Amith Yamasani733cbd52010-09-03 12:21:39 -0700423 /**
424 * Sets a listener for user actions within the SearchView.
425 *
426 * @param listener the listener object that receives callbacks when the user performs
427 * actions in the SearchView such as clicking on buttons or typing a query.
428 */
Adam Powell01f21352011-01-20 18:30:10 -0800429 public void setOnQueryTextListener(OnQueryTextListener listener) {
Amith Yamasani733cbd52010-09-03 12:21:39 -0700430 mOnQueryChangeListener = listener;
431 }
432
433 /**
Amith Yamasani93227752010-09-14 10:10:54 -0700434 * Sets a listener to inform when the user closes the SearchView.
435 *
436 * @param listener the listener to call when the user closes the SearchView.
437 */
438 public void setOnCloseListener(OnCloseListener listener) {
439 mOnCloseListener = listener;
440 }
441
442 /**
Amith Yamasani05944762010-10-08 13:52:38 -0700443 * Sets a listener to inform when the focus of the query text field changes.
444 *
445 * @param listener the listener to inform of focus changes.
446 */
447 public void setOnQueryTextFocusChangeListener(OnFocusChangeListener listener) {
448 mOnQueryTextFocusChangeListener = listener;
449 }
450
451 /**
452 * Sets a listener to inform when a suggestion is focused or clicked.
453 *
454 * @param listener the listener to inform of suggestion selection events.
455 */
Adam Powell01f21352011-01-20 18:30:10 -0800456 public void setOnSuggestionListener(OnSuggestionListener listener) {
Amith Yamasani05944762010-10-08 13:52:38 -0700457 mOnSuggestionListener = listener;
458 }
459
460 /**
Amith Yamasani48385482010-12-03 14:43:52 -0800461 * Sets a listener to inform when the search button is pressed. This is only
Scott Maincccdbe92011-02-06 15:51:47 -0800462 * relevant when the text field is not visible by default. Calling {@link #setIconified
463 * setIconified(false)} can also cause this listener to be informed.
Amith Yamasani48385482010-12-03 14:43:52 -0800464 *
465 * @param listener the listener to inform when the search button is clicked or
466 * the text field is programmatically de-iconified.
467 */
468 public void setOnSearchClickListener(OnClickListener listener) {
469 mOnSearchClickListener = listener;
470 }
471
472 /**
473 * Returns the query string currently in the text field.
474 *
475 * @return the query string
476 */
477 public CharSequence getQuery() {
478 return mQueryTextView.getText();
479 }
480
481 /**
Amith Yamasani733cbd52010-09-03 12:21:39 -0700482 * Sets a query string in the text field and optionally submits the query as well.
483 *
484 * @param query the query string. This replaces any query text already present in the
485 * text field.
486 * @param submit whether to submit the query right now or only update the contents of
487 * text field.
488 */
489 public void setQuery(CharSequence query, boolean submit) {
490 mQueryTextView.setText(query);
Dmitri Plotnikov87c50252010-10-21 21:16:42 -0700491 if (query != null) {
492 mQueryTextView.setSelection(query.length());
Amith Yamasani068d73c2011-05-27 15:15:14 -0700493 mUserQuery = query;
Dmitri Plotnikov87c50252010-10-21 21:16:42 -0700494 }
495
Amith Yamasani733cbd52010-09-03 12:21:39 -0700496 // If the query is not empty and submit is requested, submit the query
497 if (submit && !TextUtils.isEmpty(query)) {
498 onSubmitQuery();
499 }
500 }
501
502 /**
503 * Sets the hint text to display in the query text field. This overrides any hint specified
504 * in the SearchableInfo.
505 *
506 * @param hint the hint text to display
Scott Mainabdf0d52011-02-08 10:20:27 -0800507 *
508 * @attr ref android.R.styleable#SearchView_queryHint
Amith Yamasani733cbd52010-09-03 12:21:39 -0700509 */
510 public void setQueryHint(CharSequence hint) {
511 mQueryHint = hint;
512 updateQueryHint();
513 }
514
515 /**
516 * Sets the default or resting state of the search field. If true, a single search icon is
517 * shown by default and expands to show the text field and other buttons when pressed. Also,
518 * if the default state is iconified, then it collapses to that state when the close button
Amith Yamasani93227752010-09-14 10:10:54 -0700519 * is pressed. Changes to this property will take effect immediately.
Amith Yamasani733cbd52010-09-03 12:21:39 -0700520 *
Scott Maincccdbe92011-02-06 15:51:47 -0800521 * <p>The default value is true.</p>
Amith Yamasani93227752010-09-14 10:10:54 -0700522 *
523 * @param iconified whether the search field should be iconified by default
Scott Mainabdf0d52011-02-08 10:20:27 -0800524 *
525 * @attr ref android.R.styleable#SearchView_iconifiedByDefault
Amith Yamasani733cbd52010-09-03 12:21:39 -0700526 */
527 public void setIconifiedByDefault(boolean iconified) {
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -0700528 if (mIconifiedByDefault == iconified) return;
Amith Yamasani733cbd52010-09-03 12:21:39 -0700529 mIconifiedByDefault = iconified;
530 updateViewsVisibility(iconified);
Amith Yamasanib47c4fd2011-08-04 14:30:07 -0700531 updateQueryHint();
Amith Yamasani733cbd52010-09-03 12:21:39 -0700532 }
533
Amith Yamasani93227752010-09-14 10:10:54 -0700534 /**
535 * Returns the default iconified state of the search field.
536 * @return
537 */
Amith Yamasani733cbd52010-09-03 12:21:39 -0700538 public boolean isIconfiedByDefault() {
539 return mIconifiedByDefault;
540 }
541
542 /**
Amith Yamasani93227752010-09-14 10:10:54 -0700543 * Iconifies or expands the SearchView. Any query text is cleared when iconified. This is
544 * a temporary state and does not override the default iconified state set by
545 * {@link #setIconifiedByDefault(boolean)}. If the default state is iconified, then
546 * a false here will only be valid until the user closes the field. And if the default
547 * state is expanded, then a true here will only clear the text field and not close it.
548 *
549 * @param iconify a true value will collapse the SearchView to an icon, while a false will
550 * expand it.
551 */
552 public void setIconified(boolean iconify) {
553 if (iconify) {
554 onCloseClicked();
555 } else {
556 onSearchClicked();
557 }
558 }
559
560 /**
561 * Returns the current iconified state of the SearchView.
562 *
563 * @return true if the SearchView is currently iconified, false if the search field is
564 * fully visible.
565 */
566 public boolean isIconified() {
567 return mIconified;
568 }
569
570 /**
Amith Yamasani733cbd52010-09-03 12:21:39 -0700571 * Enables showing a submit button when the query is non-empty. In cases where the SearchView
572 * is being used to filter the contents of the current activity and doesn't launch a separate
573 * results activity, then the submit button should be disabled.
574 *
575 * @param enabled true to show a submit button for submitting queries, false if a submit
576 * button is not required.
577 */
578 public void setSubmitButtonEnabled(boolean enabled) {
Amith Yamasani733cbd52010-09-03 12:21:39 -0700579 mSubmitButtonEnabled = enabled;
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -0700580 updateViewsVisibility(isIconified());
Amith Yamasani733cbd52010-09-03 12:21:39 -0700581 }
582
583 /**
584 * Returns whether the submit button is enabled when necessary or never displayed.
585 *
586 * @return whether the submit button is enabled automatically when necessary
587 */
588 public boolean isSubmitButtonEnabled() {
589 return mSubmitButtonEnabled;
590 }
591
Amith Yamasanie678f462010-09-15 16:13:43 -0700592 /**
593 * Specifies if a query refinement button should be displayed alongside each suggestion
594 * or if it should depend on the flags set in the individual items retrieved from the
595 * suggestions provider. Clicking on the query refinement button will replace the text
596 * in the query text field with the text from the suggestion. This flag only takes effect
597 * if a SearchableInfo has been specified with {@link #setSearchableInfo(SearchableInfo)}
598 * and not when using a custom adapter.
599 *
600 * @param enable true if all items should have a query refinement button, false if only
601 * those items that have a query refinement flag set should have the button.
602 *
603 * @see SearchManager#SUGGEST_COLUMN_FLAGS
604 * @see SearchManager#FLAG_QUERY_REFINEMENT
605 */
606 public void setQueryRefinementEnabled(boolean enable) {
607 mQueryRefinement = enable;
608 if (mSuggestionsAdapter instanceof SuggestionsAdapter) {
609 ((SuggestionsAdapter) mSuggestionsAdapter).setQueryRefinement(
610 enable ? SuggestionsAdapter.REFINE_ALL : SuggestionsAdapter.REFINE_BY_ENTRY);
611 }
612 }
613
614 /**
615 * Returns whether query refinement is enabled for all items or only specific ones.
616 * @return true if enabled for all items, false otherwise.
617 */
618 public boolean isQueryRefinementEnabled() {
619 return mQueryRefinement;
Amith Yamasani733cbd52010-09-03 12:21:39 -0700620 }
621
622 /**
623 * You can set a custom adapter if you wish. Otherwise the default adapter is used to
624 * display the suggestions from the suggestions provider associated with the SearchableInfo.
625 *
626 * @see #setSearchableInfo(SearchableInfo)
627 */
628 public void setSuggestionsAdapter(CursorAdapter adapter) {
629 mSuggestionsAdapter = adapter;
630
631 mQueryTextView.setAdapter(mSuggestionsAdapter);
632 }
633
634 /**
635 * Returns the adapter used for suggestions, if any.
636 * @return the suggestions adapter
637 */
638 public CursorAdapter getSuggestionsAdapter() {
639 return mSuggestionsAdapter;
640 }
641
Amith Yamasani5931b1f2010-10-18 16:13:14 -0700642 /**
643 * Makes the view at most this many pixels wide
644 *
645 * @attr ref android.R.styleable#SearchView_maxWidth
646 */
647 public void setMaxWidth(int maxpixels) {
648 mMaxWidth = maxpixels;
649
650 requestLayout();
651 }
652
653 @Override
654 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Amith Yamasania95e4882011-08-17 11:41:37 -0700655 // Let the standard measurements take effect in iconified state.
656 if (isIconified()) {
657 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
658 return;
659 }
660
Amith Yamasani5931b1f2010-10-18 16:13:14 -0700661 int widthMode = MeasureSpec.getMode(widthMeasureSpec);
662 int width = MeasureSpec.getSize(widthMeasureSpec);
663
Amith Yamasani167d69a2011-08-12 19:28:37 -0700664 switch (widthMode) {
665 case MeasureSpec.AT_MOST:
666 // If there is an upper limit, don't exceed maximum width (explicit or implicit)
667 if (mMaxWidth > 0) {
668 width = Math.min(mMaxWidth, width);
669 } else {
670 width = Math.min(getPreferredWidth(), width);
671 }
672 break;
673 case MeasureSpec.EXACTLY:
674 // If an exact width is specified, still don't exceed any specified maximum width
675 if (mMaxWidth > 0) {
676 width = Math.min(mMaxWidth, width);
677 }
678 break;
679 case MeasureSpec.UNSPECIFIED:
680 // Use maximum width, if specified, else preferred width
681 width = mMaxWidth > 0 ? mMaxWidth : getPreferredWidth();
682 break;
Amith Yamasani5931b1f2010-10-18 16:13:14 -0700683 }
Amith Yamasani167d69a2011-08-12 19:28:37 -0700684 widthMode = MeasureSpec.EXACTLY;
685 super.onMeasure(MeasureSpec.makeMeasureSpec(width, widthMode), heightMeasureSpec);
686 }
687
688 private int getPreferredWidth() {
689 return getContext().getResources()
690 .getDimensionPixelSize(R.dimen.search_view_preferred_width);
Amith Yamasani5931b1f2010-10-18 16:13:14 -0700691 }
692
Amith Yamasani93227752010-09-14 10:10:54 -0700693 private void updateViewsVisibility(final boolean collapsed) {
694 mIconified = collapsed;
Amith Yamasani733cbd52010-09-03 12:21:39 -0700695 // Visibility of views that are visible when collapsed
Amith Yamasani93227752010-09-14 10:10:54 -0700696 final int visCollapsed = collapsed ? VISIBLE : GONE;
Amith Yamasani05944762010-10-08 13:52:38 -0700697 // Is there text in the query
698 final boolean hasText = !TextUtils.isEmpty(mQueryTextView.getText());
Amith Yamasani733cbd52010-09-03 12:21:39 -0700699
700 mSearchButton.setVisibility(visCollapsed);
Amith Yamasani9b2e3022011-01-14 11:34:12 -0800701 updateSubmitButton(hasText);
702 mSearchEditFrame.setVisibility(collapsed ? GONE : VISIBLE);
Amith Yamasanib4569fb2011-07-08 15:25:39 -0700703 mSearchHintIcon.setVisibility(mIconifiedByDefault ? GONE : VISIBLE);
Amith Yamasani4aedb392010-12-15 16:04:57 -0800704 updateCloseButton();
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -0700705 updateVoiceButton(!hasText);
Amith Yamasani9b2e3022011-01-14 11:34:12 -0800706 updateSubmitArea();
Amith Yamasani9b2e3022011-01-14 11:34:12 -0800707 }
708
709 private boolean hasVoiceSearch() {
710 if (mSearchable != null && mSearchable.getVoiceSearchEnabled()) {
711 Intent testIntent = null;
712 if (mSearchable.getVoiceSearchLaunchWebSearch()) {
713 testIntent = mVoiceWebSearchIntent;
714 } else if (mSearchable.getVoiceSearchLaunchRecognizer()) {
715 testIntent = mVoiceAppSearchIntent;
716 }
717 if (testIntent != null) {
718 ResolveInfo ri = getContext().getPackageManager().resolveActivity(testIntent,
719 PackageManager.MATCH_DEFAULT_ONLY);
720 return ri != null;
721 }
722 }
723 return false;
724 }
725
726 private boolean isSubmitAreaEnabled() {
727 return (mSubmitButtonEnabled || mVoiceButtonEnabled) && !isIconified();
728 }
729
730 private void updateSubmitButton(boolean hasText) {
Amith Yamasani79f74302011-03-08 14:16:35 -0800731 int visibility = GONE;
Amith Yamasanicf72ab42011-11-04 13:49:28 -0700732 if (mSubmitButtonEnabled && isSubmitAreaEnabled() && hasFocus()
733 && (hasText || !mVoiceButtonEnabled)) {
Amith Yamasani79f74302011-03-08 14:16:35 -0800734 visibility = VISIBLE;
735 }
736 mSubmitButton.setVisibility(visibility);
Amith Yamasani9b2e3022011-01-14 11:34:12 -0800737 }
738
739 private void updateSubmitArea() {
740 int visibility = GONE;
Amith Yamasani79f74302011-03-08 14:16:35 -0800741 if (isSubmitAreaEnabled()
742 && (mSubmitButton.getVisibility() == VISIBLE
743 || mVoiceButton.getVisibility() == VISIBLE)) {
744 visibility = VISIBLE;
Amith Yamasani9b2e3022011-01-14 11:34:12 -0800745 }
746 mSubmitArea.setVisibility(visibility);
Amith Yamasani733cbd52010-09-03 12:21:39 -0700747 }
748
Amith Yamasani4aedb392010-12-15 16:04:57 -0800749 private void updateCloseButton() {
750 final boolean hasText = !TextUtils.isEmpty(mQueryTextView.getText());
751 // Should we show the close button? It is not shown if there's no focus,
752 // field is not iconified by default and there is no text in it.
Amith Yamasani763bc072011-07-22 11:53:47 -0700753 final boolean showClose = hasText || (mIconifiedByDefault && !mExpandedInActionView);
Amith Yamasani167d69a2011-08-12 19:28:37 -0700754 mCloseButton.setVisibility(showClose ? VISIBLE : GONE);
Amith Yamasani4aedb392010-12-15 16:04:57 -0800755 mCloseButton.getDrawable().setState(hasText ? ENABLED_STATE_SET : EMPTY_STATE_SET);
756 }
757
Amith Yamasania95e4882011-08-17 11:41:37 -0700758 private void postUpdateFocusedState() {
759 post(mUpdateDrawableStateRunnable);
760 }
761
762 private void updateFocusedState() {
763 boolean focused = mQueryTextView.hasFocus();
Amith Yamasani79f74302011-03-08 14:16:35 -0800764 mSearchPlate.getBackground().setState(focused ? FOCUSED_STATE_SET : EMPTY_STATE_SET);
765 mSubmitArea.getBackground().setState(focused ? FOCUSED_STATE_SET : EMPTY_STATE_SET);
Amith Yamasania95e4882011-08-17 11:41:37 -0700766 invalidate();
767 }
768
769 @Override
Amith Yamasania465b2d2011-08-19 13:01:22 -0700770 protected void onDetachedFromWindow() {
Amith Yamasania95e4882011-08-17 11:41:37 -0700771 removeCallbacks(mUpdateDrawableStateRunnable);
Amith Yamasani87907642011-11-03 11:32:44 -0700772 post(mReleaseCursorRunnable);
Amith Yamasania95e4882011-08-17 11:41:37 -0700773 super.onDetachedFromWindow();
Amith Yamasani79f74302011-03-08 14:16:35 -0800774 }
775
Adam Powellccdd4ee2011-07-27 20:05:14 -0700776 private void setImeVisibility(final boolean visible) {
777 if (visible) {
778 post(mShowImeRunnable);
779 } else {
780 removeCallbacks(mShowImeRunnable);
781 InputMethodManager imm = (InputMethodManager)
782 getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
repo sync6a81b822010-09-28 13:00:05 -0700783
Adam Powellccdd4ee2011-07-27 20:05:14 -0700784 if (imm != null) {
Amith Yamasani05944762010-10-08 13:52:38 -0700785 imm.hideSoftInputFromWindow(getWindowToken(), 0);
Amith Yamasani733cbd52010-09-03 12:21:39 -0700786 }
787 }
788 }
789
Amith Yamasanie678f462010-09-15 16:13:43 -0700790 /**
791 * Called by the SuggestionsAdapter
792 * @hide
793 */
794 /* package */void onQueryRefine(CharSequence queryText) {
795 setQuery(queryText);
796 }
797
Amith Yamasani733cbd52010-09-03 12:21:39 -0700798 private final OnClickListener mOnClickListener = new OnClickListener() {
799
800 public void onClick(View v) {
801 if (v == mSearchButton) {
802 onSearchClicked();
803 } else if (v == mCloseButton) {
804 onCloseClicked();
805 } else if (v == mSubmitButton) {
806 onSubmitQuery();
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -0700807 } else if (v == mVoiceButton) {
808 onVoiceClicked();
Amith Yamasanif28d1872011-07-26 12:21:03 -0700809 } else if (v == mQueryTextView) {
810 forceSuggestionQuery();
Amith Yamasani733cbd52010-09-03 12:21:39 -0700811 }
812 }
813 };
814
815 /**
816 * Handles the key down event for dealing with action keys.
817 *
818 * @param keyCode This is the keycode of the typed key, and is the same value as
819 * found in the KeyEvent parameter.
820 * @param event The complete event record for the typed key
821 *
822 * @return true if the event was handled here, or false if not.
823 */
824 @Override
825 public boolean onKeyDown(int keyCode, KeyEvent event) {
826 if (mSearchable == null) {
827 return false;
828 }
829
830 // if it's an action specified by the searchable activity, launch the
831 // entered query with the action key
832 SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
833 if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) {
Amith Yamasani93227752010-09-14 10:10:54 -0700834 launchQuerySearch(keyCode, actionKey.getQueryActionMsg(), mQueryTextView.getText()
835 .toString());
Amith Yamasani733cbd52010-09-03 12:21:39 -0700836 return true;
837 }
838
839 return super.onKeyDown(keyCode, event);
840 }
841
Amith Yamasani968ec932010-12-02 14:00:47 -0800842 /**
843 * React to the user typing "enter" or other hardwired keys while typing in
844 * the search box. This handles these special keys while the edit box has
845 * focus.
846 */
847 View.OnKeyListener mTextKeyListener = new View.OnKeyListener() {
848 public boolean onKey(View v, int keyCode, KeyEvent event) {
849 // guard against possible race conditions
850 if (mSearchable == null) {
851 return false;
852 }
853
854 if (DBG) {
855 Log.d(LOG_TAG, "mTextListener.onKey(" + keyCode + "," + event + "), selection: "
856 + mQueryTextView.getListSelection());
857 }
858
859 // If a suggestion is selected, handle enter, search key, and action keys
860 // as presses on the selected suggestion
861 if (mQueryTextView.isPopupShowing()
862 && mQueryTextView.getListSelection() != ListView.INVALID_POSITION) {
863 return onSuggestionsKey(v, keyCode, event);
864 }
865
866 // If there is text in the query box, handle enter, and action keys
867 // The search key is handled by the dialog's onKeyDown().
Jeff Brown4e6319b2010-12-13 10:36:51 -0800868 if (!mQueryTextView.isEmpty() && event.hasNoModifiers()) {
869 if (event.getAction() == KeyEvent.ACTION_UP) {
870 if (keyCode == KeyEvent.KEYCODE_ENTER) {
871 v.cancelLongPress();
Amith Yamasani968ec932010-12-02 14:00:47 -0800872
Jeff Brown4e6319b2010-12-13 10:36:51 -0800873 // Launch as a regular search.
874 launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null, mQueryTextView.getText()
875 .toString());
876 return true;
877 }
Amith Yamasani968ec932010-12-02 14:00:47 -0800878 }
879 if (event.getAction() == KeyEvent.ACTION_DOWN) {
880 SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
881 if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) {
882 launchQuerySearch(keyCode, actionKey.getQueryActionMsg(), mQueryTextView
883 .getText().toString());
884 return true;
885 }
886 }
887 }
888 return false;
889 }
890 };
891
892 /**
893 * React to the user typing while in the suggestions list. First, check for
894 * action keys. If not handled, try refocusing regular characters into the
895 * EditText.
896 */
897 private boolean onSuggestionsKey(View v, int keyCode, KeyEvent event) {
898 // guard against possible race conditions (late arrival after dismiss)
899 if (mSearchable == null) {
900 return false;
901 }
902 if (mSuggestionsAdapter == null) {
903 return false;
904 }
Jeff Brown4e6319b2010-12-13 10:36:51 -0800905 if (event.getAction() == KeyEvent.ACTION_DOWN && event.hasNoModifiers()) {
Amith Yamasani968ec932010-12-02 14:00:47 -0800906 // First, check for enter or search (both of which we'll treat as a
907 // "click")
Jeff Brown4e6319b2010-12-13 10:36:51 -0800908 if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_SEARCH
909 || keyCode == KeyEvent.KEYCODE_TAB) {
Amith Yamasani968ec932010-12-02 14:00:47 -0800910 int position = mQueryTextView.getListSelection();
911 return onItemClicked(position, KeyEvent.KEYCODE_UNKNOWN, null);
912 }
913
914 // Next, check for left/right moves, which we use to "return" the
915 // user to the edit view
916 if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
917 // give "focus" to text editor, with cursor at the beginning if
918 // left key, at end if right key
919 // TODO: Reverse left/right for right-to-left languages, e.g.
920 // Arabic
921 int selPoint = (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) ? 0 : mQueryTextView
922 .length();
923 mQueryTextView.setSelection(selPoint);
924 mQueryTextView.setListSelection(0);
925 mQueryTextView.clearListSelection();
926 mQueryTextView.ensureImeVisible(true);
927
928 return true;
929 }
930
931 // Next, check for an "up and out" move
932 if (keyCode == KeyEvent.KEYCODE_DPAD_UP && 0 == mQueryTextView.getListSelection()) {
933 // TODO: restoreUserQuery();
934 // let ACTV complete the move
935 return false;
936 }
937
938 // Next, check for an "action key"
939 SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
940 if ((actionKey != null)
941 && ((actionKey.getSuggestActionMsg() != null) || (actionKey
942 .getSuggestActionMsgColumn() != null))) {
943 // launch suggestion using action key column
944 int position = mQueryTextView.getListSelection();
945 if (position != ListView.INVALID_POSITION) {
946 Cursor c = mSuggestionsAdapter.getCursor();
947 if (c.moveToPosition(position)) {
948 final String actionMsg = getActionKeyMessage(c, actionKey);
949 if (actionMsg != null && (actionMsg.length() > 0)) {
950 return onItemClicked(position, keyCode, actionMsg);
951 }
952 }
953 }
954 }
955 }
956 return false;
957 }
958
959 /**
960 * For a given suggestion and a given cursor row, get the action message. If
961 * not provided by the specific row/column, also check for a single
962 * definition (for the action key).
963 *
964 * @param c The cursor providing suggestions
965 * @param actionKey The actionkey record being examined
966 *
967 * @return Returns a string, or null if no action key message for this
968 * suggestion
969 */
970 private static String getActionKeyMessage(Cursor c, SearchableInfo.ActionKeyInfo actionKey) {
971 String result = null;
972 // check first in the cursor data, for a suggestion-specific message
973 final String column = actionKey.getSuggestActionMsgColumn();
974 if (column != null) {
975 result = SuggestionsAdapter.getColumnString(c, column);
976 }
977 // If the cursor didn't give us a message, see if there's a single
978 // message defined
979 // for the actionkey (for all suggestions)
980 if (result == null) {
981 result = actionKey.getSuggestActionMsg();
982 }
983 return result;
984 }
985
Amith Yamasanib4569fb2011-07-08 15:25:39 -0700986 private int getSearchIconId() {
987 TypedValue outValue = new TypedValue();
988 getContext().getTheme().resolveAttribute(com.android.internal.R.attr.searchViewSearchIcon,
989 outValue, true);
990 return outValue.resourceId;
991 }
992
993 private CharSequence getDecoratedHint(CharSequence hintText) {
994 // If the field is always expanded, then don't add the search icon to the hint
995 if (!mIconifiedByDefault) return hintText;
996
997 SpannableStringBuilder ssb = new SpannableStringBuilder(" "); // for the icon
998 ssb.append(hintText);
999 Drawable searchIcon = getContext().getResources().getDrawable(getSearchIconId());
1000 int textSize = (int) (mQueryTextView.getTextSize() * 1.25);
1001 searchIcon.setBounds(0, 0, textSize, textSize);
1002 ssb.setSpan(new ImageSpan(searchIcon), 1, 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
1003 return ssb;
1004 }
1005
Amith Yamasani733cbd52010-09-03 12:21:39 -07001006 private void updateQueryHint() {
1007 if (mQueryHint != null) {
Amith Yamasanib4569fb2011-07-08 15:25:39 -07001008 mQueryTextView.setHint(getDecoratedHint(mQueryHint));
Amith Yamasani733cbd52010-09-03 12:21:39 -07001009 } else if (mSearchable != null) {
1010 CharSequence hint = null;
1011 int hintId = mSearchable.getHintId();
1012 if (hintId != 0) {
1013 hint = getContext().getString(hintId);
1014 }
1015 if (hint != null) {
Amith Yamasanib4569fb2011-07-08 15:25:39 -07001016 mQueryTextView.setHint(getDecoratedHint(hint));
Amith Yamasani733cbd52010-09-03 12:21:39 -07001017 }
Amith Yamasanib4569fb2011-07-08 15:25:39 -07001018 } else {
1019 mQueryTextView.setHint(getDecoratedHint(""));
Amith Yamasani733cbd52010-09-03 12:21:39 -07001020 }
1021 }
1022
1023 /**
1024 * Updates the auto-complete text view.
1025 */
1026 private void updateSearchAutoComplete() {
Amith Yamasani733cbd52010-09-03 12:21:39 -07001027 mQueryTextView.setDropDownAnimationStyle(0); // no animation
Amith Yamasani5931b1f2010-10-18 16:13:14 -07001028 mQueryTextView.setThreshold(mSearchable.getSuggestThreshold());
Amith Yamasani5607a382011-08-09 14:16:37 -07001029 mQueryTextView.setImeOptions(mSearchable.getImeOptions());
1030 int inputType = mSearchable.getInputType();
1031 // We only touch this if the input type is set up for text (which it almost certainly
1032 // should be, in the case of search!)
1033 if ((inputType & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_TEXT) {
1034 // The existence of a suggestions authority is the proxy for "suggestions
1035 // are available here"
1036 inputType &= ~InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;
1037 if (mSearchable.getSuggestAuthority() != null) {
1038 inputType |= InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;
1039 }
1040 }
1041 mQueryTextView.setInputType(inputType);
Amith Yamasani87907642011-11-03 11:32:44 -07001042 if (mSuggestionsAdapter != null) {
1043 mSuggestionsAdapter.changeCursor(null);
1044 }
Amith Yamasani733cbd52010-09-03 12:21:39 -07001045 // attach the suggestions adapter, if suggestions are available
1046 // The existence of a suggestions authority is the proxy for "suggestions available here"
1047 if (mSearchable.getSuggestAuthority() != null) {
1048 mSuggestionsAdapter = new SuggestionsAdapter(getContext(),
1049 this, mSearchable, mOutsideDrawablesCache);
1050 mQueryTextView.setAdapter(mSuggestionsAdapter);
Amith Yamasanie678f462010-09-15 16:13:43 -07001051 ((SuggestionsAdapter) mSuggestionsAdapter).setQueryRefinement(
1052 mQueryRefinement ? SuggestionsAdapter.REFINE_ALL
1053 : SuggestionsAdapter.REFINE_BY_ENTRY);
Amith Yamasani733cbd52010-09-03 12:21:39 -07001054 }
1055 }
1056
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001057 /**
1058 * Update the visibility of the voice button. There are actually two voice search modes,
1059 * either of which will activate the button.
1060 * @param empty whether the search query text field is empty. If it is, then the other
Amith Yamasani79f74302011-03-08 14:16:35 -08001061 * criteria apply to make the voice button visible.
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001062 */
1063 private void updateVoiceButton(boolean empty) {
Amith Yamasani79f74302011-03-08 14:16:35 -08001064 int visibility = GONE;
Amith Yamasani167d69a2011-08-12 19:28:37 -07001065 if (mVoiceButtonEnabled && !isIconified() && empty) {
Amith Yamasani9b2e3022011-01-14 11:34:12 -08001066 visibility = VISIBLE;
1067 mSubmitButton.setVisibility(GONE);
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001068 }
1069 mVoiceButton.setVisibility(visibility);
1070 }
1071
Amith Yamasani733cbd52010-09-03 12:21:39 -07001072 private final OnEditorActionListener mOnEditorActionListener = new OnEditorActionListener() {
1073
1074 /**
1075 * Called when the input method default action key is pressed.
1076 */
1077 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
1078 onSubmitQuery();
1079 return true;
1080 }
1081 };
1082
1083 private void onTextChanged(CharSequence newText) {
1084 CharSequence text = mQueryTextView.getText();
Amith Yamasani068d73c2011-05-27 15:15:14 -07001085 mUserQuery = text;
Amith Yamasani733cbd52010-09-03 12:21:39 -07001086 boolean hasText = !TextUtils.isEmpty(text);
Amith Yamasanicf72ab42011-11-04 13:49:28 -07001087 updateSubmitButton(hasText);
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001088 updateVoiceButton(!hasText);
Amith Yamasani73e00df2010-12-16 16:31:29 -08001089 updateCloseButton();
Amith Yamasani9b2e3022011-01-14 11:34:12 -08001090 updateSubmitArea();
Amith Yamasanib47c4fd2011-08-04 14:30:07 -07001091 if (mOnQueryChangeListener != null && !TextUtils.equals(newText, mOldQueryText)) {
Adam Powell01f21352011-01-20 18:30:10 -08001092 mOnQueryChangeListener.onQueryTextChange(newText.toString());
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001093 }
Amith Yamasanib47c4fd2011-08-04 14:30:07 -07001094 mOldQueryText = newText.toString();
Amith Yamasani733cbd52010-09-03 12:21:39 -07001095 }
1096
1097 private void onSubmitQuery() {
1098 CharSequence query = mQueryTextView.getText();
Amith Yamasani6a7421b2011-07-27 11:55:53 -07001099 if (query != null && TextUtils.getTrimmedLength(query) > 0) {
Amith Yamasani733cbd52010-09-03 12:21:39 -07001100 if (mOnQueryChangeListener == null
Adam Powell01f21352011-01-20 18:30:10 -08001101 || !mOnQueryChangeListener.onQueryTextSubmit(query.toString())) {
Amith Yamasani733cbd52010-09-03 12:21:39 -07001102 if (mSearchable != null) {
1103 launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null, query.toString());
Amith Yamasani05944762010-10-08 13:52:38 -07001104 setImeVisibility(false);
Amith Yamasani733cbd52010-09-03 12:21:39 -07001105 }
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001106 dismissSuggestions();
Amith Yamasani733cbd52010-09-03 12:21:39 -07001107 }
1108 }
1109 }
1110
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001111 private void dismissSuggestions() {
1112 mQueryTextView.dismissDropDown();
1113 }
1114
Amith Yamasani733cbd52010-09-03 12:21:39 -07001115 private void onCloseClicked() {
Amith Yamasani24652982011-06-23 16:16:05 -07001116 CharSequence text = mQueryTextView.getText();
1117 if (TextUtils.isEmpty(text)) {
1118 if (mIconifiedByDefault) {
Amith Yamasanib4569fb2011-07-08 15:25:39 -07001119 // If the app doesn't override the close behavior
1120 if (mOnCloseListener == null || !mOnCloseListener.onClose()) {
1121 // hide the keyboard and remove focus
1122 clearFocus();
1123 // collapse the search field
1124 updateViewsVisibility(true);
1125 }
Amith Yamasani05944762010-10-08 13:52:38 -07001126 }
Amith Yamasani24652982011-06-23 16:16:05 -07001127 } else {
1128 mQueryTextView.setText("");
1129 mQueryTextView.requestFocus();
1130 setImeVisibility(true);
1131 }
1132
Amith Yamasani733cbd52010-09-03 12:21:39 -07001133 }
1134
1135 private void onSearchClicked() {
Amith Yamasani733cbd52010-09-03 12:21:39 -07001136 updateViewsVisibility(false);
Amith Yamasani7f8aef62011-01-25 11:58:09 -08001137 mQueryTextView.requestFocus();
Amith Yamasani05944762010-10-08 13:52:38 -07001138 setImeVisibility(true);
Amith Yamasani48385482010-12-03 14:43:52 -08001139 if (mOnSearchClickListener != null) {
1140 mOnSearchClickListener.onClick(this);
1141 }
Amith Yamasani733cbd52010-09-03 12:21:39 -07001142 }
1143
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001144 private void onVoiceClicked() {
1145 // guard against possible race conditions
1146 if (mSearchable == null) {
1147 return;
1148 }
1149 SearchableInfo searchable = mSearchable;
1150 try {
1151 if (searchable.getVoiceSearchLaunchWebSearch()) {
1152 Intent webSearchIntent = createVoiceWebSearchIntent(mVoiceWebSearchIntent,
1153 searchable);
1154 getContext().startActivity(webSearchIntent);
1155 } else if (searchable.getVoiceSearchLaunchRecognizer()) {
1156 Intent appSearchIntent = createVoiceAppSearchIntent(mVoiceAppSearchIntent,
1157 searchable);
1158 getContext().startActivity(appSearchIntent);
1159 }
1160 } catch (ActivityNotFoundException e) {
1161 // Should not happen, since we check the availability of
1162 // voice search before showing the button. But just in case...
1163 Log.w(LOG_TAG, "Could not find voice search activity");
1164 }
1165 }
1166
Amith Yamasani4aedb392010-12-15 16:04:57 -08001167 void onTextFocusChanged() {
Amith Yamasani79f74302011-03-08 14:16:35 -08001168 updateViewsVisibility(isIconified());
Amith Yamasania95e4882011-08-17 11:41:37 -07001169 // Delayed update to make sure that the focus has settled down and window focus changes
1170 // don't affect it. A synchronous update was not working.
1171 postUpdateFocusedState();
Amith Yamasanif28d1872011-07-26 12:21:03 -07001172 if (mQueryTextView.hasFocus()) {
1173 forceSuggestionQuery();
1174 }
Amith Yamasani4aedb392010-12-15 16:04:57 -08001175 }
1176
Amith Yamasanib4569fb2011-07-08 15:25:39 -07001177 @Override
Amith Yamasania95e4882011-08-17 11:41:37 -07001178 public void onWindowFocusChanged(boolean hasWindowFocus) {
1179 super.onWindowFocusChanged(hasWindowFocus);
1180
1181 postUpdateFocusedState();
Amith Yamasanib4569fb2011-07-08 15:25:39 -07001182 }
1183
Amith Yamasani763bc072011-07-22 11:53:47 -07001184 /**
1185 * {@inheritDoc}
1186 */
1187 @Override
1188 public void onActionViewCollapsed() {
Amith Yamasani10da5902011-07-26 16:14:26 -07001189 clearFocus();
1190 updateViewsVisibility(true);
Adam Powell53f56c42011-09-25 13:46:15 -07001191 mQueryTextView.setImeOptions(mCollapsedImeOptions);
Amith Yamasani763bc072011-07-22 11:53:47 -07001192 mExpandedInActionView = false;
1193 }
1194
1195 /**
1196 * {@inheritDoc}
1197 */
1198 @Override
1199 public void onActionViewExpanded() {
Amith Yamasani434c73f2011-11-01 11:44:50 -07001200 if (mExpandedInActionView) return;
1201
Amith Yamasani763bc072011-07-22 11:53:47 -07001202 mExpandedInActionView = true;
Adam Powell53f56c42011-09-25 13:46:15 -07001203 mCollapsedImeOptions = mQueryTextView.getImeOptions();
1204 mQueryTextView.setImeOptions(mCollapsedImeOptions | EditorInfo.IME_FLAG_NO_FULLSCREEN);
Amith Yamasani87907642011-11-03 11:32:44 -07001205 mQueryTextView.setText("");
Amith Yamasani763bc072011-07-22 11:53:47 -07001206 setIconified(false);
1207 }
1208
Amith Yamasanib4569fb2011-07-08 15:25:39 -07001209 private void adjustDropDownSizeAndPosition() {
1210 if (mDropDownAnchor.getWidth() > 1) {
1211 Resources res = getContext().getResources();
1212 int anchorPadding = mSearchPlate.getPaddingLeft();
1213 Rect dropDownPadding = new Rect();
1214 int iconOffset = mIconifiedByDefault
1215 ? res.getDimensionPixelSize(R.dimen.dropdownitem_icon_width)
1216 + res.getDimensionPixelSize(R.dimen.dropdownitem_text_padding_left)
1217 : 0;
1218 mQueryTextView.getDropDownBackground().getPadding(dropDownPadding);
1219 mQueryTextView.setDropDownHorizontalOffset(-(dropDownPadding.left + iconOffset)
1220 + anchorPadding);
1221 mQueryTextView.setDropDownWidth(mDropDownAnchor.getWidth() + dropDownPadding.left
1222 + dropDownPadding.right + iconOffset - (anchorPadding));
1223 }
1224 }
1225
Amith Yamasani968ec932010-12-02 14:00:47 -08001226 private boolean onItemClicked(int position, int actionKey, String actionMsg) {
1227 if (mOnSuggestionListener == null
Adam Powell01f21352011-01-20 18:30:10 -08001228 || !mOnSuggestionListener.onSuggestionClick(position)) {
Amith Yamasani968ec932010-12-02 14:00:47 -08001229 launchSuggestion(position, KeyEvent.KEYCODE_UNKNOWN, null);
1230 setImeVisibility(false);
1231 dismissSuggestions();
1232 return true;
1233 }
1234 return false;
1235 }
1236
1237 private boolean onItemSelected(int position) {
1238 if (mOnSuggestionListener == null
Adam Powell01f21352011-01-20 18:30:10 -08001239 || !mOnSuggestionListener.onSuggestionSelect(position)) {
Amith Yamasani968ec932010-12-02 14:00:47 -08001240 rewriteQueryFromSuggestion(position);
1241 return true;
1242 }
1243 return false;
1244 }
1245
Amith Yamasani733cbd52010-09-03 12:21:39 -07001246 private final OnItemClickListener mOnItemClickListener = new OnItemClickListener() {
1247
1248 /**
1249 * Implements OnItemClickListener
1250 */
1251 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Amith Yamasani968ec932010-12-02 14:00:47 -08001252 if (DBG) Log.d(LOG_TAG, "onItemClick() position " + position);
1253 onItemClicked(position, KeyEvent.KEYCODE_UNKNOWN, null);
Amith Yamasani733cbd52010-09-03 12:21:39 -07001254 }
1255 };
1256
1257 private final OnItemSelectedListener mOnItemSelectedListener = new OnItemSelectedListener() {
1258
1259 /**
1260 * Implements OnItemSelectedListener
1261 */
1262 public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
Amith Yamasani968ec932010-12-02 14:00:47 -08001263 if (DBG) Log.d(LOG_TAG, "onItemSelected() position " + position);
1264 SearchView.this.onItemSelected(position);
Amith Yamasani733cbd52010-09-03 12:21:39 -07001265 }
1266
1267 /**
1268 * Implements OnItemSelectedListener
1269 */
1270 public void onNothingSelected(AdapterView<?> parent) {
1271 if (DBG)
1272 Log.d(LOG_TAG, "onNothingSelected()");
1273 }
1274 };
1275
1276 /**
1277 * Query rewriting.
1278 */
1279 private void rewriteQueryFromSuggestion(int position) {
1280 CharSequence oldQuery = mQueryTextView.getText();
1281 Cursor c = mSuggestionsAdapter.getCursor();
1282 if (c == null) {
1283 return;
1284 }
1285 if (c.moveToPosition(position)) {
1286 // Get the new query from the suggestion.
1287 CharSequence newQuery = mSuggestionsAdapter.convertToString(c);
1288 if (newQuery != null) {
1289 // The suggestion rewrites the query.
1290 // Update the text field, without getting new suggestions.
1291 setQuery(newQuery);
1292 } else {
1293 // The suggestion does not rewrite the query, restore the user's query.
1294 setQuery(oldQuery);
1295 }
1296 } else {
1297 // We got a bad position, restore the user's query.
1298 setQuery(oldQuery);
1299 }
1300 }
1301
1302 /**
1303 * Launches an intent based on a suggestion.
1304 *
1305 * @param position The index of the suggestion to create the intent from.
1306 * @param actionKey The key code of the action key that was pressed,
1307 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
1308 * @param actionMsg The message for the action key that was pressed,
1309 * or <code>null</code> if none.
1310 * @return true if a successful launch, false if could not (e.g. bad position).
1311 */
1312 private boolean launchSuggestion(int position, int actionKey, String actionMsg) {
1313 Cursor c = mSuggestionsAdapter.getCursor();
1314 if ((c != null) && c.moveToPosition(position)) {
1315
1316 Intent intent = createIntentFromSuggestion(c, actionKey, actionMsg);
1317
1318 // launch the intent
1319 launchIntent(intent);
1320
1321 return true;
1322 }
1323 return false;
1324 }
1325
1326 /**
1327 * Launches an intent, including any special intent handling.
1328 */
1329 private void launchIntent(Intent intent) {
1330 if (intent == null) {
1331 return;
1332 }
1333 try {
1334 // If the intent was created from a suggestion, it will always have an explicit
1335 // component here.
1336 getContext().startActivity(intent);
1337 } catch (RuntimeException ex) {
1338 Log.e(LOG_TAG, "Failed launch activity: " + intent, ex);
1339 }
1340 }
1341
1342 /**
1343 * Sets the text in the query box, without updating the suggestions.
1344 */
1345 private void setQuery(CharSequence query) {
Amith Yamasanie678f462010-09-15 16:13:43 -07001346 mQueryTextView.setText(query, true);
1347 // Move the cursor to the end
1348 mQueryTextView.setSelection(TextUtils.isEmpty(query) ? 0 : query.length());
Amith Yamasani733cbd52010-09-03 12:21:39 -07001349 }
1350
1351 private void launchQuerySearch(int actionKey, String actionMsg, String query) {
1352 String action = Intent.ACTION_SEARCH;
Amith Yamasanie678f462010-09-15 16:13:43 -07001353 Intent intent = createIntent(action, null, null, query, actionKey, actionMsg);
Amith Yamasani733cbd52010-09-03 12:21:39 -07001354 getContext().startActivity(intent);
1355 }
1356
1357 /**
1358 * Constructs an intent from the given information and the search dialog state.
1359 *
1360 * @param action Intent action.
1361 * @param data Intent data, or <code>null</code>.
1362 * @param extraData Data for {@link SearchManager#EXTRA_DATA_KEY} or <code>null</code>.
1363 * @param query Intent query, or <code>null</code>.
Amith Yamasani733cbd52010-09-03 12:21:39 -07001364 * @param actionKey The key code of the action key that was pressed,
1365 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
1366 * @param actionMsg The message for the action key that was pressed,
1367 * or <code>null</code> if none.
1368 * @param mode The search mode, one of the acceptable values for
1369 * {@link SearchManager#SEARCH_MODE}, or {@code null}.
1370 * @return The intent.
1371 */
1372 private Intent createIntent(String action, Uri data, String extraData, String query,
Amith Yamasanie678f462010-09-15 16:13:43 -07001373 int actionKey, String actionMsg) {
Amith Yamasani733cbd52010-09-03 12:21:39 -07001374 // Now build the Intent
1375 Intent intent = new Intent(action);
1376 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
1377 // We need CLEAR_TOP to avoid reusing an old task that has other activities
1378 // on top of the one we want. We don't want to do this in in-app search though,
1379 // as it can be destructive to the activity stack.
1380 if (data != null) {
1381 intent.setData(data);
1382 }
Amith Yamasani068d73c2011-05-27 15:15:14 -07001383 intent.putExtra(SearchManager.USER_QUERY, mUserQuery);
Amith Yamasani733cbd52010-09-03 12:21:39 -07001384 if (query != null) {
1385 intent.putExtra(SearchManager.QUERY, query);
1386 }
1387 if (extraData != null) {
1388 intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData);
1389 }
Amith Yamasani940ef382011-03-02 18:43:23 -08001390 if (mAppSearchData != null) {
1391 intent.putExtra(SearchManager.APP_DATA, mAppSearchData);
1392 }
Amith Yamasani733cbd52010-09-03 12:21:39 -07001393 if (actionKey != KeyEvent.KEYCODE_UNKNOWN) {
1394 intent.putExtra(SearchManager.ACTION_KEY, actionKey);
1395 intent.putExtra(SearchManager.ACTION_MSG, actionMsg);
1396 }
1397 intent.setComponent(mSearchable.getSearchActivity());
1398 return intent;
1399 }
1400
1401 /**
Amith Yamasaniebcf5a3a2010-10-13 11:35:24 -07001402 * Create and return an Intent that can launch the voice search activity for web search.
1403 */
1404 private Intent createVoiceWebSearchIntent(Intent baseIntent, SearchableInfo searchable) {
1405 Intent voiceIntent = new Intent(baseIntent);
1406 ComponentName searchActivity = searchable.getSearchActivity();
1407 voiceIntent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, searchActivity == null ? null
1408 : searchActivity.flattenToShortString());
1409 return voiceIntent;
1410 }
1411
1412 /**
1413 * Create and return an Intent that can launch the voice search activity, perform a specific
1414 * voice transcription, and forward the results to the searchable activity.
1415 *
1416 * @param baseIntent The voice app search intent to start from
1417 * @return A completely-configured intent ready to send to the voice search activity
1418 */
1419 private Intent createVoiceAppSearchIntent(Intent baseIntent, SearchableInfo searchable) {
1420 ComponentName searchActivity = searchable.getSearchActivity();
1421
1422 // create the necessary intent to set up a search-and-forward operation
1423 // in the voice search system. We have to keep the bundle separate,
1424 // because it becomes immutable once it enters the PendingIntent
1425 Intent queryIntent = new Intent(Intent.ACTION_SEARCH);
1426 queryIntent.setComponent(searchActivity);
1427 PendingIntent pending = PendingIntent.getActivity(getContext(), 0, queryIntent,
1428 PendingIntent.FLAG_ONE_SHOT);
1429
1430 // Now set up the bundle that will be inserted into the pending intent
1431 // when it's time to do the search. We always build it here (even if empty)
1432 // because the voice search activity will always need to insert "QUERY" into
1433 // it anyway.
1434 Bundle queryExtras = new Bundle();
1435
1436 // Now build the intent to launch the voice search. Add all necessary
1437 // extras to launch the voice recognizer, and then all the necessary extras
1438 // to forward the results to the searchable activity
1439 Intent voiceIntent = new Intent(baseIntent);
1440
1441 // Add all of the configuration options supplied by the searchable's metadata
1442 String languageModel = RecognizerIntent.LANGUAGE_MODEL_FREE_FORM;
1443 String prompt = null;
1444 String language = null;
1445 int maxResults = 1;
1446
1447 Resources resources = getResources();
1448 if (searchable.getVoiceLanguageModeId() != 0) {
1449 languageModel = resources.getString(searchable.getVoiceLanguageModeId());
1450 }
1451 if (searchable.getVoicePromptTextId() != 0) {
1452 prompt = resources.getString(searchable.getVoicePromptTextId());
1453 }
1454 if (searchable.getVoiceLanguageId() != 0) {
1455 language = resources.getString(searchable.getVoiceLanguageId());
1456 }
1457 if (searchable.getVoiceMaxResults() != 0) {
1458 maxResults = searchable.getVoiceMaxResults();
1459 }
1460 voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, languageModel);
1461 voiceIntent.putExtra(RecognizerIntent.EXTRA_PROMPT, prompt);
1462 voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, language);
1463 voiceIntent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, maxResults);
1464 voiceIntent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, searchActivity == null ? null
1465 : searchActivity.flattenToShortString());
1466
1467 // Add the values that configure forwarding the results
1468 voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT, pending);
1469 voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT_BUNDLE, queryExtras);
1470
1471 return voiceIntent;
1472 }
1473
1474 /**
Amith Yamasani733cbd52010-09-03 12:21:39 -07001475 * When a particular suggestion has been selected, perform the various lookups required
1476 * to use the suggestion. This includes checking the cursor for suggestion-specific data,
1477 * and/or falling back to the XML for defaults; It also creates REST style Uri data when
1478 * the suggestion includes a data id.
1479 *
1480 * @param c The suggestions cursor, moved to the row of the user's selection
1481 * @param actionKey The key code of the action key that was pressed,
1482 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
1483 * @param actionMsg The message for the action key that was pressed,
1484 * or <code>null</code> if none.
1485 * @return An intent for the suggestion at the cursor's position.
1486 */
1487 private Intent createIntentFromSuggestion(Cursor c, int actionKey, String actionMsg) {
1488 try {
1489 // use specific action if supplied, or default action if supplied, or fixed default
1490 String action = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_ACTION);
1491
Amith Yamasani733cbd52010-09-03 12:21:39 -07001492 if (action == null) {
1493 action = mSearchable.getSuggestIntentAction();
1494 }
1495 if (action == null) {
1496 action = Intent.ACTION_SEARCH;
1497 }
1498
1499 // use specific data if supplied, or default data if supplied
1500 String data = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA);
1501 if (data == null) {
1502 data = mSearchable.getSuggestIntentData();
1503 }
1504 // then, if an ID was provided, append it.
1505 if (data != null) {
1506 String id = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID);
1507 if (id != null) {
1508 data = data + "/" + Uri.encode(id);
1509 }
1510 }
1511 Uri dataUri = (data == null) ? null : Uri.parse(data);
1512
Amith Yamasani733cbd52010-09-03 12:21:39 -07001513 String query = getColumnString(c, SearchManager.SUGGEST_COLUMN_QUERY);
1514 String extraData = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA);
1515
Amith Yamasanie678f462010-09-15 16:13:43 -07001516 return createIntent(action, dataUri, extraData, query, actionKey, actionMsg);
Amith Yamasani733cbd52010-09-03 12:21:39 -07001517 } catch (RuntimeException e ) {
1518 int rowNum;
1519 try { // be really paranoid now
1520 rowNum = c.getPosition();
1521 } catch (RuntimeException e2 ) {
1522 rowNum = -1;
1523 }
1524 Log.w(LOG_TAG, "Search Suggestions cursor at row " + rowNum +
1525 " returned exception" + e.toString());
1526 return null;
1527 }
1528 }
1529
Amith Yamasanif28d1872011-07-26 12:21:03 -07001530 private void forceSuggestionQuery() {
1531 mQueryTextView.doBeforeTextChanged();
1532 mQueryTextView.doAfterTextChanged();
1533 }
1534
Amith Yamasani968ec932010-12-02 14:00:47 -08001535 static boolean isLandscapeMode(Context context) {
1536 return context.getResources().getConfiguration().orientation
1537 == Configuration.ORIENTATION_LANDSCAPE;
1538 }
1539
Amith Yamasani733cbd52010-09-03 12:21:39 -07001540 /**
1541 * Callback to watch the text field for empty/non-empty
1542 */
1543 private TextWatcher mTextWatcher = new TextWatcher() {
1544
1545 public void beforeTextChanged(CharSequence s, int start, int before, int after) { }
1546
1547 public void onTextChanged(CharSequence s, int start,
1548 int before, int after) {
1549 SearchView.this.onTextChanged(s);
1550 }
1551
1552 public void afterTextChanged(Editable s) {
1553 }
1554 };
Amith Yamasani968ec932010-12-02 14:00:47 -08001555
1556 /**
1557 * Local subclass for AutoCompleteTextView.
1558 * @hide
1559 */
1560 public static class SearchAutoComplete extends AutoCompleteTextView {
1561
1562 private int mThreshold;
1563 private SearchView mSearchView;
1564
1565 public SearchAutoComplete(Context context) {
1566 super(context);
1567 mThreshold = getThreshold();
1568 }
1569
1570 public SearchAutoComplete(Context context, AttributeSet attrs) {
1571 super(context, attrs);
1572 mThreshold = getThreshold();
1573 }
1574
1575 public SearchAutoComplete(Context context, AttributeSet attrs, int defStyle) {
1576 super(context, attrs, defStyle);
1577 mThreshold = getThreshold();
1578 }
1579
1580 void setSearchView(SearchView searchView) {
1581 mSearchView = searchView;
1582 }
1583
1584 @Override
1585 public void setThreshold(int threshold) {
1586 super.setThreshold(threshold);
1587 mThreshold = threshold;
1588 }
1589
1590 /**
1591 * Returns true if the text field is empty, or contains only whitespace.
1592 */
1593 private boolean isEmpty() {
1594 return TextUtils.getTrimmedLength(getText()) == 0;
1595 }
1596
1597 /**
1598 * We override this method to avoid replacing the query box text when a
1599 * suggestion is clicked.
1600 */
1601 @Override
1602 protected void replaceText(CharSequence text) {
1603 }
1604
1605 /**
1606 * We override this method to avoid an extra onItemClick being called on
1607 * the drop-down's OnItemClickListener by
1608 * {@link AutoCompleteTextView#onKeyUp(int, KeyEvent)} when an item is
1609 * clicked with the trackball.
1610 */
1611 @Override
1612 public void performCompletion() {
1613 }
1614
1615 /**
1616 * We override this method to be sure and show the soft keyboard if
1617 * appropriate when the TextView has focus.
1618 */
1619 @Override
1620 public void onWindowFocusChanged(boolean hasWindowFocus) {
1621 super.onWindowFocusChanged(hasWindowFocus);
1622
Amith Yamasaniacd8d2d2010-12-06 15:50:23 -08001623 if (hasWindowFocus && mSearchView.hasFocus() && getVisibility() == VISIBLE) {
Amith Yamasani968ec932010-12-02 14:00:47 -08001624 InputMethodManager inputManager = (InputMethodManager) getContext()
1625 .getSystemService(Context.INPUT_METHOD_SERVICE);
1626 inputManager.showSoftInput(this, 0);
1627 // If in landscape mode, then make sure that
1628 // the ime is in front of the dropdown.
1629 if (isLandscapeMode(getContext())) {
1630 ensureImeVisible(true);
1631 }
1632 }
1633 }
1634
Amith Yamasani4aedb392010-12-15 16:04:57 -08001635 @Override
1636 protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
1637 super.onFocusChanged(focused, direction, previouslyFocusedRect);
1638 mSearchView.onTextFocusChanged();
1639 }
1640
Amith Yamasani968ec932010-12-02 14:00:47 -08001641 /**
1642 * We override this method so that we can allow a threshold of zero,
1643 * which ACTV does not.
1644 */
1645 @Override
1646 public boolean enoughToFilter() {
1647 return mThreshold <= 0 || super.enoughToFilter();
1648 }
Amith Yamasanib4569fb2011-07-08 15:25:39 -07001649
1650 @Override
1651 public boolean onKeyPreIme(int keyCode, KeyEvent event) {
1652 if (keyCode == KeyEvent.KEYCODE_BACK) {
1653 // special case for the back key, we do not even try to send it
1654 // to the drop down list but instead, consume it immediately
1655 if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
1656 KeyEvent.DispatcherState state = getKeyDispatcherState();
1657 if (state != null) {
1658 state.startTracking(event, this);
1659 }
1660 return true;
1661 } else if (event.getAction() == KeyEvent.ACTION_UP) {
1662 KeyEvent.DispatcherState state = getKeyDispatcherState();
1663 if (state != null) {
1664 state.handleUpEvent(event);
1665 }
1666 if (event.isTracking() && !event.isCanceled()) {
1667 mSearchView.clearFocus();
1668 mSearchView.setImeVisibility(false);
1669 return true;
1670 }
1671 }
1672 }
1673 return super.onKeyPreIme(keyCode, event);
1674 }
1675
Amith Yamasani968ec932010-12-02 14:00:47 -08001676 }
Amith Yamasani05944762010-10-08 13:52:38 -07001677}