2 * Copyright (C) 2010 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com
.actionbarsherlock
.widget
;
19 import android
.app
.PendingIntent
;
20 import android
.app
.SearchManager
;
21 import android
.app
.SearchableInfo
;
22 import android
.content
.ActivityNotFoundException
;
23 import android
.content
.ComponentName
;
24 import android
.content
.Context
;
25 import android
.content
.Intent
;
26 import android
.content
.pm
.PackageManager
;
27 import android
.content
.pm
.ResolveInfo
;
28 import android
.content
.res
.Configuration
;
29 import android
.content
.res
.Resources
;
30 import android
.content
.res
.TypedArray
;
31 import android
.database
.Cursor
;
32 import android
.graphics
.Rect
;
33 import android
.graphics
.drawable
.Drawable
;
34 import android
.net
.Uri
;
35 import android
.os
.Build
;
36 import android
.os
.Bundle
;
37 import android
.os
.ResultReceiver
;
38 import android
.speech
.RecognizerIntent
;
39 import android
.support
.v4
.view
.KeyEventCompat
;
40 import android
.support
.v4
.widget
.CursorAdapter
;
41 import android
.text
.Editable
;
42 import android
.text
.InputType
;
43 import android
.text
.Spannable
;
44 import android
.text
.SpannableStringBuilder
;
45 import android
.text
.TextUtils
;
46 import android
.text
.TextWatcher
;
47 import android
.text
.style
.ImageSpan
;
48 import android
.util
.AttributeSet
;
49 import android
.util
.Log
;
50 import android
.util
.TypedValue
;
51 import android
.view
.KeyEvent
;
52 import android
.view
.LayoutInflater
;
53 import android
.view
.View
;
54 import android
.view
.ViewTreeObserver
;
55 import android
.view
.accessibility
.AccessibilityEvent
;
56 import android
.view
.accessibility
.AccessibilityNodeInfo
;
57 import android
.view
.inputmethod
.EditorInfo
;
58 import android
.view
.inputmethod
.InputMethodManager
;
59 import android
.widget
.AdapterView
;
60 import android
.widget
.AdapterView
.OnItemClickListener
;
61 import android
.widget
.AdapterView
.OnItemSelectedListener
;
62 import android
.widget
.AutoCompleteTextView
;
63 import android
.widget
.ImageView
;
64 import android
.widget
.LinearLayout
;
65 import android
.widget
.ListView
;
66 import android
.widget
.TextView
;
67 import android
.widget
.TextView
.OnEditorActionListener
;
68 import com
.actionbarsherlock
.R
;
69 import com
.actionbarsherlock
.view
.CollapsibleActionView
;
71 import java
.lang
.reflect
.Method
;
72 import java
.util
.WeakHashMap
;
74 import static com
.actionbarsherlock
.widget
.SuggestionsAdapter
.getColumnString
;
77 * A widget that provides a user interface for the user to enter a search query and submit a request
78 * to a search provider. Shows a list of query suggestions or results, if available, and allows the
79 * user to pick a suggestion or result to launch into.
82 * When the SearchView is used in an ActionBar as an action view for a collapsible menu item, it
83 * needs to be set to iconified by default using {@link #setIconifiedByDefault(boolean)
84 * setIconifiedByDefault(true)}. This is the default, so nothing needs to be done.
87 * If you want the search field to always be visible, then call setIconifiedByDefault(false).
90 * <div class="special reference">
91 * <h3>Developer Guides</h3>
92 * <p>For information about using {@code SearchView}, read the
93 * <a href="{@docRoot}guide/topics/search/index.html">Search</a> developer guide.</p>
96 * @see android.view.MenuItem#SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW
97 * @attr ref android.R.styleable#SearchView_iconifiedByDefault
98 * @attr ref android.R.styleable#SearchView_imeOptions
99 * @attr ref android.R.styleable#SearchView_inputType
100 * @attr ref android.R.styleable#SearchView_maxWidth
101 * @attr ref android.R.styleable#SearchView_queryHint
103 public class SearchView
extends LinearLayout
implements CollapsibleActionView
{
105 private static final boolean DBG
= false
;
106 private static final String LOG_TAG
= "SearchView";
109 * Private constant for removing the microphone in the keyboard.
111 private static final String IME_OPTION_NO_MICROPHONE
= "nm";
113 private OnQueryTextListener mOnQueryChangeListener
;
114 private OnCloseListener mOnCloseListener
;
115 private OnFocusChangeListener mOnQueryTextFocusChangeListener
;
116 private OnSuggestionListener mOnSuggestionListener
;
117 private OnClickListener mOnSearchClickListener
;
119 private boolean mIconifiedByDefault
;
120 private boolean mIconified
;
121 private CursorAdapter mSuggestionsAdapter
;
122 private View mSearchButton
;
123 private View mSubmitButton
;
124 private View mSearchPlate
;
125 private View mSubmitArea
;
126 private ImageView mCloseButton
;
127 private View mSearchEditFrame
;
128 private View mVoiceButton
;
129 private SearchAutoComplete mQueryTextView
;
130 private View mDropDownAnchor
;
131 private ImageView mSearchHintIcon
;
132 private boolean mSubmitButtonEnabled
;
133 private CharSequence mQueryHint
;
134 private boolean mQueryRefinement
;
135 private boolean mClearingFocus
;
136 private int mMaxWidth
;
137 private boolean mVoiceButtonEnabled
;
138 private CharSequence mOldQueryText
;
139 private CharSequence mUserQuery
;
140 private boolean mExpandedInActionView
;
141 private int mCollapsedImeOptions
;
143 private SearchableInfo mSearchable
;
144 private Bundle mAppSearchData
;
147 * SearchView can be set expanded before the IME is ready to be shown during
148 * initial UI setup. The show operation is asynchronous to account for this.
150 private Runnable mShowImeRunnable
= new Runnable() {
152 InputMethodManager imm
= (InputMethodManager
)
153 getContext().getSystemService(Context
.INPUT_METHOD_SERVICE
);
156 showSoftInputUnchecked(SearchView
.this, imm
, 0);
161 private Runnable mUpdateDrawableStateRunnable
= new Runnable() {
163 updateFocusedState();
167 private Runnable mReleaseCursorRunnable
= new Runnable() {
169 if (mSuggestionsAdapter
!= null
&& mSuggestionsAdapter
instanceof SuggestionsAdapter
) {
170 mSuggestionsAdapter
.changeCursor(null
);
175 // For voice searching
176 private final Intent mVoiceWebSearchIntent
;
177 private final Intent mVoiceAppSearchIntent
;
179 // A weak map of drawables we've gotten from other packages, so we don't load them
181 private final WeakHashMap
<String
, Drawable
.ConstantState
> mOutsideDrawablesCache
=
182 new WeakHashMap
<String
, Drawable
.ConstantState
>();
185 * Callbacks for changes to the query text.
187 public interface OnQueryTextListener
{
190 * Called when the user submits the query. This could be due to a key press on the
191 * keyboard or due to pressing a submit button.
192 * The listener can override the standard behavior by returning true
193 * to indicate that it has handled the submit request. Otherwise return false to
194 * let the SearchView handle the submission by launching any associated intent.
196 * @param query the query text that is to be submitted
198 * @return true if the query has been handled by the listener, false to let the
199 * SearchView perform the default action.
201 boolean onQueryTextSubmit(String query
);
204 * Called when the query text is changed by the user.
206 * @param newText the new content of the query text field.
208 * @return false if the SearchView should perform the default action of showing any
209 * suggestions if available, true if the action was handled by the listener.
211 boolean onQueryTextChange(String newText
);
214 public interface OnCloseListener
{
217 * The user is attempting to close the SearchView.
219 * @return true if the listener wants to override the default behavior of clearing the
220 * text field and dismissing it, false otherwise.
226 * Callback interface for selection events on suggestions. These callbacks
227 * are only relevant when a SearchableInfo has been specified by {@link #setSearchableInfo}.
229 public interface OnSuggestionListener
{
232 * Called when a suggestion was selected by navigating to it.
233 * @param position the absolute position in the list of suggestions.
235 * @return true if the listener handles the event and wants to override the default
236 * behavior of possibly rewriting the query based on the selected item, false otherwise.
238 boolean onSuggestionSelect(int position
);
241 * Called when a suggestion was clicked.
242 * @param position the absolute position of the clicked item in the list of suggestions.
244 * @return true if the listener handles the event and wants to override the default
245 * behavior of launching any intent or submitting a search query specified on that item.
246 * Return false otherwise.
248 boolean onSuggestionClick(int position
);
251 public SearchView(Context context
) {
255 public SearchView(Context context
, AttributeSet attrs
) {
256 super(context
, attrs
);
258 if (Build
.VERSION
.SDK_INT
< Build
.VERSION_CODES
.FROYO
) {
259 throw new IllegalStateException("SearchView is API 8+ only.");
262 LayoutInflater inflater
= (LayoutInflater
) context
263 .getSystemService(Context
.LAYOUT_INFLATER_SERVICE
);
264 inflater
.inflate(R
.layout
.abs__search_view
, this, true
);
266 mSearchButton
= findViewById(R
.id
.abs__search_button
);
267 mQueryTextView
= (SearchAutoComplete
) findViewById(R
.id
.abs__search_src_text
);
268 mQueryTextView
.setSearchView(this);
270 mSearchEditFrame
= findViewById(R
.id
.abs__search_edit_frame
);
271 mSearchPlate
= findViewById(R
.id
.abs__search_plate
);
272 mSubmitArea
= findViewById(R
.id
.abs__submit_area
);
273 mSubmitButton
= findViewById(R
.id
.abs__search_go_btn
);
274 mCloseButton
= (ImageView
) findViewById(R
.id
.abs__search_close_btn
);
275 mVoiceButton
= findViewById(R
.id
.abs__search_voice_btn
);
276 mSearchHintIcon
= (ImageView
) findViewById(R
.id
.abs__search_mag_icon
);
278 mSearchButton
.setOnClickListener(mOnClickListener
);
279 mCloseButton
.setOnClickListener(mOnClickListener
);
280 mSubmitButton
.setOnClickListener(mOnClickListener
);
281 mVoiceButton
.setOnClickListener(mOnClickListener
);
282 mQueryTextView
.setOnClickListener(mOnClickListener
);
284 mQueryTextView
.addTextChangedListener(mTextWatcher
);
285 mQueryTextView
.setOnEditorActionListener(mOnEditorActionListener
);
286 mQueryTextView
.setOnItemClickListener(mOnItemClickListener
);
287 mQueryTextView
.setOnItemSelectedListener(mOnItemSelectedListener
);
288 mQueryTextView
.setOnKeyListener(mTextKeyListener
);
289 // Inform any listener of focus changes
290 mQueryTextView
.setOnFocusChangeListener(new OnFocusChangeListener() {
292 public void onFocusChange(View v
, boolean hasFocus
) {
293 if (mOnQueryTextFocusChangeListener
!= null
) {
294 mOnQueryTextFocusChangeListener
.onFocusChange(SearchView
.this, hasFocus
);
299 TypedArray a
= context
.obtainStyledAttributes(attrs
, R
.styleable
.SherlockSearchView
, 0, 0);
300 setIconifiedByDefault(a
.getBoolean(R
.styleable
.SherlockSearchView_iconifiedByDefault
, true
));
301 int maxWidth
= a
.getDimensionPixelSize(R
.styleable
.SherlockSearchView_android_maxWidth
, -1);
302 if (maxWidth
!= -1) {
303 setMaxWidth(maxWidth
);
305 CharSequence queryHint
= a
.getText(R
.styleable
.SherlockSearchView_queryHint
);
306 if (!TextUtils
.isEmpty(queryHint
)) {
307 setQueryHint(queryHint
);
309 int imeOptions
= a
.getInt(R
.styleable
.SherlockSearchView_android_imeOptions
, -1);
310 if (imeOptions
!= -1) {
311 setImeOptions(imeOptions
);
313 int inputType
= a
.getInt(R
.styleable
.SherlockSearchView_android_inputType
, -1);
314 if (inputType
!= -1) {
315 setInputType(inputType
);
320 boolean focusable
= true
;
322 a
= context
.obtainStyledAttributes(attrs
, R
.styleable
.SherlockView
, 0, 0);
323 focusable
= a
.getBoolean(R
.styleable
.SherlockView_android_focusable
, focusable
);
325 setFocusable(focusable
);
327 // Save voice intent for later queries/launching
328 mVoiceWebSearchIntent
= new Intent(RecognizerIntent
.ACTION_WEB_SEARCH
);
329 mVoiceWebSearchIntent
.addFlags(Intent
.FLAG_ACTIVITY_NEW_TASK
);
330 mVoiceWebSearchIntent
.putExtra(RecognizerIntent
.EXTRA_LANGUAGE_MODEL
,
331 RecognizerIntent
.LANGUAGE_MODEL_WEB_SEARCH
);
333 mVoiceAppSearchIntent
= new Intent(RecognizerIntent
.ACTION_RECOGNIZE_SPEECH
);
334 mVoiceAppSearchIntent
.addFlags(Intent
.FLAG_ACTIVITY_NEW_TASK
);
336 mDropDownAnchor
= findViewById(mQueryTextView
.getDropDownAnchor());
337 if (mDropDownAnchor
!= null
) {
338 if (Build
.VERSION
.SDK_INT
>= Build
.VERSION_CODES
.HONEYCOMB
) {
339 mDropDownAnchor
.addOnLayoutChangeListener(new OnLayoutChangeListener() {
341 public void onLayoutChange(View v
, int left
, int top
, int right
, int bottom
,
342 int oldLeft
, int oldTop
, int oldRight
, int oldBottom
) {
343 adjustDropDownSizeAndPosition();
347 mDropDownAnchor
.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver
.OnGlobalLayoutListener() {
348 @Override public void onGlobalLayout() {
349 adjustDropDownSizeAndPosition();
355 updateViewsVisibility(mIconifiedByDefault
);
360 * Sets the SearchableInfo for this SearchView. Properties in the SearchableInfo are used
361 * to display labels, hints, suggestions, create intents for launching search results screens
362 * and controlling other affordances such as a voice button.
364 * @param searchable a SearchableInfo can be retrieved from the SearchManager, for a specific
365 * activity or a global search provider.
367 public void setSearchableInfo(SearchableInfo searchable
) {
368 mSearchable
= searchable
;
369 if (mSearchable
!= null
) {
370 updateSearchAutoComplete();
373 // Cache the voice search capability
374 mVoiceButtonEnabled
= hasVoiceSearch();
376 if (mVoiceButtonEnabled
) {
377 // Disable the microphone on the keyboard, as a mic is displayed near the text box
378 // TODO: use imeOptions to disable voice input when the new API will be available
379 mQueryTextView
.setPrivateImeOptions(IME_OPTION_NO_MICROPHONE
);
381 updateViewsVisibility(isIconified());
385 * Sets the APP_DATA for legacy SearchDialog use.
386 * @param appSearchData bundle provided by the app when launching the search dialog
389 public void setAppSearchData(Bundle appSearchData
) {
390 mAppSearchData
= appSearchData
;
394 * Sets the IME options on the query text field.
396 * @see TextView#setImeOptions(int)
397 * @param imeOptions the options to set on the query text field
399 * @attr ref android.R.styleable#SearchView_imeOptions
401 public void setImeOptions(int imeOptions
) {
402 mQueryTextView
.setImeOptions(imeOptions
);
406 * Returns the IME options set on the query text field.
407 * @return the ime options
408 * @see TextView#setImeOptions(int)
410 * @attr ref android.R.styleable#SearchView_imeOptions
412 public int getImeOptions() {
413 return mQueryTextView
.getImeOptions();
417 * Sets the input type on the query text field.
419 * @see TextView#setInputType(int)
420 * @param inputType the input type to set on the query text field
422 * @attr ref android.R.styleable#SearchView_inputType
424 public void setInputType(int inputType
) {
425 mQueryTextView
.setInputType(inputType
);
429 * Returns the input type set on the query text field.
430 * @return the input type
432 * @attr ref android.R.styleable#SearchView_inputType
434 public int getInputType() {
435 return mQueryTextView
.getInputType();
440 public boolean requestFocus(int direction
, Rect previouslyFocusedRect
) {
441 // Don't accept focus if in the middle of clearing focus
442 if (mClearingFocus
) return false
;
443 // Check if SearchView is focusable.
444 if (!isFocusable()) return false
;
445 // If it is not iconified, then give the focus to the text field
446 if (!isIconified()) {
447 boolean result
= mQueryTextView
.requestFocus(direction
, previouslyFocusedRect
);
449 updateViewsVisibility(false
);
453 return super.requestFocus(direction
, previouslyFocusedRect
);
459 public void clearFocus() {
460 mClearingFocus
= true
;
461 setImeVisibility(false
);
463 mQueryTextView
.clearFocus();
464 mClearingFocus
= false
;
468 * Sets a listener for user actions within the SearchView.
470 * @param listener the listener object that receives callbacks when the user performs
471 * actions in the SearchView such as clicking on buttons or typing a query.
473 public void setOnQueryTextListener(OnQueryTextListener listener
) {
474 mOnQueryChangeListener
= listener
;
478 * Sets a listener to inform when the user closes the SearchView.
480 * @param listener the listener to call when the user closes the SearchView.
482 public void setOnCloseListener(OnCloseListener listener
) {
483 mOnCloseListener
= listener
;
487 * Sets a listener to inform when the focus of the query text field changes.
489 * @param listener the listener to inform of focus changes.
491 public void setOnQueryTextFocusChangeListener(OnFocusChangeListener listener
) {
492 mOnQueryTextFocusChangeListener
= listener
;
496 * Sets a listener to inform when a suggestion is focused or clicked.
498 * @param listener the listener to inform of suggestion selection events.
500 public void setOnSuggestionListener(OnSuggestionListener listener
) {
501 mOnSuggestionListener
= listener
;
505 * Sets a listener to inform when the search button is pressed. This is only
506 * relevant when the text field is not visible by default. Calling {@link #setIconified
507 * setIconified(false)} can also cause this listener to be informed.
509 * @param listener the listener to inform when the search button is clicked or
510 * the text field is programmatically de-iconified.
512 public void setOnSearchClickListener(OnClickListener listener
) {
513 mOnSearchClickListener
= listener
;
517 * Returns the query string currently in the text field.
519 * @return the query string
521 public CharSequence
getQuery() {
522 return mQueryTextView
.getText();
526 * Sets a query string in the text field and optionally submits the query as well.
528 * @param query the query string. This replaces any query text already present in the
530 * @param submit whether to submit the query right now or only update the contents of
533 public void setQuery(CharSequence query
, boolean submit
) {
534 mQueryTextView
.setText(query
);
536 mQueryTextView
.setSelection(mQueryTextView
.length());
540 // If the query is not empty and submit is requested, submit the query
541 if (submit
&& !TextUtils
.isEmpty(query
)) {
547 * Sets the hint text to display in the query text field. This overrides any hint specified
548 * in the SearchableInfo.
550 * @param hint the hint text to display
552 * @attr ref android.R.styleable#SearchView_queryHint
554 public void setQueryHint(CharSequence hint
) {
560 * Gets the hint text to display in the query text field.
561 * @return the query hint text, if specified, null otherwise.
563 * @attr ref android.R.styleable#SearchView_queryHint
565 public CharSequence
getQueryHint() {
566 if (mQueryHint
!= null
) {
568 } else if (mSearchable
!= null
) {
569 CharSequence hint
= null
;
570 int hintId
= mSearchable
.getHintId();
572 hint
= getContext().getString(hintId
);
580 * Sets the default or resting state of the search field. If true, a single search icon is
581 * shown by default and expands to show the text field and other buttons when pressed. Also,
582 * if the default state is iconified, then it collapses to that state when the close button
583 * is pressed. Changes to this property will take effect immediately.
585 * <p>The default value is true.</p>
587 * @param iconified whether the search field should be iconified by default
589 * @attr ref android.R.styleable#SearchView_iconifiedByDefault
591 public void setIconifiedByDefault(boolean iconified
) {
592 if (mIconifiedByDefault
== iconified
) return;
593 mIconifiedByDefault
= iconified
;
594 updateViewsVisibility(iconified
);
599 * Returns the default iconified state of the search field.
602 * @attr ref android.R.styleable#SearchView_iconifiedByDefault
604 public boolean isIconfiedByDefault() {
605 return mIconifiedByDefault
;
609 * Iconifies or expands the SearchView. Any query text is cleared when iconified. This is
610 * a temporary state and does not override the default iconified state set by
611 * {@link #setIconifiedByDefault(boolean)}. If the default state is iconified, then
612 * a false here will only be valid until the user closes the field. And if the default
613 * state is expanded, then a true here will only clear the text field and not close it.
615 * @param iconify a true value will collapse the SearchView to an icon, while a false will
618 public void setIconified(boolean iconify
) {
627 * Returns the current iconified state of the SearchView.
629 * @return true if the SearchView is currently iconified, false if the search field is
632 public boolean isIconified() {
637 * Enables showing a submit button when the query is non-empty. In cases where the SearchView
638 * is being used to filter the contents of the current activity and doesn't launch a separate
639 * results activity, then the submit button should be disabled.
641 * @param enabled true to show a submit button for submitting queries, false if a submit
642 * button is not required.
644 public void setSubmitButtonEnabled(boolean enabled
) {
645 mSubmitButtonEnabled
= enabled
;
646 updateViewsVisibility(isIconified());
650 * Returns whether the submit button is enabled when necessary or never displayed.
652 * @return whether the submit button is enabled automatically when necessary
654 public boolean isSubmitButtonEnabled() {
655 return mSubmitButtonEnabled
;
659 * Specifies if a query refinement button should be displayed alongside each suggestion
660 * or if it should depend on the flags set in the individual items retrieved from the
661 * suggestions provider. Clicking on the query refinement button will replace the text
662 * in the query text field with the text from the suggestion. This flag only takes effect
663 * if a SearchableInfo has been specified with {@link #setSearchableInfo(SearchableInfo)}
664 * and not when using a custom adapter.
666 * @param enable true if all items should have a query refinement button, false if only
667 * those items that have a query refinement flag set should have the button.
669 * @see SearchManager#SUGGEST_COLUMN_FLAGS
670 * @see SearchManager#FLAG_QUERY_REFINEMENT
672 public void setQueryRefinementEnabled(boolean enable
) {
673 mQueryRefinement
= enable
;
674 if (mSuggestionsAdapter
instanceof SuggestionsAdapter
) {
675 ((SuggestionsAdapter
) mSuggestionsAdapter
).setQueryRefinement(
676 enable ? SuggestionsAdapter
.REFINE_ALL
: SuggestionsAdapter
.REFINE_BY_ENTRY
);
681 * Returns whether query refinement is enabled for all items or only specific ones.
682 * @return true if enabled for all items, false otherwise.
684 public boolean isQueryRefinementEnabled() {
685 return mQueryRefinement
;
689 * You can set a custom adapter if you wish. Otherwise the default adapter is used to
690 * display the suggestions from the suggestions provider associated with the SearchableInfo.
692 * @see #setSearchableInfo(SearchableInfo)
694 public void setSuggestionsAdapter(CursorAdapter adapter
) {
695 mSuggestionsAdapter
= adapter
;
697 mQueryTextView
.setAdapter(mSuggestionsAdapter
);
701 * Returns the adapter used for suggestions, if any.
702 * @return the suggestions adapter
704 public CursorAdapter
getSuggestionsAdapter() {
705 return mSuggestionsAdapter
;
709 * Makes the view at most this many pixels wide
711 * @attr ref android.R.styleable#SearchView_maxWidth
713 public void setMaxWidth(int maxpixels
) {
714 mMaxWidth
= maxpixels
;
720 * Gets the specified maximum width in pixels, if set. Returns zero if
721 * no maximum width was specified.
722 * @return the maximum width of the view
724 * @attr ref android.R.styleable#SearchView_maxWidth
726 public int getMaxWidth() {
731 protected void onMeasure(int widthMeasureSpec
, int heightMeasureSpec
) {
732 // Let the standard measurements take effect in iconified state.
734 super.onMeasure(widthMeasureSpec
, heightMeasureSpec
);
738 int widthMode
= MeasureSpec
.getMode(widthMeasureSpec
);
739 int width
= MeasureSpec
.getSize(widthMeasureSpec
);
742 case MeasureSpec
.AT_MOST
:
743 // If there is an upper limit, don't exceed maximum width (explicit or implicit)
745 width
= Math
.min(mMaxWidth
, width
);
747 width
= Math
.min(getPreferredWidth(), width
);
750 case MeasureSpec
.EXACTLY
:
751 // If an exact width is specified, still don't exceed any specified maximum width
753 width
= Math
.min(mMaxWidth
, width
);
756 case MeasureSpec
.UNSPECIFIED
:
757 // Use maximum width, if specified, else preferred width
758 width
= mMaxWidth
> 0 ? mMaxWidth
: getPreferredWidth();
761 widthMode
= MeasureSpec
.EXACTLY
;
762 super.onMeasure(MeasureSpec
.makeMeasureSpec(width
, widthMode
), heightMeasureSpec
);
765 private int getPreferredWidth() {
766 return getContext().getResources()
767 .getDimensionPixelSize(R
.dimen
.abs__search_view_preferred_width
);
770 private void updateViewsVisibility(final boolean collapsed
) {
771 mIconified
= collapsed
;
772 // Visibility of views that are visible when collapsed
773 final int visCollapsed
= collapsed ? VISIBLE
: GONE
;
774 // Is there text in the query
775 final boolean hasText
= !TextUtils
.isEmpty(mQueryTextView
.getText());
777 mSearchButton
.setVisibility(visCollapsed
);
778 updateSubmitButton(hasText
);
779 mSearchEditFrame
.setVisibility(collapsed ? GONE
: VISIBLE
);
780 mSearchHintIcon
.setVisibility(mIconifiedByDefault ? GONE
: VISIBLE
);
782 updateVoiceButton(!hasText
);
786 private boolean hasVoiceSearch() {
787 if (mSearchable
!= null
&& mSearchable
.getVoiceSearchEnabled()) {
788 Intent testIntent
= null
;
789 if (mSearchable
.getVoiceSearchLaunchWebSearch()) {
790 testIntent
= mVoiceWebSearchIntent
;
791 } else if (mSearchable
.getVoiceSearchLaunchRecognizer()) {
792 testIntent
= mVoiceAppSearchIntent
;
794 if (testIntent
!= null
) {
795 ResolveInfo ri
= getContext().getPackageManager().resolveActivity(testIntent
,
796 PackageManager
.MATCH_DEFAULT_ONLY
);
803 private boolean isSubmitAreaEnabled() {
804 return (mSubmitButtonEnabled
|| mVoiceButtonEnabled
) && !isIconified();
807 private void updateSubmitButton(boolean hasText
) {
808 int visibility
= GONE
;
809 if (mSubmitButtonEnabled
&& isSubmitAreaEnabled() && hasFocus()
810 && (hasText
|| !mVoiceButtonEnabled
)) {
811 visibility
= VISIBLE
;
813 mSubmitButton
.setVisibility(visibility
);
816 private void updateSubmitArea() {
817 int visibility
= GONE
;
818 if (isSubmitAreaEnabled()
819 && (mSubmitButton
.getVisibility() == VISIBLE
820 || mVoiceButton
.getVisibility() == VISIBLE
)) {
821 visibility
= VISIBLE
;
823 mSubmitArea
.setVisibility(visibility
);
826 private void updateCloseButton() {
827 final boolean hasText
= !TextUtils
.isEmpty(mQueryTextView
.getText());
828 // Should we show the close button? It is not shown if there's no focus,
829 // field is not iconified by default and there is no text in it.
830 final boolean showClose
= hasText
|| (mIconifiedByDefault
&& !mExpandedInActionView
);
831 mCloseButton
.setVisibility(showClose ? VISIBLE
: GONE
);
832 mCloseButton
.getDrawable().setState(hasText ? ENABLED_STATE_SET
: EMPTY_STATE_SET
);
835 private void postUpdateFocusedState() {
836 post(mUpdateDrawableStateRunnable
);
839 private void updateFocusedState() {
840 boolean focused
= mQueryTextView
.hasFocus();
841 mSearchPlate
.getBackground().setState(focused ? FOCUSED_STATE_SET
: EMPTY_STATE_SET
);
842 mSubmitArea
.getBackground().setState(focused ? FOCUSED_STATE_SET
: EMPTY_STATE_SET
);
847 protected void onDetachedFromWindow() {
848 removeCallbacks(mUpdateDrawableStateRunnable
);
849 post(mReleaseCursorRunnable
);
850 super.onDetachedFromWindow();
853 private void setImeVisibility(final boolean visible
) {
855 post(mShowImeRunnable
);
857 removeCallbacks(mShowImeRunnable
);
858 InputMethodManager imm
= (InputMethodManager
)
859 getContext().getSystemService(Context
.INPUT_METHOD_SERVICE
);
862 imm
.hideSoftInputFromWindow(getWindowToken(), 0);
868 * Called by the SuggestionsAdapter
871 /* package */void onQueryRefine(CharSequence queryText
) {
875 private final OnClickListener mOnClickListener
= new OnClickListener() {
877 public void onClick(View v
) {
878 if (v
== mSearchButton
) {
880 } else if (v
== mCloseButton
) {
882 } else if (v
== mSubmitButton
) {
884 } else if (v
== mVoiceButton
) {
886 } else if (v
== mQueryTextView
) {
887 forceSuggestionQuery();
893 * Handles the key down event for dealing with action keys.
895 * @param keyCode This is the keycode of the typed key, and is the same value as
896 * found in the KeyEvent parameter.
897 * @param event The complete event record for the typed key
899 * @return true if the event was handled here, or false if not.
902 public boolean onKeyDown(int keyCode
, KeyEvent event
) {
903 if (mSearchable
== null
) {
907 // if it's an action specified by the searchable activity, launch the
908 // entered query with the action key
909 // TODO SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
910 // TODO if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) {
911 // TODO launchQuerySearch(keyCode, actionKey.getQueryActionMsg(), mQueryTextView.getText()
912 // TODO .toString());
916 return super.onKeyDown(keyCode
, event
);
920 * React to the user typing "enter" or other hardwired keys while typing in
921 * the search box. This handles these special keys while the edit box has
924 View
.OnKeyListener mTextKeyListener
= new View
.OnKeyListener() {
925 public boolean onKey(View v
, int keyCode
, KeyEvent event
) {
926 // guard against possible race conditions
927 if (mSearchable
== null
) {
932 Log
.d(LOG_TAG
, "mTextListener.onKey(" + keyCode
+ "," + event
+ "), selection: "
933 + mQueryTextView
.getListSelection());
936 // If a suggestion is selected, handle enter, search key, and action keys
937 // as presses on the selected suggestion
938 if (mQueryTextView
.isPopupShowing()
939 && mQueryTextView
.getListSelection() != ListView
.INVALID_POSITION
) {
940 return onSuggestionsKey(v
, keyCode
, event
);
943 // If there is text in the query box, handle enter, and action keys
944 // The search key is handled by the dialog's onKeyDown().
945 if (!mQueryTextView
.isEmpty() && KeyEventCompat
.hasNoModifiers(event
)) {
946 if (event
.getAction() == KeyEvent
.ACTION_UP
) {
947 if (keyCode
== KeyEvent
.KEYCODE_ENTER
) {
950 // Launch as a regular search.
951 launchQuerySearch(KeyEvent
.KEYCODE_UNKNOWN
, null
, mQueryTextView
.getText()
956 if (event
.getAction() == KeyEvent
.ACTION_DOWN
) {
957 // TODO SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
958 // TODO if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) {
959 // TODO launchQuerySearch(keyCode, actionKey.getQueryActionMsg(), mQueryTextView
960 // TODO .getText().toString());
970 * React to the user typing while in the suggestions list. First, check for
971 * action keys. If not handled, try refocusing regular characters into the
974 private boolean onSuggestionsKey(View v
, int keyCode
, KeyEvent event
) {
975 // guard against possible race conditions (late arrival after dismiss)
976 if (mSearchable
== null
) {
979 if (mSuggestionsAdapter
== null
) {
982 if (event
.getAction() == KeyEvent
.ACTION_DOWN
&& KeyEventCompat
.hasNoModifiers(event
)) {
983 // First, check for enter or search (both of which we'll treat as a
985 if (keyCode
== KeyEvent
.KEYCODE_ENTER
|| keyCode
== KeyEvent
.KEYCODE_SEARCH
986 || keyCode
== KeyEvent
.KEYCODE_TAB
) {
987 int position
= mQueryTextView
.getListSelection();
988 return onItemClicked(position
, KeyEvent
.KEYCODE_UNKNOWN
, null
);
991 // Next, check for left/right moves, which we use to "return" the
992 // user to the edit view
993 if (keyCode
== KeyEvent
.KEYCODE_DPAD_LEFT
|| keyCode
== KeyEvent
.KEYCODE_DPAD_RIGHT
) {
994 // give "focus" to text editor, with cursor at the beginning if
995 // left key, at end if right key
996 // TODO: Reverse left/right for right-to-left languages, e.g.
998 int selPoint
= (keyCode
== KeyEvent
.KEYCODE_DPAD_LEFT
) ?
0 : mQueryTextView
1000 mQueryTextView
.setSelection(selPoint
);
1001 mQueryTextView
.setListSelection(0);
1002 mQueryTextView
.clearListSelection();
1003 ensureImeVisible(mQueryTextView
, true
);
1008 // Next, check for an "up and out" move
1009 if (keyCode
== KeyEvent
.KEYCODE_DPAD_UP
&& 0 == mQueryTextView
.getListSelection()) {
1010 // TODO: restoreUserQuery();
1011 // let ACTV complete the move
1015 // Next, check for an "action key"
1016 // TODO SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
1017 // TODO if ((actionKey != null)
1018 // TODO && ((actionKey.getSuggestActionMsg() != null) || (actionKey
1019 // TODO .getSuggestActionMsgColumn() != null))) {
1020 // TODO // launch suggestion using action key column
1021 // TODO int position = mQueryTextView.getListSelection();
1022 // TODO if (position != ListView.INVALID_POSITION) {
1023 // TODO Cursor c = mSuggestionsAdapter.getCursor();
1024 // TODO if (c.moveToPosition(position)) {
1025 // TODO final String actionMsg = getActionKeyMessage(c, actionKey);
1026 // TODO if (actionMsg != null && (actionMsg.length() > 0)) {
1027 // TODO return onItemClicked(position, keyCode, actionMsg);
1037 * For a given suggestion and a given cursor row, get the action message. If
1038 * not provided by the specific row/column, also check for a single
1039 * definition (for the action key).
1041 * @param c The cursor providing suggestions
1042 * @param actionKey The actionkey record being examined
1044 * @return Returns a string, or null if no action key message for this
1047 // TODO private static String getActionKeyMessage(Cursor c, SearchableInfo.ActionKeyInfo actionKey) {
1048 // TODO String result = null;
1049 // TODO // check first in the cursor data, for a suggestion-specific message
1050 // TODO final String column = actionKey.getSuggestActionMsgColumn();
1051 // TODO if (column != null) {
1052 // TODO result = SuggestionsAdapter.getColumnString(c, column);
1054 // TODO // If the cursor didn't give us a message, see if there's a single
1055 // TODO // message defined
1056 // TODO // for the actionkey (for all suggestions)
1057 // TODO if (result == null) {
1058 // TODO result = actionKey.getSuggestActionMsg();
1060 // TODO return result;
1063 private int getSearchIconId() {
1064 TypedValue outValue
= new TypedValue();
1065 getContext().getTheme().resolveAttribute(R
.attr
.searchViewSearchIcon
,
1067 return outValue
.resourceId
;
1070 private CharSequence
getDecoratedHint(CharSequence hintText
) {
1071 // If the field is always expanded, then don't add the search icon to the hint
1072 if (!mIconifiedByDefault
) return hintText
;
1074 SpannableStringBuilder ssb
= new SpannableStringBuilder(" "); // for the icon
1075 ssb
.append(hintText
);
1076 Drawable searchIcon
= getContext().getResources().getDrawable(getSearchIconId());
1077 int textSize
= (int) (mQueryTextView
.getTextSize() * 1.25);
1078 searchIcon
.setBounds(0, 0, textSize
, textSize
);
1079 ssb
.setSpan(new ImageSpan(searchIcon
), 1, 2, Spannable
.SPAN_EXCLUSIVE_EXCLUSIVE
);
1083 private void updateQueryHint() {
1084 if (mQueryHint
!= null
) {
1085 mQueryTextView
.setHint(getDecoratedHint(mQueryHint
));
1086 } else if (mSearchable
!= null
) {
1087 CharSequence hint
= null
;
1088 int hintId
= mSearchable
.getHintId();
1090 hint
= getContext().getString(hintId
);
1093 mQueryTextView
.setHint(getDecoratedHint(hint
));
1096 mQueryTextView
.setHint(getDecoratedHint(""));
1101 * Updates the auto-complete text view.
1103 private void updateSearchAutoComplete() {
1104 // TODO mQueryTextView.setDropDownAnimationStyle(0); // no animation
1105 mQueryTextView
.setThreshold(mSearchable
.getSuggestThreshold());
1106 mQueryTextView
.setImeOptions(mSearchable
.getImeOptions());
1107 int inputType
= mSearchable
.getInputType();
1108 // We only touch this if the input type is set up for text (which it almost certainly
1109 // should be, in the case of search!)
1110 if ((inputType
& InputType
.TYPE_MASK_CLASS
) == InputType
.TYPE_CLASS_TEXT
) {
1111 // The existence of a suggestions authority is the proxy for "suggestions
1112 // are available here"
1113 inputType
&= ~InputType
.TYPE_TEXT_FLAG_AUTO_COMPLETE
;
1114 if (mSearchable
.getSuggestAuthority() != null
) {
1115 inputType
|= InputType
.TYPE_TEXT_FLAG_AUTO_COMPLETE
;
1116 // TYPE_TEXT_FLAG_AUTO_COMPLETE means that the text editor is performing
1117 // auto-completion based on its own semantics, which it will present to the user
1118 // as they type. This generally means that the input method should not show its
1119 // own candidates, and the spell checker should not be in action. The text editor
1120 // supplies its candidates by calling InputMethodManager.displayCompletions(),
1121 // which in turn will call InputMethodSession.displayCompletions().
1122 inputType
|= InputType
.TYPE_TEXT_FLAG_NO_SUGGESTIONS
;
1125 mQueryTextView
.setInputType(inputType
);
1126 if (mSuggestionsAdapter
!= null
) {
1127 mSuggestionsAdapter
.changeCursor(null
);
1129 // attach the suggestions adapter, if suggestions are available
1130 // The existence of a suggestions authority is the proxy for "suggestions available here"
1131 if (mSearchable
.getSuggestAuthority() != null
) {
1132 mSuggestionsAdapter
= new SuggestionsAdapter(getContext(),
1133 this, mSearchable
, mOutsideDrawablesCache
);
1134 mQueryTextView
.setAdapter(mSuggestionsAdapter
);
1135 ((SuggestionsAdapter
) mSuggestionsAdapter
).setQueryRefinement(
1136 mQueryRefinement ? SuggestionsAdapter
.REFINE_ALL
1137 : SuggestionsAdapter
.REFINE_BY_ENTRY
);
1142 * Update the visibility of the voice button. There are actually two voice search modes,
1143 * either of which will activate the button.
1144 * @param empty whether the search query text field is empty. If it is, then the other
1145 * criteria apply to make the voice button visible.
1147 private void updateVoiceButton(boolean empty
) {
1148 int visibility
= GONE
;
1149 if (mVoiceButtonEnabled
&& !isIconified() && empty
) {
1150 visibility
= VISIBLE
;
1151 mSubmitButton
.setVisibility(GONE
);
1153 mVoiceButton
.setVisibility(visibility
);
1156 private final OnEditorActionListener mOnEditorActionListener
= new OnEditorActionListener() {
1159 * Called when the input method default action key is pressed.
1161 public boolean onEditorAction(TextView v
, int actionId
, KeyEvent event
) {
1167 private void onTextChanged(CharSequence newText
) {
1168 CharSequence text
= mQueryTextView
.getText();
1170 boolean hasText
= !TextUtils
.isEmpty(text
);
1171 updateSubmitButton(hasText
);
1172 updateVoiceButton(!hasText
);
1173 updateCloseButton();
1175 if (mOnQueryChangeListener
!= null
&& !TextUtils
.equals(newText
, mOldQueryText
)) {
1176 mOnQueryChangeListener
.onQueryTextChange(newText
.toString());
1178 mOldQueryText
= newText
.toString();
1181 private void onSubmitQuery() {
1182 CharSequence query
= mQueryTextView
.getText();
1183 if (query
!= null
&& TextUtils
.getTrimmedLength(query
) > 0) {
1184 if (mOnQueryChangeListener
== null
1185 || !mOnQueryChangeListener
.onQueryTextSubmit(query
.toString())) {
1186 if (mSearchable
!= null
) {
1187 launchQuerySearch(KeyEvent
.KEYCODE_UNKNOWN
, null
, query
.toString());
1188 setImeVisibility(false
);
1190 dismissSuggestions();
1195 private void dismissSuggestions() {
1196 mQueryTextView
.dismissDropDown();
1199 private void onCloseClicked() {
1200 CharSequence text
= mQueryTextView
.getText();
1201 if (TextUtils
.isEmpty(text
)) {
1202 if (mIconifiedByDefault
) {
1203 // If the app doesn't override the close behavior
1204 if (mOnCloseListener
== null
|| !mOnCloseListener
.onClose()) {
1205 // hide the keyboard and remove focus
1207 // collapse the search field
1208 updateViewsVisibility(true
);
1212 mQueryTextView
.setText("");
1213 mQueryTextView
.requestFocus();
1214 setImeVisibility(true
);
1219 private void onSearchClicked() {
1220 updateViewsVisibility(false
);
1221 mQueryTextView
.requestFocus();
1222 setImeVisibility(true
);
1223 if (mOnSearchClickListener
!= null
) {
1224 mOnSearchClickListener
.onClick(this);
1228 private void onVoiceClicked() {
1229 // guard against possible race conditions
1230 if (mSearchable
== null
) {
1233 SearchableInfo searchable
= mSearchable
;
1235 if (searchable
.getVoiceSearchLaunchWebSearch()) {
1236 Intent webSearchIntent
= createVoiceWebSearchIntent(mVoiceWebSearchIntent
,
1238 getContext().startActivity(webSearchIntent
);
1239 } else if (searchable
.getVoiceSearchLaunchRecognizer()) {
1240 Intent appSearchIntent
= createVoiceAppSearchIntent(mVoiceAppSearchIntent
,
1242 getContext().startActivity(appSearchIntent
);
1244 } catch (ActivityNotFoundException e
) {
1245 // Should not happen, since we check the availability of
1246 // voice search before showing the button. But just in case...
1247 Log
.w(LOG_TAG
, "Could not find voice search activity");
1251 void onTextFocusChanged() {
1252 updateViewsVisibility(isIconified());
1253 // Delayed update to make sure that the focus has settled down and window focus changes
1254 // don't affect it. A synchronous update was not working.
1255 postUpdateFocusedState();
1256 if (mQueryTextView
.hasFocus()) {
1257 forceSuggestionQuery();
1262 public void onWindowFocusChanged(boolean hasWindowFocus
) {
1263 super.onWindowFocusChanged(hasWindowFocus
);
1265 postUpdateFocusedState();
1272 public void onActionViewCollapsed() {
1274 updateViewsVisibility(true
);
1275 mQueryTextView
.setImeOptions(mCollapsedImeOptions
);
1276 mExpandedInActionView
= false
;
1283 public void onActionViewExpanded() {
1284 if (mExpandedInActionView
) return;
1286 mExpandedInActionView
= true
;
1287 mCollapsedImeOptions
= mQueryTextView
.getImeOptions();
1288 mQueryTextView
.setImeOptions(mCollapsedImeOptions
| EditorInfo
.IME_FLAG_NO_FULLSCREEN
);
1289 mQueryTextView
.setText("");
1290 setIconified(false
);
1294 public void onInitializeAccessibilityEvent(AccessibilityEvent event
) {
1295 super.onInitializeAccessibilityEvent(event
);
1296 event
.setClassName(SearchView
.class.getName());
1300 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info
) {
1301 super.onInitializeAccessibilityNodeInfo(info
);
1302 info
.setClassName(SearchView
.class.getName());
1305 private void adjustDropDownSizeAndPosition() {
1306 if (mDropDownAnchor
.getWidth() > 1) {
1307 Resources res
= getContext().getResources();
1308 int anchorPadding
= mSearchPlate
.getPaddingLeft();
1309 Rect dropDownPadding
= new Rect();
1310 int iconOffset
= mIconifiedByDefault
1311 ? res
.getDimensionPixelSize(R
.dimen
.abs__dropdownitem_icon_width
)
1312 + res
.getDimensionPixelSize(R
.dimen
.abs__dropdownitem_text_padding_left
)
1314 mQueryTextView
.getDropDownBackground().getPadding(dropDownPadding
);
1315 mQueryTextView
.setDropDownHorizontalOffset(-(dropDownPadding
.left
+ iconOffset
)
1317 mQueryTextView
.setDropDownWidth(mDropDownAnchor
.getWidth() + dropDownPadding
.left
1318 + dropDownPadding
.right
+ iconOffset
- (anchorPadding
));
1322 private boolean onItemClicked(int position
, int actionKey
, String actionMsg
) {
1323 if (mOnSuggestionListener
== null
1324 || !mOnSuggestionListener
.onSuggestionClick(position
)) {
1325 launchSuggestion(position
, KeyEvent
.KEYCODE_UNKNOWN
, null
);
1326 setImeVisibility(false
);
1327 dismissSuggestions();
1333 private boolean onItemSelected(int position
) {
1334 if (mOnSuggestionListener
== null
1335 || !mOnSuggestionListener
.onSuggestionSelect(position
)) {
1336 rewriteQueryFromSuggestion(position
);
1342 private final OnItemClickListener mOnItemClickListener
= new OnItemClickListener() {
1345 * Implements OnItemClickListener
1347 public void onItemClick(AdapterView
<?
> parent
, View view
, int position
, long id
) {
1348 if (DBG
) Log
.d(LOG_TAG
, "onItemClick() position " + position
);
1349 onItemClicked(position
, KeyEvent
.KEYCODE_UNKNOWN
, null
);
1353 private final OnItemSelectedListener mOnItemSelectedListener
= new OnItemSelectedListener() {
1356 * Implements OnItemSelectedListener
1358 public void onItemSelected(AdapterView
<?
> parent
, View view
, int position
, long id
) {
1359 if (DBG
) Log
.d(LOG_TAG
, "onItemSelected() position " + position
);
1360 SearchView
.this.onItemSelected(position
);
1364 * Implements OnItemSelectedListener
1366 public void onNothingSelected(AdapterView
<?
> parent
) {
1368 Log
.d(LOG_TAG
, "onNothingSelected()");
1375 private void rewriteQueryFromSuggestion(int position
) {
1376 CharSequence oldQuery
= mQueryTextView
.getText();
1377 Cursor c
= mSuggestionsAdapter
.getCursor();
1381 if (c
.moveToPosition(position
)) {
1382 // Get the new query from the suggestion.
1383 CharSequence newQuery
= mSuggestionsAdapter
.convertToString(c
);
1384 if (newQuery
!= null
) {
1385 // The suggestion rewrites the query.
1386 // Update the text field, without getting new suggestions.
1389 // The suggestion does not rewrite the query, restore the user's query.
1393 // We got a bad position, restore the user's query.
1399 * Launches an intent based on a suggestion.
1401 * @param position The index of the suggestion to create the intent from.
1402 * @param actionKey The key code of the action key that was pressed,
1403 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
1404 * @param actionMsg The message for the action key that was pressed,
1405 * or <code>null</code> if none.
1406 * @return true if a successful launch, false if could not (e.g. bad position).
1408 private boolean launchSuggestion(int position
, int actionKey
, String actionMsg
) {
1409 Cursor c
= mSuggestionsAdapter
.getCursor();
1410 if ((c
!= null
) && c
.moveToPosition(position
)) {
1412 Intent intent
= createIntentFromSuggestion(c
, actionKey
, actionMsg
);
1414 // launch the intent
1415 launchIntent(intent
);
1423 * Launches an intent, including any special intent handling.
1425 private void launchIntent(Intent intent
) {
1426 if (intent
== null
) {
1430 // If the intent was created from a suggestion, it will always have an explicit
1432 getContext().startActivity(intent
);
1433 } catch (RuntimeException ex
) {
1434 Log
.e(LOG_TAG
, "Failed launch activity: " + intent
, ex
);
1439 * Sets the text in the query box, without updating the suggestions.
1441 private void setQuery(CharSequence query
) {
1442 setText(mQueryTextView
, query
, true
);
1443 // Move the cursor to the end
1444 mQueryTextView
.setSelection(TextUtils
.isEmpty(query
) ?
0 : query
.length());
1447 private void launchQuerySearch(int actionKey
, String actionMsg
, String query
) {
1448 String action
= Intent
.ACTION_SEARCH
;
1449 Intent intent
= createIntent(action
, null
, null
, query
, actionKey
, actionMsg
);
1450 getContext().startActivity(intent
);
1454 * Constructs an intent from the given information and the search dialog state.
1456 * @param action Intent action.
1457 * @param data Intent data, or <code>null</code>.
1458 * @param extraData Data for {@link SearchManager#EXTRA_DATA_KEY} or <code>null</code>.
1459 * @param query Intent query, or <code>null</code>.
1460 * @param actionKey The key code of the action key that was pressed,
1461 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
1462 * @param actionMsg The message for the action key that was pressed,
1463 * or <code>null</code> if none.
1464 * @return The intent.
1466 private Intent
createIntent(String action
, Uri data
, String extraData
, String query
,
1467 int actionKey
, String actionMsg
) {
1468 // Now build the Intent
1469 Intent intent
= new Intent(action
);
1470 intent
.addFlags(Intent
.FLAG_ACTIVITY_NEW_TASK
);
1471 // We need CLEAR_TOP to avoid reusing an old task that has other activities
1472 // on top of the one we want. We don't want to do this in in-app search though,
1473 // as it can be destructive to the activity stack.
1475 intent
.setData(data
);
1477 intent
.putExtra(SearchManager
.USER_QUERY
, mUserQuery
);
1478 if (query
!= null
) {
1479 intent
.putExtra(SearchManager
.QUERY
, query
);
1481 if (extraData
!= null
) {
1482 intent
.putExtra(SearchManager
.EXTRA_DATA_KEY
, extraData
);
1484 if (mAppSearchData
!= null
) {
1485 intent
.putExtra(SearchManager
.APP_DATA
, mAppSearchData
);
1487 if (actionKey
!= KeyEvent
.KEYCODE_UNKNOWN
) {
1488 intent
.putExtra(SearchManager
.ACTION_KEY
, actionKey
);
1489 intent
.putExtra(SearchManager
.ACTION_MSG
, actionMsg
);
1491 intent
.setComponent(mSearchable
.getSearchActivity());
1496 * Create and return an Intent that can launch the voice search activity for web search.
1498 private Intent
createVoiceWebSearchIntent(Intent baseIntent
, SearchableInfo searchable
) {
1499 Intent voiceIntent
= new Intent(baseIntent
);
1500 ComponentName searchActivity
= searchable
.getSearchActivity();
1501 voiceIntent
.putExtra(RecognizerIntent
.EXTRA_CALLING_PACKAGE
, searchActivity
== null ? null
1502 : searchActivity
.flattenToShortString());
1507 * Create and return an Intent that can launch the voice search activity, perform a specific
1508 * voice transcription, and forward the results to the searchable activity.
1510 * @param baseIntent The voice app search intent to start from
1511 * @return A completely-configured intent ready to send to the voice search activity
1513 private Intent
createVoiceAppSearchIntent(Intent baseIntent
, SearchableInfo searchable
) {
1514 ComponentName searchActivity
= searchable
.getSearchActivity();
1516 // create the necessary intent to set up a search-and-forward operation
1517 // in the voice search system. We have to keep the bundle separate,
1518 // because it becomes immutable once it enters the PendingIntent
1519 Intent queryIntent
= new Intent(Intent
.ACTION_SEARCH
);
1520 queryIntent
.setComponent(searchActivity
);
1521 PendingIntent pending
= PendingIntent
.getActivity(getContext(), 0, queryIntent
,
1522 PendingIntent
.FLAG_ONE_SHOT
);
1524 // Now set up the bundle that will be inserted into the pending intent
1525 // when it's time to do the search. We always build it here (even if empty)
1526 // because the voice search activity will always need to insert "QUERY" into
1528 Bundle queryExtras
= new Bundle();
1530 // Now build the intent to launch the voice search. Add all necessary
1531 // extras to launch the voice recognizer, and then all the necessary extras
1532 // to forward the results to the searchable activity
1533 Intent voiceIntent
= new Intent(baseIntent
);
1535 // Add all of the configuration options supplied by the searchable's metadata
1536 String languageModel
= RecognizerIntent
.LANGUAGE_MODEL_FREE_FORM
;
1537 String prompt
= null
;
1538 String language
= null
;
1541 Resources resources
= getResources();
1542 if (searchable
.getVoiceLanguageModeId() != 0) {
1543 languageModel
= resources
.getString(searchable
.getVoiceLanguageModeId());
1545 if (searchable
.getVoicePromptTextId() != 0) {
1546 prompt
= resources
.getString(searchable
.getVoicePromptTextId());
1548 if (searchable
.getVoiceLanguageId() != 0) {
1549 language
= resources
.getString(searchable
.getVoiceLanguageId());
1551 if (searchable
.getVoiceMaxResults() != 0) {
1552 maxResults
= searchable
.getVoiceMaxResults();
1554 voiceIntent
.putExtra(RecognizerIntent
.EXTRA_LANGUAGE_MODEL
, languageModel
);
1555 voiceIntent
.putExtra(RecognizerIntent
.EXTRA_PROMPT
, prompt
);
1556 voiceIntent
.putExtra(RecognizerIntent
.EXTRA_LANGUAGE
, language
);
1557 voiceIntent
.putExtra(RecognizerIntent
.EXTRA_MAX_RESULTS
, maxResults
);
1558 voiceIntent
.putExtra(RecognizerIntent
.EXTRA_CALLING_PACKAGE
, searchActivity
== null ? null
1559 : searchActivity
.flattenToShortString());
1561 // Add the values that configure forwarding the results
1562 voiceIntent
.putExtra(RecognizerIntent
.EXTRA_RESULTS_PENDINGINTENT
, pending
);
1563 voiceIntent
.putExtra(RecognizerIntent
.EXTRA_RESULTS_PENDINGINTENT_BUNDLE
, queryExtras
);
1569 * When a particular suggestion has been selected, perform the various lookups required
1570 * to use the suggestion. This includes checking the cursor for suggestion-specific data,
1571 * and/or falling back to the XML for defaults; It also creates REST style Uri data when
1572 * the suggestion includes a data id.
1574 * @param c The suggestions cursor, moved to the row of the user's selection
1575 * @param actionKey The key code of the action key that was pressed,
1576 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
1577 * @param actionMsg The message for the action key that was pressed,
1578 * or <code>null</code> if none.
1579 * @return An intent for the suggestion at the cursor's position.
1581 private Intent
createIntentFromSuggestion(Cursor c
, int actionKey
, String actionMsg
) {
1583 // use specific action if supplied, or default action if supplied, or fixed default
1584 String action
= getColumnString(c
, SearchManager
.SUGGEST_COLUMN_INTENT_ACTION
);
1586 if (action
== null
) {
1587 action
= mSearchable
.getSuggestIntentAction();
1589 if (action
== null
) {
1590 action
= Intent
.ACTION_SEARCH
;
1593 // use specific data if supplied, or default data if supplied
1594 String data
= getColumnString(c
, SearchManager
.SUGGEST_COLUMN_INTENT_DATA
);
1596 data
= mSearchable
.getSuggestIntentData();
1598 // then, if an ID was provided, append it.
1600 String id
= getColumnString(c
, SearchManager
.SUGGEST_COLUMN_INTENT_DATA_ID
);
1602 data
= data
+ "/" + Uri
.encode(id
);
1605 Uri dataUri
= (data
== null
) ? null
: Uri
.parse(data
);
1607 String query
= getColumnString(c
, SearchManager
.SUGGEST_COLUMN_QUERY
);
1608 String extraData
= getColumnString(c
, SearchManager
.SUGGEST_COLUMN_INTENT_EXTRA_DATA
);
1610 return createIntent(action
, dataUri
, extraData
, query
, actionKey
, actionMsg
);
1611 } catch (RuntimeException e
) {
1613 try { // be really paranoid now
1614 rowNum
= c
.getPosition();
1615 } catch (RuntimeException e2
) {
1618 Log
.w(LOG_TAG
, "Search suggestions cursor at row " + rowNum
+
1619 " returned exception.", e
);
1624 private void forceSuggestionQuery() {
1626 Method before
= SearchAutoComplete
.class.getMethod("doBeforeTextChanged");
1627 Method after
= SearchAutoComplete
.class.getMethod("doAfterTextChanged");
1628 before
.setAccessible(true
);
1629 after
.setAccessible(true
);
1630 before
.invoke(mQueryTextView
);
1631 after
.invoke(mQueryTextView
);
1632 } catch (Exception e
) {
1637 static boolean isLandscapeMode(Context context
) {
1638 return context
.getResources().getConfiguration().orientation
1639 == Configuration
.ORIENTATION_LANDSCAPE
;
1643 * Callback to watch the text field for empty/non-empty
1645 private TextWatcher mTextWatcher
= new TextWatcher() {
1647 public void beforeTextChanged(CharSequence s
, int start
, int before
, int after
) { }
1649 public void onTextChanged(CharSequence s
, int start
,
1650 int before
, int after
) {
1651 SearchView
.this.onTextChanged(s
);
1654 public void afterTextChanged(Editable s
) {
1659 * Local subclass for AutoCompleteTextView.
1662 public static class SearchAutoComplete
extends AutoCompleteTextView
{
1664 private int mThreshold
;
1665 private SearchView mSearchView
;
1667 public SearchAutoComplete(Context context
) {
1669 mThreshold
= getThreshold();
1672 public SearchAutoComplete(Context context
, AttributeSet attrs
) {
1673 super(context
, attrs
);
1674 mThreshold
= getThreshold();
1677 public SearchAutoComplete(Context context
, AttributeSet attrs
, int defStyle
) {
1678 super(context
, attrs
, defStyle
);
1679 mThreshold
= getThreshold();
1682 void setSearchView(SearchView searchView
) {
1683 mSearchView
= searchView
;
1687 public void setThreshold(int threshold
) {
1688 super.setThreshold(threshold
);
1689 mThreshold
= threshold
;
1693 * Returns true if the text field is empty, or contains only whitespace.
1695 private boolean isEmpty() {
1696 return TextUtils
.getTrimmedLength(getText()) == 0;
1700 * We override this method to avoid replacing the query box text when a
1701 * suggestion is clicked.
1704 protected void replaceText(CharSequence text
) {
1708 * We override this method to avoid an extra onItemClick being called on
1709 * the drop-down's OnItemClickListener by
1710 * {@link AutoCompleteTextView#onKeyUp(int, KeyEvent)} when an item is
1711 * clicked with the trackball.
1714 public void performCompletion() {
1718 * We override this method to be sure and show the soft keyboard if
1719 * appropriate when the TextView has focus.
1722 public void onWindowFocusChanged(boolean hasWindowFocus
) {
1723 super.onWindowFocusChanged(hasWindowFocus
);
1725 if (hasWindowFocus
&& mSearchView
.hasFocus() && getVisibility() == VISIBLE
) {
1726 InputMethodManager inputManager
= (InputMethodManager
) getContext()
1727 .getSystemService(Context
.INPUT_METHOD_SERVICE
);
1728 inputManager
.showSoftInput(this, 0);
1729 // If in landscape mode, then make sure that
1730 // the ime is in front of the dropdown.
1731 if (isLandscapeMode(getContext())) {
1732 ensureImeVisible(this, true
);
1738 protected void onFocusChanged(boolean focused
, int direction
, Rect previouslyFocusedRect
) {
1739 super.onFocusChanged(focused
, direction
, previouslyFocusedRect
);
1740 mSearchView
.onTextFocusChanged();
1744 * We override this method so that we can allow a threshold of zero,
1745 * which ACTV does not.
1748 public boolean enoughToFilter() {
1749 return mThreshold
<= 0 || super.enoughToFilter();
1753 public boolean onKeyPreIme(int keyCode
, KeyEvent event
) {
1754 if (keyCode
== KeyEvent
.KEYCODE_BACK
) {
1755 // special case for the back key, we do not even try to send it
1756 // to the drop down list but instead, consume it immediately
1757 if (event
.getAction() == KeyEvent
.ACTION_DOWN
&& event
.getRepeatCount() == 0) {
1758 KeyEvent
.DispatcherState state
= getKeyDispatcherState();
1759 if (state
!= null
) {
1760 state
.startTracking(event
, this);
1763 } else if (event
.getAction() == KeyEvent
.ACTION_UP
) {
1764 KeyEvent
.DispatcherState state
= getKeyDispatcherState();
1765 if (state
!= null
) {
1766 state
.handleUpEvent(event
);
1768 if (event
.isTracking() && !event
.isCanceled()) {
1769 mSearchView
.clearFocus();
1770 mSearchView
.setImeVisibility(false
);
1775 return super.onKeyPreIme(keyCode
, event
);
1780 private static void ensureImeVisible(AutoCompleteTextView view
, boolean visible
) {
1782 Method method
= AutoCompleteTextView
.class.getMethod("ensureImeVisible", boolean.class);
1783 method
.setAccessible(true
);
1784 method
.invoke(view
, visible
);
1785 } catch (Exception e
) {
1790 private static void showSoftInputUnchecked(View view
, InputMethodManager imm
, int flags
) {
1792 Method method
= imm
.getClass().getMethod("showSoftInputUnchecked", int.class, ResultReceiver
.class);
1793 method
.setAccessible(true
);
1794 method
.invoke(imm
, flags
, null
);
1795 } catch (Exception e
) {
1796 //Fallback to public API which hopefully does mostly the same thing
1797 imm
.showSoftInput(view
, flags
);
1801 private static void setText(AutoCompleteTextView view
, CharSequence text
, boolean filter
) {
1803 Method method
= AutoCompleteTextView
.class.getMethod("setText", CharSequence
.class, boolean.class);
1804 method
.setAccessible(true
);
1805 method
.invoke(view
, text
, filter
);
1806 } catch (Exception e
) {
1807 //Fallback to public API which hopefully does mostly the same thing