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