Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright (C) 2010 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.widget;
     18 
     19 import static android.widget.SuggestionsAdapter.getColumnString;
     20 
     21 import android.app.PendingIntent;
     22 import android.app.SearchManager;
     23 import android.app.SearchableInfo;
     24 import android.content.ActivityNotFoundException;
     25 import android.content.ComponentName;
     26 import android.content.Context;
     27 import android.content.Intent;
     28 import android.content.pm.PackageManager;
     29 import android.content.pm.ResolveInfo;
     30 import android.content.res.Configuration;
     31 import android.content.res.Resources;
     32 import android.content.res.TypedArray;
     33 import android.database.Cursor;
     34 import android.graphics.Rect;
     35 import android.graphics.drawable.Drawable;
     36 import android.net.Uri;
     37 import android.os.Bundle;
     38 import android.os.Handler;
     39 import android.speech.RecognizerIntent;
     40 import android.text.Editable;
     41 import android.text.InputType;
     42 import android.text.Spannable;
     43 import android.text.SpannableStringBuilder;
     44 import android.text.TextUtils;
     45 import android.text.TextWatcher;
     46 import android.text.style.ImageSpan;
     47 import android.util.AttributeSet;
     48 import android.util.Log;
     49 import android.util.TypedValue;
     50 import android.view.CollapsibleActionView;
     51 import android.view.KeyEvent;
     52 import android.view.LayoutInflater;
     53 import android.view.View;
     54 import android.view.inputmethod.EditorInfo;
     55 import android.view.inputmethod.InputMethodManager;
     56 import android.widget.AdapterView.OnItemClickListener;
     57 import android.widget.AdapterView.OnItemSelectedListener;
     58 import android.widget.TextView.OnEditorActionListener;
     59 
     60 import com.android.internal.R;
     61 
     62 import java.util.WeakHashMap;
     63 
     64 /**
     65  * A widget that provides a user interface for the user to enter a search query and submit a request
     66  * to a search provider. Shows a list of query suggestions or results, if available, and allows the
     67  * user to pick a suggestion or result to launch into.
     68  *
     69  * <p>
     70  * When the SearchView is used in an ActionBar as an action view for a collapsible menu item, it
     71  * needs to be set to iconified by default using {@link #setIconifiedByDefault(boolean)
     72  * setIconifiedByDefault(true)}. This is the default, so nothing needs to be done.
     73  * </p>
     74  * <p>
     75  * If you want the search field to always be visible, then call setIconifiedByDefault(false).
     76  * </p>
     77  *
     78  * <p>
     79  * For more information, see the <a href="{@docRoot}guide/topics/search/index.html">Search</a>
     80  * documentation.
     81  * <p>
     82  *
     83  * @see android.view.MenuItem#SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW
     84  * @attr ref android.R.styleable#SearchView_iconifiedByDefault
     85  * @attr ref android.R.styleable#SearchView_imeOptions
     86  * @attr ref android.R.styleable#SearchView_inputType
     87  * @attr ref android.R.styleable#SearchView_maxWidth
     88  * @attr ref android.R.styleable#SearchView_queryHint
     89  */
     90 public class SearchView extends LinearLayout implements CollapsibleActionView {
     91 
     92     private static final boolean DBG = false;
     93     private static final String LOG_TAG = "SearchView";
     94 
     95     /**
     96      * Private constant for removing the microphone in the keyboard.
     97      */
     98     private static final String IME_OPTION_NO_MICROPHONE = "nm";
     99 
    100     private OnQueryTextListener mOnQueryChangeListener;
    101     private OnCloseListener mOnCloseListener;
    102     private OnFocusChangeListener mOnQueryTextFocusChangeListener;
    103     private OnSuggestionListener mOnSuggestionListener;
    104     private OnClickListener mOnSearchClickListener;
    105 
    106     private boolean mIconifiedByDefault;
    107     private boolean mIconified;
    108     private CursorAdapter mSuggestionsAdapter;
    109     private View mSearchButton;
    110     private View mSubmitButton;
    111     private View mSearchPlate;
    112     private View mSubmitArea;
    113     private ImageView mCloseButton;
    114     private View mSearchEditFrame;
    115     private View mVoiceButton;
    116     private SearchAutoComplete mQueryTextView;
    117     private View mDropDownAnchor;
    118     private ImageView mSearchHintIcon;
    119     private boolean mSubmitButtonEnabled;
    120     private CharSequence mQueryHint;
    121     private boolean mQueryRefinement;
    122     private boolean mClearingFocus;
    123     private int mMaxWidth;
    124     private boolean mVoiceButtonEnabled;
    125     private CharSequence mOldQueryText;
    126     private CharSequence mUserQuery;
    127     private boolean mExpandedInActionView;
    128     private int mCollapsedImeOptions;
    129 
    130     private SearchableInfo mSearchable;
    131     private Bundle mAppSearchData;
    132 
    133     /*
    134      * SearchView can be set expanded before the IME is ready to be shown during
    135      * initial UI setup. The show operation is asynchronous to account for this.
    136      */
    137     private Runnable mShowImeRunnable = new Runnable() {
    138         public void run() {
    139             InputMethodManager imm = (InputMethodManager)
    140                     getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
    141 
    142             if (imm != null) {
    143                 imm.showSoftInputUnchecked(0, null);
    144             }
    145         }
    146     };
    147 
    148     private Runnable mUpdateDrawableStateRunnable = new Runnable() {
    149         public void run() {
    150             updateFocusedState();
    151         }
    152     };
    153 
    154     // For voice searching
    155     private final Intent mVoiceWebSearchIntent;
    156     private final Intent mVoiceAppSearchIntent;
    157 
    158     // A weak map of drawables we've gotten from other packages, so we don't load them
    159     // more than once.
    160     private final WeakHashMap<String, Drawable.ConstantState> mOutsideDrawablesCache =
    161             new WeakHashMap<String, Drawable.ConstantState>();
    162 
    163     /**
    164      * Callbacks for changes to the query text.
    165      */
    166     public interface OnQueryTextListener {
    167 
    168         /**
    169          * Called when the user submits the query. This could be due to a key press on the
    170          * keyboard or due to pressing a submit button.
    171          * The listener can override the standard behavior by returning true
    172          * to indicate that it has handled the submit request. Otherwise return false to
    173          * let the SearchView handle the submission by launching any associated intent.
    174          *
    175          * @param query the query text that is to be submitted
    176          *
    177          * @return true if the query has been handled by the listener, false to let the
    178          * SearchView perform the default action.
    179          */
    180         boolean onQueryTextSubmit(String query);
    181 
    182         /**
    183          * Called when the query text is changed by the user.
    184          *
    185          * @param newText the new content of the query text field.
    186          *
    187          * @return false if the SearchView should perform the default action of showing any
    188          * suggestions if available, true if the action was handled by the listener.
    189          */
    190         boolean onQueryTextChange(String newText);
    191     }
    192 
    193     public interface OnCloseListener {
    194 
    195         /**
    196          * The user is attempting to close the SearchView.
    197          *
    198          * @return true if the listener wants to override the default behavior of clearing the
    199          * text field and dismissing it, false otherwise.
    200          */
    201         boolean onClose();
    202     }
    203 
    204     /**
    205      * Callback interface for selection events on suggestions. These callbacks
    206      * are only relevant when a SearchableInfo has been specified by {@link #setSearchableInfo}.
    207      */
    208     public interface OnSuggestionListener {
    209 
    210         /**
    211          * Called when a suggestion was selected by navigating to it.
    212          * @param position the absolute position in the list of suggestions.
    213          *
    214          * @return true if the listener handles the event and wants to override the default
    215          * behavior of possibly rewriting the query based on the selected item, false otherwise.
    216          */
    217         boolean onSuggestionSelect(int position);
    218 
    219         /**
    220          * Called when a suggestion was clicked.
    221          * @param position the absolute position of the clicked item in the list of suggestions.
    222          *
    223          * @return true if the listener handles the event and wants to override the default
    224          * behavior of launching any intent or submitting a search query specified on that item.
    225          * Return false otherwise.
    226          */
    227         boolean onSuggestionClick(int position);
    228     }
    229 
    230     public SearchView(Context context) {
    231         this(context, null);
    232     }
    233 
    234     public SearchView(Context context, AttributeSet attrs) {
    235         super(context, attrs);
    236 
    237         LayoutInflater inflater = (LayoutInflater) context
    238                 .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    239         inflater.inflate(R.layout.search_view, this, true);
    240 
    241         mSearchButton = findViewById(R.id.search_button);
    242         mQueryTextView = (SearchAutoComplete) findViewById(R.id.search_src_text);
    243         mQueryTextView.setSearchView(this);
    244 
    245         mSearchEditFrame = findViewById(R.id.search_edit_frame);
    246         mSearchPlate = findViewById(R.id.search_plate);
    247         mSubmitArea = findViewById(R.id.submit_area);
    248         mSubmitButton = findViewById(R.id.search_go_btn);
    249         mCloseButton = (ImageView) findViewById(R.id.search_close_btn);
    250         mVoiceButton = findViewById(R.id.search_voice_btn);
    251         mSearchHintIcon = (ImageView) findViewById(R.id.search_mag_icon);
    252 
    253         mSearchButton.setOnClickListener(mOnClickListener);
    254         mCloseButton.setOnClickListener(mOnClickListener);
    255         mSubmitButton.setOnClickListener(mOnClickListener);
    256         mVoiceButton.setOnClickListener(mOnClickListener);
    257         mQueryTextView.setOnClickListener(mOnClickListener);
    258 
    259         mQueryTextView.addTextChangedListener(mTextWatcher);
    260         mQueryTextView.setOnEditorActionListener(mOnEditorActionListener);
    261         mQueryTextView.setOnItemClickListener(mOnItemClickListener);
    262         mQueryTextView.setOnItemSelectedListener(mOnItemSelectedListener);
    263         mQueryTextView.setOnKeyListener(mTextKeyListener);
    264         // Inform any listener of focus changes
    265         mQueryTextView.setOnFocusChangeListener(new OnFocusChangeListener() {
    266 
    267             public void onFocusChange(View v, boolean hasFocus) {
    268                 if (mOnQueryTextFocusChangeListener != null) {
    269                     mOnQueryTextFocusChangeListener.onFocusChange(SearchView.this, hasFocus);
    270                 }
    271             }
    272         });
    273 
    274         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SearchView, 0, 0);
    275         setIconifiedByDefault(a.getBoolean(R.styleable.SearchView_iconifiedByDefault, true));
    276         int maxWidth = a.getDimensionPixelSize(R.styleable.SearchView_maxWidth, -1);
    277         if (maxWidth != -1) {
    278             setMaxWidth(maxWidth);
    279         }
    280         CharSequence queryHint = a.getText(R.styleable.SearchView_queryHint);
    281         if (!TextUtils.isEmpty(queryHint)) {
    282             setQueryHint(queryHint);
    283         }
    284         int imeOptions = a.getInt(R.styleable.SearchView_imeOptions, -1);
    285         if (imeOptions != -1) {
    286             setImeOptions(imeOptions);
    287         }
    288         int inputType = a.getInt(R.styleable.SearchView_inputType, -1);
    289         if (inputType != -1) {
    290             setInputType(inputType);
    291         }
    292 
    293         a.recycle();
    294 
    295         boolean focusable = true;
    296 
    297         a = context.obtainStyledAttributes(attrs, R.styleable.View, 0, 0);
    298         focusable = a.getBoolean(R.styleable.View_focusable, focusable);
    299         a.recycle();
    300         setFocusable(focusable);
    301 
    302         // Save voice intent for later queries/launching
    303         mVoiceWebSearchIntent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH);
    304         mVoiceWebSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    305         mVoiceWebSearchIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
    306                 RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH);
    307 
    308         mVoiceAppSearchIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
    309         mVoiceAppSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    310 
    311         mDropDownAnchor = findViewById(mQueryTextView.getDropDownAnchor());
    312         if (mDropDownAnchor != null) {
    313             mDropDownAnchor.addOnLayoutChangeListener(new OnLayoutChangeListener() {
    314                 @Override
    315                 public void onLayoutChange(View v, int left, int top, int right, int bottom,
    316                         int oldLeft, int oldTop, int oldRight, int oldBottom) {
    317                     adjustDropDownSizeAndPosition();
    318                 }
    319 
    320             });
    321         }
    322 
    323         updateViewsVisibility(mIconifiedByDefault);
    324         updateQueryHint();
    325     }
    326 
    327     /**
    328      * Sets the SearchableInfo for this SearchView. Properties in the SearchableInfo are used
    329      * to display labels, hints, suggestions, create intents for launching search results screens
    330      * and controlling other affordances such as a voice button.
    331      *
    332      * @param searchable a SearchableInfo can be retrieved from the SearchManager, for a specific
    333      * activity or a global search provider.
    334      */
    335     public void setSearchableInfo(SearchableInfo searchable) {
    336         mSearchable = searchable;
    337         if (mSearchable != null) {
    338             updateSearchAutoComplete();
    339             updateQueryHint();
    340         }
    341         // Cache the voice search capability
    342         mVoiceButtonEnabled = hasVoiceSearch();
    343 
    344         if (mVoiceButtonEnabled) {
    345             // Disable the microphone on the keyboard, as a mic is displayed near the text box
    346             // TODO: use imeOptions to disable voice input when the new API will be available
    347             mQueryTextView.setPrivateImeOptions(IME_OPTION_NO_MICROPHONE);
    348         }
    349         updateViewsVisibility(isIconified());
    350     }
    351 
    352     /**
    353      * Sets the APP_DATA for legacy SearchDialog use.
    354      * @param appSearchData bundle provided by the app when launching the search dialog
    355      * @hide
    356      */
    357     public void setAppSearchData(Bundle appSearchData) {
    358         mAppSearchData = appSearchData;
    359     }
    360 
    361     /**
    362      * Sets the IME options on the query text field.
    363      *
    364      * @see TextView#setImeOptions(int)
    365      * @param imeOptions the options to set on the query text field
    366      *
    367      * @attr ref android.R.styleable#SearchView_imeOptions
    368      */
    369     public void setImeOptions(int imeOptions) {
    370         mQueryTextView.setImeOptions(imeOptions);
    371     }
    372 
    373     /**
    374      * Sets the input type on the query text field.
    375      *
    376      * @see TextView#setInputType(int)
    377      * @param inputType the input type to set on the query text field
    378      *
    379      * @attr ref android.R.styleable#SearchView_inputType
    380      */
    381     public void setInputType(int inputType) {
    382         mQueryTextView.setInputType(inputType);
    383     }
    384 
    385     /** @hide */
    386     @Override
    387     public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
    388         // Don't accept focus if in the middle of clearing focus
    389         if (mClearingFocus) return false;
    390         // Check if SearchView is focusable.
    391         if (!isFocusable()) return false;
    392         // If it is not iconified, then give the focus to the text field
    393         if (!isIconified()) {
    394             boolean result = mQueryTextView.requestFocus(direction, previouslyFocusedRect);
    395             if (result) {
    396                 updateViewsVisibility(false);
    397             }
    398             return result;
    399         } else {
    400             return super.requestFocus(direction, previouslyFocusedRect);
    401         }
    402     }
    403 
    404     /** @hide */
    405     @Override
    406     public void clearFocus() {
    407         mClearingFocus = true;
    408         setImeVisibility(false);
    409         super.clearFocus();
    410         mQueryTextView.clearFocus();
    411         mClearingFocus = false;
    412     }
    413 
    414     /**
    415      * Sets a listener for user actions within the SearchView.
    416      *
    417      * @param listener the listener object that receives callbacks when the user performs
    418      * actions in the SearchView such as clicking on buttons or typing a query.
    419      */
    420     public void setOnQueryTextListener(OnQueryTextListener listener) {
    421         mOnQueryChangeListener = listener;
    422     }
    423 
    424     /**
    425      * Sets a listener to inform when the user closes the SearchView.
    426      *
    427      * @param listener the listener to call when the user closes the SearchView.
    428      */
    429     public void setOnCloseListener(OnCloseListener listener) {
    430         mOnCloseListener = listener;
    431     }
    432 
    433     /**
    434      * Sets a listener to inform when the focus of the query text field changes.
    435      *
    436      * @param listener the listener to inform of focus changes.
    437      */
    438     public void setOnQueryTextFocusChangeListener(OnFocusChangeListener listener) {
    439         mOnQueryTextFocusChangeListener = listener;
    440     }
    441 
    442     /**
    443      * Sets a listener to inform when a suggestion is focused or clicked.
    444      *
    445      * @param listener the listener to inform of suggestion selection events.
    446      */
    447     public void setOnSuggestionListener(OnSuggestionListener listener) {
    448         mOnSuggestionListener = listener;
    449     }
    450 
    451     /**
    452      * Sets a listener to inform when the search button is pressed. This is only
    453      * relevant when the text field is not visible by default. Calling {@link #setIconified
    454      * setIconified(false)} can also cause this listener to be informed.
    455      *
    456      * @param listener the listener to inform when the search button is clicked or
    457      * the text field is programmatically de-iconified.
    458      */
    459     public void setOnSearchClickListener(OnClickListener listener) {
    460         mOnSearchClickListener = listener;
    461     }
    462 
    463     /**
    464      * Returns the query string currently in the text field.
    465      *
    466      * @return the query string
    467      */
    468     public CharSequence getQuery() {
    469         return mQueryTextView.getText();
    470     }
    471 
    472     /**
    473      * Sets a query string in the text field and optionally submits the query as well.
    474      *
    475      * @param query the query string. This replaces any query text already present in the
    476      * text field.
    477      * @param submit whether to submit the query right now or only update the contents of
    478      * text field.
    479      */
    480     public void setQuery(CharSequence query, boolean submit) {
    481         mQueryTextView.setText(query);
    482         if (query != null) {
    483             mQueryTextView.setSelection(query.length());
    484             mUserQuery = query;
    485         }
    486 
    487         // If the query is not empty and submit is requested, submit the query
    488         if (submit && !TextUtils.isEmpty(query)) {
    489             onSubmitQuery();
    490         }
    491     }
    492 
    493     /**
    494      * Sets the hint text to display in the query text field. This overrides any hint specified
    495      * in the SearchableInfo.
    496      *
    497      * @param hint the hint text to display
    498      *
    499      * @attr ref android.R.styleable#SearchView_queryHint
    500      */
    501     public void setQueryHint(CharSequence hint) {
    502         mQueryHint = hint;
    503         updateQueryHint();
    504     }
    505 
    506     /**
    507      * Sets the default or resting state of the search field. If true, a single search icon is
    508      * shown by default and expands to show the text field and other buttons when pressed. Also,
    509      * if the default state is iconified, then it collapses to that state when the close button
    510      * is pressed. Changes to this property will take effect immediately.
    511      *
    512      * <p>The default value is true.</p>
    513      *
    514      * @param iconified whether the search field should be iconified by default
    515      *
    516      * @attr ref android.R.styleable#SearchView_iconifiedByDefault
    517      */
    518     public void setIconifiedByDefault(boolean iconified) {
    519         if (mIconifiedByDefault == iconified) return;
    520         mIconifiedByDefault = iconified;
    521         updateViewsVisibility(iconified);
    522         updateQueryHint();
    523     }
    524 
    525     /**
    526      * Returns the default iconified state of the search field.
    527      * @return
    528      */
    529     public boolean isIconfiedByDefault() {
    530         return mIconifiedByDefault;
    531     }
    532 
    533     /**
    534      * Iconifies or expands the SearchView. Any query text is cleared when iconified. This is
    535      * a temporary state and does not override the default iconified state set by
    536      * {@link #setIconifiedByDefault(boolean)}. If the default state is iconified, then
    537      * a false here will only be valid until the user closes the field. And if the default
    538      * state is expanded, then a true here will only clear the text field and not close it.
    539      *
    540      * @param iconify a true value will collapse the SearchView to an icon, while a false will
    541      * expand it.
    542      */
    543     public void setIconified(boolean iconify) {
    544         if (iconify) {
    545             onCloseClicked();
    546         } else {
    547             onSearchClicked();
    548         }
    549     }
    550 
    551     /**
    552      * Returns the current iconified state of the SearchView.
    553      *
    554      * @return true if the SearchView is currently iconified, false if the search field is
    555      * fully visible.
    556      */
    557     public boolean isIconified() {
    558         return mIconified;
    559     }
    560 
    561     /**
    562      * Enables showing a submit button when the query is non-empty. In cases where the SearchView
    563      * is being used to filter the contents of the current activity and doesn't launch a separate
    564      * results activity, then the submit button should be disabled.
    565      *
    566      * @param enabled true to show a submit button for submitting queries, false if a submit
    567      * button is not required.
    568      */
    569     public void setSubmitButtonEnabled(boolean enabled) {
    570         mSubmitButtonEnabled = enabled;
    571         updateViewsVisibility(isIconified());
    572     }
    573 
    574     /**
    575      * Returns whether the submit button is enabled when necessary or never displayed.
    576      *
    577      * @return whether the submit button is enabled automatically when necessary
    578      */
    579     public boolean isSubmitButtonEnabled() {
    580         return mSubmitButtonEnabled;
    581     }
    582 
    583     /**
    584      * Specifies if a query refinement button should be displayed alongside each suggestion
    585      * or if it should depend on the flags set in the individual items retrieved from the
    586      * suggestions provider. Clicking on the query refinement button will replace the text
    587      * in the query text field with the text from the suggestion. This flag only takes effect
    588      * if a SearchableInfo has been specified with {@link #setSearchableInfo(SearchableInfo)}
    589      * and not when using a custom adapter.
    590      *
    591      * @param enable true if all items should have a query refinement button, false if only
    592      * those items that have a query refinement flag set should have the button.
    593      *
    594      * @see SearchManager#SUGGEST_COLUMN_FLAGS
    595      * @see SearchManager#FLAG_QUERY_REFINEMENT
    596      */
    597     public void setQueryRefinementEnabled(boolean enable) {
    598         mQueryRefinement = enable;
    599         if (mSuggestionsAdapter instanceof SuggestionsAdapter) {
    600             ((SuggestionsAdapter) mSuggestionsAdapter).setQueryRefinement(
    601                     enable ? SuggestionsAdapter.REFINE_ALL : SuggestionsAdapter.REFINE_BY_ENTRY);
    602         }
    603     }
    604 
    605     /**
    606      * Returns whether query refinement is enabled for all items or only specific ones.
    607      * @return true if enabled for all items, false otherwise.
    608      */
    609     public boolean isQueryRefinementEnabled() {
    610         return mQueryRefinement;
    611     }
    612 
    613     /**
    614      * You can set a custom adapter if you wish. Otherwise the default adapter is used to
    615      * display the suggestions from the suggestions provider associated with the SearchableInfo.
    616      *
    617      * @see #setSearchableInfo(SearchableInfo)
    618      */
    619     public void setSuggestionsAdapter(CursorAdapter adapter) {
    620         mSuggestionsAdapter = adapter;
    621 
    622         mQueryTextView.setAdapter(mSuggestionsAdapter);
    623     }
    624 
    625     /**
    626      * Returns the adapter used for suggestions, if any.
    627      * @return the suggestions adapter
    628      */
    629     public CursorAdapter getSuggestionsAdapter() {
    630         return mSuggestionsAdapter;
    631     }
    632 
    633     /**
    634      * Makes the view at most this many pixels wide
    635      *
    636      * @attr ref android.R.styleable#SearchView_maxWidth
    637      */
    638     public void setMaxWidth(int maxpixels) {
    639         mMaxWidth = maxpixels;
    640 
    641         requestLayout();
    642     }
    643 
    644     @Override
    645     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    646         // Let the standard measurements take effect in iconified state.
    647         if (isIconified()) {
    648             super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    649             return;
    650         }
    651 
    652         int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    653         int width = MeasureSpec.getSize(widthMeasureSpec);
    654 
    655         switch (widthMode) {
    656         case MeasureSpec.AT_MOST:
    657             // If there is an upper limit, don't exceed maximum width (explicit or implicit)
    658             if (mMaxWidth > 0) {
    659                 width = Math.min(mMaxWidth, width);
    660             } else {
    661                 width = Math.min(getPreferredWidth(), width);
    662             }
    663             break;
    664         case MeasureSpec.EXACTLY:
    665             // If an exact width is specified, still don't exceed any specified maximum width
    666             if (mMaxWidth > 0) {
    667                 width = Math.min(mMaxWidth, width);
    668             }
    669             break;
    670         case MeasureSpec.UNSPECIFIED:
    671             // Use maximum width, if specified, else preferred width
    672             width = mMaxWidth > 0 ? mMaxWidth : getPreferredWidth();
    673             break;
    674         }
    675         widthMode = MeasureSpec.EXACTLY;
    676         super.onMeasure(MeasureSpec.makeMeasureSpec(width, widthMode), heightMeasureSpec);
    677     }
    678 
    679     private int getPreferredWidth() {
    680         return getContext().getResources()
    681                 .getDimensionPixelSize(R.dimen.search_view_preferred_width);
    682     }
    683 
    684     private void updateViewsVisibility(final boolean collapsed) {
    685         mIconified = collapsed;
    686         // Visibility of views that are visible when collapsed
    687         final int visCollapsed = collapsed ? VISIBLE : GONE;
    688         // Is there text in the query
    689         final boolean hasText = !TextUtils.isEmpty(mQueryTextView.getText());
    690 
    691         mSearchButton.setVisibility(visCollapsed);
    692         updateSubmitButton(hasText);
    693         mSearchEditFrame.setVisibility(collapsed ? GONE : VISIBLE);
    694         mSearchHintIcon.setVisibility(mIconifiedByDefault ? GONE : VISIBLE);
    695         updateCloseButton();
    696         updateVoiceButton(!hasText);
    697         updateSubmitArea();
    698     }
    699 
    700     private boolean hasVoiceSearch() {
    701         if (mSearchable != null && mSearchable.getVoiceSearchEnabled()) {
    702             Intent testIntent = null;
    703             if (mSearchable.getVoiceSearchLaunchWebSearch()) {
    704                 testIntent = mVoiceWebSearchIntent;
    705             } else if (mSearchable.getVoiceSearchLaunchRecognizer()) {
    706                 testIntent = mVoiceAppSearchIntent;
    707             }
    708             if (testIntent != null) {
    709                 ResolveInfo ri = getContext().getPackageManager().resolveActivity(testIntent,
    710                         PackageManager.MATCH_DEFAULT_ONLY);
    711                 return ri != null;
    712             }
    713         }
    714         return false;
    715     }
    716 
    717     private boolean isSubmitAreaEnabled() {
    718         return (mSubmitButtonEnabled || mVoiceButtonEnabled) && !isIconified();
    719     }
    720 
    721     private void updateSubmitButton(boolean hasText) {
    722         int visibility = GONE;
    723         if (isSubmitAreaEnabled() && hasFocus() && (hasText || !mVoiceButtonEnabled)) {
    724             visibility = VISIBLE;
    725         }
    726         mSubmitButton.setVisibility(visibility);
    727     }
    728 
    729     private void updateSubmitArea() {
    730         int visibility = GONE;
    731         if (isSubmitAreaEnabled()
    732                 && (mSubmitButton.getVisibility() == VISIBLE
    733                         || mVoiceButton.getVisibility() == VISIBLE)) {
    734             visibility = VISIBLE;
    735         }
    736         mSubmitArea.setVisibility(visibility);
    737     }
    738 
    739     private void updateCloseButton() {
    740         final boolean hasText = !TextUtils.isEmpty(mQueryTextView.getText());
    741         // Should we show the close button? It is not shown if there's no focus,
    742         // field is not iconified by default and there is no text in it.
    743         final boolean showClose = hasText || (mIconifiedByDefault && !mExpandedInActionView);
    744         mCloseButton.setVisibility(showClose ? VISIBLE : GONE);
    745         mCloseButton.getDrawable().setState(hasText ? ENABLED_STATE_SET : EMPTY_STATE_SET);
    746     }
    747 
    748     private void postUpdateFocusedState() {
    749         post(mUpdateDrawableStateRunnable);
    750     }
    751 
    752     private void updateFocusedState() {
    753         boolean focused = mQueryTextView.hasFocus();
    754         mSearchPlate.getBackground().setState(focused ? FOCUSED_STATE_SET : EMPTY_STATE_SET);
    755         mSubmitArea.getBackground().setState(focused ? FOCUSED_STATE_SET : EMPTY_STATE_SET);
    756         invalidate();
    757     }
    758 
    759     @Override
    760     protected void onDetachedFromWindow() {
    761         removeCallbacks(mUpdateDrawableStateRunnable);
    762         super.onDetachedFromWindow();
    763     }
    764 
    765     private void setImeVisibility(final boolean visible) {
    766         if (visible) {
    767             post(mShowImeRunnable);
    768         } else {
    769             removeCallbacks(mShowImeRunnable);
    770             InputMethodManager imm = (InputMethodManager)
    771                     getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
    772 
    773             if (imm != null) {
    774                 imm.hideSoftInputFromWindow(getWindowToken(), 0);
    775             }
    776         }
    777     }
    778 
    779     /**
    780      * Called by the SuggestionsAdapter
    781      * @hide
    782      */
    783     /* package */void onQueryRefine(CharSequence queryText) {
    784         setQuery(queryText);
    785     }
    786 
    787     private final OnClickListener mOnClickListener = new OnClickListener() {
    788 
    789         public void onClick(View v) {
    790             if (v == mSearchButton) {
    791                 onSearchClicked();
    792             } else if (v == mCloseButton) {
    793                 onCloseClicked();
    794             } else if (v == mSubmitButton) {
    795                 onSubmitQuery();
    796             } else if (v == mVoiceButton) {
    797                 onVoiceClicked();
    798             } else if (v == mQueryTextView) {
    799                 forceSuggestionQuery();
    800             }
    801         }
    802     };
    803 
    804     /**
    805      * Handles the key down event for dealing with action keys.
    806      *
    807      * @param keyCode This is the keycode of the typed key, and is the same value as
    808      *        found in the KeyEvent parameter.
    809      * @param event The complete event record for the typed key
    810      *
    811      * @return true if the event was handled here, or false if not.
    812      */
    813     @Override
    814     public boolean onKeyDown(int keyCode, KeyEvent event) {
    815         if (mSearchable == null) {
    816             return false;
    817         }
    818 
    819         // if it's an action specified by the searchable activity, launch the
    820         // entered query with the action key
    821         SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
    822         if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) {
    823             launchQuerySearch(keyCode, actionKey.getQueryActionMsg(), mQueryTextView.getText()
    824                     .toString());
    825             return true;
    826         }
    827 
    828         return super.onKeyDown(keyCode, event);
    829     }
    830 
    831     /**
    832      * React to the user typing "enter" or other hardwired keys while typing in
    833      * the search box. This handles these special keys while the edit box has
    834      * focus.
    835      */
    836     View.OnKeyListener mTextKeyListener = new View.OnKeyListener() {
    837         public boolean onKey(View v, int keyCode, KeyEvent event) {
    838             // guard against possible race conditions
    839             if (mSearchable == null) {
    840                 return false;
    841             }
    842 
    843             if (DBG) {
    844                 Log.d(LOG_TAG, "mTextListener.onKey(" + keyCode + "," + event + "), selection: "
    845                         + mQueryTextView.getListSelection());
    846             }
    847 
    848             // If a suggestion is selected, handle enter, search key, and action keys
    849             // as presses on the selected suggestion
    850             if (mQueryTextView.isPopupShowing()
    851                     && mQueryTextView.getListSelection() != ListView.INVALID_POSITION) {
    852                 return onSuggestionsKey(v, keyCode, event);
    853             }
    854 
    855             // If there is text in the query box, handle enter, and action keys
    856             // The search key is handled by the dialog's onKeyDown().
    857             if (!mQueryTextView.isEmpty() && event.hasNoModifiers()) {
    858                 if (event.getAction() == KeyEvent.ACTION_UP) {
    859                     if (keyCode == KeyEvent.KEYCODE_ENTER) {
    860                         v.cancelLongPress();
    861 
    862                         // Launch as a regular search.
    863                         launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null, mQueryTextView.getText()
    864                                 .toString());
    865                         return true;
    866                     }
    867                 }
    868                 if (event.getAction() == KeyEvent.ACTION_DOWN) {
    869                     SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
    870                     if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) {
    871                         launchQuerySearch(keyCode, actionKey.getQueryActionMsg(), mQueryTextView
    872                                 .getText().toString());
    873                         return true;
    874                     }
    875                 }
    876             }
    877             return false;
    878         }
    879     };
    880 
    881     /**
    882      * React to the user typing while in the suggestions list. First, check for
    883      * action keys. If not handled, try refocusing regular characters into the
    884      * EditText.
    885      */
    886     private boolean onSuggestionsKey(View v, int keyCode, KeyEvent event) {
    887         // guard against possible race conditions (late arrival after dismiss)
    888         if (mSearchable == null) {
    889             return false;
    890         }
    891         if (mSuggestionsAdapter == null) {
    892             return false;
    893         }
    894         if (event.getAction() == KeyEvent.ACTION_DOWN && event.hasNoModifiers()) {
    895             // First, check for enter or search (both of which we'll treat as a
    896             // "click")
    897             if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_SEARCH
    898                     || keyCode == KeyEvent.KEYCODE_TAB) {
    899                 int position = mQueryTextView.getListSelection();
    900                 return onItemClicked(position, KeyEvent.KEYCODE_UNKNOWN, null);
    901             }
    902 
    903             // Next, check for left/right moves, which we use to "return" the
    904             // user to the edit view
    905             if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
    906                 // give "focus" to text editor, with cursor at the beginning if
    907                 // left key, at end if right key
    908                 // TODO: Reverse left/right for right-to-left languages, e.g.
    909                 // Arabic
    910                 int selPoint = (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) ? 0 : mQueryTextView
    911                         .length();
    912                 mQueryTextView.setSelection(selPoint);
    913                 mQueryTextView.setListSelection(0);
    914                 mQueryTextView.clearListSelection();
    915                 mQueryTextView.ensureImeVisible(true);
    916 
    917                 return true;
    918             }
    919 
    920             // Next, check for an "up and out" move
    921             if (keyCode == KeyEvent.KEYCODE_DPAD_UP && 0 == mQueryTextView.getListSelection()) {
    922                 // TODO: restoreUserQuery();
    923                 // let ACTV complete the move
    924                 return false;
    925             }
    926 
    927             // Next, check for an "action key"
    928             SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
    929             if ((actionKey != null)
    930                     && ((actionKey.getSuggestActionMsg() != null) || (actionKey
    931                             .getSuggestActionMsgColumn() != null))) {
    932                 // launch suggestion using action key column
    933                 int position = mQueryTextView.getListSelection();
    934                 if (position != ListView.INVALID_POSITION) {
    935                     Cursor c = mSuggestionsAdapter.getCursor();
    936                     if (c.moveToPosition(position)) {
    937                         final String actionMsg = getActionKeyMessage(c, actionKey);
    938                         if (actionMsg != null && (actionMsg.length() > 0)) {
    939                             return onItemClicked(position, keyCode, actionMsg);
    940                         }
    941                     }
    942                 }
    943             }
    944         }
    945         return false;
    946     }
    947 
    948     /**
    949      * For a given suggestion and a given cursor row, get the action message. If
    950      * not provided by the specific row/column, also check for a single
    951      * definition (for the action key).
    952      *
    953      * @param c The cursor providing suggestions
    954      * @param actionKey The actionkey record being examined
    955      *
    956      * @return Returns a string, or null if no action key message for this
    957      *         suggestion
    958      */
    959     private static String getActionKeyMessage(Cursor c, SearchableInfo.ActionKeyInfo actionKey) {
    960         String result = null;
    961         // check first in the cursor data, for a suggestion-specific message
    962         final String column = actionKey.getSuggestActionMsgColumn();
    963         if (column != null) {
    964             result = SuggestionsAdapter.getColumnString(c, column);
    965         }
    966         // If the cursor didn't give us a message, see if there's a single
    967         // message defined
    968         // for the actionkey (for all suggestions)
    969         if (result == null) {
    970             result = actionKey.getSuggestActionMsg();
    971         }
    972         return result;
    973     }
    974 
    975     private int getSearchIconId() {
    976         TypedValue outValue = new TypedValue();
    977         getContext().getTheme().resolveAttribute(com.android.internal.R.attr.searchViewSearchIcon,
    978                 outValue, true);
    979         return outValue.resourceId;
    980     }
    981 
    982     private CharSequence getDecoratedHint(CharSequence hintText) {
    983         // If the field is always expanded, then don't add the search icon to the hint
    984         if (!mIconifiedByDefault) return hintText;
    985 
    986         SpannableStringBuilder ssb = new SpannableStringBuilder("   "); // for the icon
    987         ssb.append(hintText);
    988         Drawable searchIcon = getContext().getResources().getDrawable(getSearchIconId());
    989         int textSize = (int) (mQueryTextView.getTextSize() * 1.25);
    990         searchIcon.setBounds(0, 0, textSize, textSize);
    991         ssb.setSpan(new ImageSpan(searchIcon), 1, 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    992         return ssb;
    993     }
    994 
    995     private void updateQueryHint() {
    996         if (mQueryHint != null) {
    997             mQueryTextView.setHint(getDecoratedHint(mQueryHint));
    998         } else if (mSearchable != null) {
    999             CharSequence hint = null;
   1000             int hintId = mSearchable.getHintId();
   1001             if (hintId != 0) {
   1002                 hint = getContext().getString(hintId);
   1003             }
   1004             if (hint != null) {
   1005                 mQueryTextView.setHint(getDecoratedHint(hint));
   1006             }
   1007         } else {
   1008             mQueryTextView.setHint(getDecoratedHint(""));
   1009         }
   1010     }
   1011 
   1012     /**
   1013      * Updates the auto-complete text view.
   1014      */
   1015     private void updateSearchAutoComplete() {
   1016         mQueryTextView.setDropDownAnimationStyle(0); // no animation
   1017         mQueryTextView.setThreshold(mSearchable.getSuggestThreshold());
   1018         mQueryTextView.setImeOptions(mSearchable.getImeOptions());
   1019         int inputType = mSearchable.getInputType();
   1020         // We only touch this if the input type is set up for text (which it almost certainly
   1021         // should be, in the case of search!)
   1022         if ((inputType & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_TEXT) {
   1023             // The existence of a suggestions authority is the proxy for "suggestions
   1024             // are available here"
   1025             inputType &= ~InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;
   1026             if (mSearchable.getSuggestAuthority() != null) {
   1027                 inputType |= InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;
   1028             }
   1029         }
   1030         mQueryTextView.setInputType(inputType);
   1031 
   1032         // attach the suggestions adapter, if suggestions are available
   1033         // The existence of a suggestions authority is the proxy for "suggestions available here"
   1034         if (mSearchable.getSuggestAuthority() != null) {
   1035             mSuggestionsAdapter = new SuggestionsAdapter(getContext(),
   1036                     this, mSearchable, mOutsideDrawablesCache);
   1037             mQueryTextView.setAdapter(mSuggestionsAdapter);
   1038             ((SuggestionsAdapter) mSuggestionsAdapter).setQueryRefinement(
   1039                     mQueryRefinement ? SuggestionsAdapter.REFINE_ALL
   1040                     : SuggestionsAdapter.REFINE_BY_ENTRY);
   1041         }
   1042     }
   1043 
   1044     /**
   1045      * Update the visibility of the voice button.  There are actually two voice search modes,
   1046      * either of which will activate the button.
   1047      * @param empty whether the search query text field is empty. If it is, then the other
   1048      * criteria apply to make the voice button visible.
   1049      */
   1050     private void updateVoiceButton(boolean empty) {
   1051         int visibility = GONE;
   1052         if (mVoiceButtonEnabled && !isIconified() && empty) {
   1053             visibility = VISIBLE;
   1054             mSubmitButton.setVisibility(GONE);
   1055         }
   1056         mVoiceButton.setVisibility(visibility);
   1057     }
   1058 
   1059     private final OnEditorActionListener mOnEditorActionListener = new OnEditorActionListener() {
   1060 
   1061         /**
   1062          * Called when the input method default action key is pressed.
   1063          */
   1064         public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
   1065             onSubmitQuery();
   1066             return true;
   1067         }
   1068     };
   1069 
   1070     private void onTextChanged(CharSequence newText) {
   1071         CharSequence text = mQueryTextView.getText();
   1072         mUserQuery = text;
   1073         boolean hasText = !TextUtils.isEmpty(text);
   1074         if (isSubmitButtonEnabled()) {
   1075             updateSubmitButton(hasText);
   1076         }
   1077         updateVoiceButton(!hasText);
   1078         updateCloseButton();
   1079         updateSubmitArea();
   1080         if (mOnQueryChangeListener != null && !TextUtils.equals(newText, mOldQueryText)) {
   1081             mOnQueryChangeListener.onQueryTextChange(newText.toString());
   1082         }
   1083         mOldQueryText = newText.toString();
   1084     }
   1085 
   1086     private void onSubmitQuery() {
   1087         CharSequence query = mQueryTextView.getText();
   1088         if (query != null && TextUtils.getTrimmedLength(query) > 0) {
   1089             if (mOnQueryChangeListener == null
   1090                     || !mOnQueryChangeListener.onQueryTextSubmit(query.toString())) {
   1091                 if (mSearchable != null) {
   1092                     launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null, query.toString());
   1093                     setImeVisibility(false);
   1094                 }
   1095                 dismissSuggestions();
   1096             }
   1097         }
   1098     }
   1099 
   1100     private void dismissSuggestions() {
   1101         mQueryTextView.dismissDropDown();
   1102     }
   1103 
   1104     private void onCloseClicked() {
   1105         CharSequence text = mQueryTextView.getText();
   1106         if (TextUtils.isEmpty(text)) {
   1107             if (mIconifiedByDefault) {
   1108                 // If the app doesn't override the close behavior
   1109                 if (mOnCloseListener == null || !mOnCloseListener.onClose()) {
   1110                     // hide the keyboard and remove focus
   1111                     clearFocus();
   1112                     // collapse the search field
   1113                     updateViewsVisibility(true);
   1114                 }
   1115             }
   1116         } else {
   1117             mQueryTextView.setText("");
   1118             mQueryTextView.requestFocus();
   1119             setImeVisibility(true);
   1120         }
   1121 
   1122     }
   1123 
   1124     private void onSearchClicked() {
   1125         updateViewsVisibility(false);
   1126         mQueryTextView.requestFocus();
   1127         setImeVisibility(true);
   1128         if (mOnSearchClickListener != null) {
   1129             mOnSearchClickListener.onClick(this);
   1130         }
   1131     }
   1132 
   1133     private void onVoiceClicked() {
   1134         // guard against possible race conditions
   1135         if (mSearchable == null) {
   1136             return;
   1137         }
   1138         SearchableInfo searchable = mSearchable;
   1139         try {
   1140             if (searchable.getVoiceSearchLaunchWebSearch()) {
   1141                 Intent webSearchIntent = createVoiceWebSearchIntent(mVoiceWebSearchIntent,
   1142                         searchable);
   1143                 getContext().startActivity(webSearchIntent);
   1144             } else if (searchable.getVoiceSearchLaunchRecognizer()) {
   1145                 Intent appSearchIntent = createVoiceAppSearchIntent(mVoiceAppSearchIntent,
   1146                         searchable);
   1147                 getContext().startActivity(appSearchIntent);
   1148             }
   1149         } catch (ActivityNotFoundException e) {
   1150             // Should not happen, since we check the availability of
   1151             // voice search before showing the button. But just in case...
   1152             Log.w(LOG_TAG, "Could not find voice search activity");
   1153         }
   1154     }
   1155 
   1156     void onTextFocusChanged() {
   1157         updateViewsVisibility(isIconified());
   1158         // Delayed update to make sure that the focus has settled down and window focus changes
   1159         // don't affect it. A synchronous update was not working.
   1160         postUpdateFocusedState();
   1161         if (mQueryTextView.hasFocus()) {
   1162             forceSuggestionQuery();
   1163         }
   1164     }
   1165 
   1166     @Override
   1167     public void onWindowFocusChanged(boolean hasWindowFocus) {
   1168         super.onWindowFocusChanged(hasWindowFocus);
   1169 
   1170         postUpdateFocusedState();
   1171     }
   1172 
   1173     /**
   1174      * {@inheritDoc}
   1175      */
   1176     @Override
   1177     public void onActionViewCollapsed() {
   1178         clearFocus();
   1179         updateViewsVisibility(true);
   1180         mQueryTextView.setText("");
   1181         mQueryTextView.setImeOptions(mCollapsedImeOptions);
   1182         mExpandedInActionView = false;
   1183     }
   1184 
   1185     /**
   1186      * {@inheritDoc}
   1187      */
   1188     @Override
   1189     public void onActionViewExpanded() {
   1190         mExpandedInActionView = true;
   1191         mCollapsedImeOptions = mQueryTextView.getImeOptions();
   1192         mQueryTextView.setImeOptions(mCollapsedImeOptions | EditorInfo.IME_FLAG_NO_FULLSCREEN);
   1193         setIconified(false);
   1194     }
   1195 
   1196     private void adjustDropDownSizeAndPosition() {
   1197         if (mDropDownAnchor.getWidth() > 1) {
   1198             Resources res = getContext().getResources();
   1199             int anchorPadding = mSearchPlate.getPaddingLeft();
   1200             Rect dropDownPadding = new Rect();
   1201             int iconOffset = mIconifiedByDefault
   1202                     ? res.getDimensionPixelSize(R.dimen.dropdownitem_icon_width)
   1203                     + res.getDimensionPixelSize(R.dimen.dropdownitem_text_padding_left)
   1204                     : 0;
   1205             mQueryTextView.getDropDownBackground().getPadding(dropDownPadding);
   1206             mQueryTextView.setDropDownHorizontalOffset(-(dropDownPadding.left + iconOffset)
   1207                     + anchorPadding);
   1208             mQueryTextView.setDropDownWidth(mDropDownAnchor.getWidth() + dropDownPadding.left
   1209                     + dropDownPadding.right + iconOffset - (anchorPadding));
   1210         }
   1211     }
   1212 
   1213     private boolean onItemClicked(int position, int actionKey, String actionMsg) {
   1214         if (mOnSuggestionListener == null
   1215                 || !mOnSuggestionListener.onSuggestionClick(position)) {
   1216             launchSuggestion(position, KeyEvent.KEYCODE_UNKNOWN, null);
   1217             setImeVisibility(false);
   1218             dismissSuggestions();
   1219             return true;
   1220         }
   1221         return false;
   1222     }
   1223 
   1224     private boolean onItemSelected(int position) {
   1225         if (mOnSuggestionListener == null
   1226                 || !mOnSuggestionListener.onSuggestionSelect(position)) {
   1227             rewriteQueryFromSuggestion(position);
   1228             return true;
   1229         }
   1230         return false;
   1231     }
   1232 
   1233     private final OnItemClickListener mOnItemClickListener = new OnItemClickListener() {
   1234 
   1235         /**
   1236          * Implements OnItemClickListener
   1237          */
   1238         public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
   1239             if (DBG) Log.d(LOG_TAG, "onItemClick() position " + position);
   1240             onItemClicked(position, KeyEvent.KEYCODE_UNKNOWN, null);
   1241         }
   1242     };
   1243 
   1244     private final OnItemSelectedListener mOnItemSelectedListener = new OnItemSelectedListener() {
   1245 
   1246         /**
   1247          * Implements OnItemSelectedListener
   1248          */
   1249         public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
   1250             if (DBG) Log.d(LOG_TAG, "onItemSelected() position " + position);
   1251             SearchView.this.onItemSelected(position);
   1252         }
   1253 
   1254         /**
   1255          * Implements OnItemSelectedListener
   1256          */
   1257         public void onNothingSelected(AdapterView<?> parent) {
   1258             if (DBG)
   1259                 Log.d(LOG_TAG, "onNothingSelected()");
   1260         }
   1261     };
   1262 
   1263     /**
   1264      * Query rewriting.
   1265      */
   1266     private void rewriteQueryFromSuggestion(int position) {
   1267         CharSequence oldQuery = mQueryTextView.getText();
   1268         Cursor c = mSuggestionsAdapter.getCursor();
   1269         if (c == null) {
   1270             return;
   1271         }
   1272         if (c.moveToPosition(position)) {
   1273             // Get the new query from the suggestion.
   1274             CharSequence newQuery = mSuggestionsAdapter.convertToString(c);
   1275             if (newQuery != null) {
   1276                 // The suggestion rewrites the query.
   1277                 // Update the text field, without getting new suggestions.
   1278                 setQuery(newQuery);
   1279             } else {
   1280                 // The suggestion does not rewrite the query, restore the user's query.
   1281                 setQuery(oldQuery);
   1282             }
   1283         } else {
   1284             // We got a bad position, restore the user's query.
   1285             setQuery(oldQuery);
   1286         }
   1287     }
   1288 
   1289     /**
   1290      * Launches an intent based on a suggestion.
   1291      *
   1292      * @param position The index of the suggestion to create the intent from.
   1293      * @param actionKey The key code of the action key that was pressed,
   1294      *        or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
   1295      * @param actionMsg The message for the action key that was pressed,
   1296      *        or <code>null</code> if none.
   1297      * @return true if a successful launch, false if could not (e.g. bad position).
   1298      */
   1299     private boolean launchSuggestion(int position, int actionKey, String actionMsg) {
   1300         Cursor c = mSuggestionsAdapter.getCursor();
   1301         if ((c != null) && c.moveToPosition(position)) {
   1302 
   1303             Intent intent = createIntentFromSuggestion(c, actionKey, actionMsg);
   1304 
   1305             // launch the intent
   1306             launchIntent(intent);
   1307 
   1308             return true;
   1309         }
   1310         return false;
   1311     }
   1312 
   1313     /**
   1314      * Launches an intent, including any special intent handling.
   1315      */
   1316     private void launchIntent(Intent intent) {
   1317         if (intent == null) {
   1318             return;
   1319         }
   1320         try {
   1321             // If the intent was created from a suggestion, it will always have an explicit
   1322             // component here.
   1323             getContext().startActivity(intent);
   1324         } catch (RuntimeException ex) {
   1325             Log.e(LOG_TAG, "Failed launch activity: " + intent, ex);
   1326         }
   1327     }
   1328 
   1329     /**
   1330      * Sets the text in the query box, without updating the suggestions.
   1331      */
   1332     private void setQuery(CharSequence query) {
   1333         mQueryTextView.setText(query, true);
   1334         // Move the cursor to the end
   1335         mQueryTextView.setSelection(TextUtils.isEmpty(query) ? 0 : query.length());
   1336     }
   1337 
   1338     private void launchQuerySearch(int actionKey, String actionMsg, String query) {
   1339         String action = Intent.ACTION_SEARCH;
   1340         Intent intent = createIntent(action, null, null, query, actionKey, actionMsg);
   1341         getContext().startActivity(intent);
   1342     }
   1343 
   1344     /**
   1345      * Constructs an intent from the given information and the search dialog state.
   1346      *
   1347      * @param action Intent action.
   1348      * @param data Intent data, or <code>null</code>.
   1349      * @param extraData Data for {@link SearchManager#EXTRA_DATA_KEY} or <code>null</code>.
   1350      * @param query Intent query, or <code>null</code>.
   1351      * @param actionKey The key code of the action key that was pressed,
   1352      *        or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
   1353      * @param actionMsg The message for the action key that was pressed,
   1354      *        or <code>null</code> if none.
   1355      * @param mode The search mode, one of the acceptable values for
   1356      *             {@link SearchManager#SEARCH_MODE}, or {@code null}.
   1357      * @return The intent.
   1358      */
   1359     private Intent createIntent(String action, Uri data, String extraData, String query,
   1360             int actionKey, String actionMsg) {
   1361         // Now build the Intent
   1362         Intent intent = new Intent(action);
   1363         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
   1364         // We need CLEAR_TOP to avoid reusing an old task that has other activities
   1365         // on top of the one we want. We don't want to do this in in-app search though,
   1366         // as it can be destructive to the activity stack.
   1367         if (data != null) {
   1368             intent.setData(data);
   1369         }
   1370         intent.putExtra(SearchManager.USER_QUERY, mUserQuery);
   1371         if (query != null) {
   1372             intent.putExtra(SearchManager.QUERY, query);
   1373         }
   1374         if (extraData != null) {
   1375             intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData);
   1376         }
   1377         if (mAppSearchData != null) {
   1378             intent.putExtra(SearchManager.APP_DATA, mAppSearchData);
   1379         }
   1380         if (actionKey != KeyEvent.KEYCODE_UNKNOWN) {
   1381             intent.putExtra(SearchManager.ACTION_KEY, actionKey);
   1382             intent.putExtra(SearchManager.ACTION_MSG, actionMsg);
   1383         }
   1384         intent.setComponent(mSearchable.getSearchActivity());
   1385         return intent;
   1386     }
   1387 
   1388     /**
   1389      * Create and return an Intent that can launch the voice search activity for web search.
   1390      */
   1391     private Intent createVoiceWebSearchIntent(Intent baseIntent, SearchableInfo searchable) {
   1392         Intent voiceIntent = new Intent(baseIntent);
   1393         ComponentName searchActivity = searchable.getSearchActivity();
   1394         voiceIntent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, searchActivity == null ? null
   1395                 : searchActivity.flattenToShortString());
   1396         return voiceIntent;
   1397     }
   1398 
   1399     /**
   1400      * Create and return an Intent that can launch the voice search activity, perform a specific
   1401      * voice transcription, and forward the results to the searchable activity.
   1402      *
   1403      * @param baseIntent The voice app search intent to start from
   1404      * @return A completely-configured intent ready to send to the voice search activity
   1405      */
   1406     private Intent createVoiceAppSearchIntent(Intent baseIntent, SearchableInfo searchable) {
   1407         ComponentName searchActivity = searchable.getSearchActivity();
   1408 
   1409         // create the necessary intent to set up a search-and-forward operation
   1410         // in the voice search system.   We have to keep the bundle separate,
   1411         // because it becomes immutable once it enters the PendingIntent
   1412         Intent queryIntent = new Intent(Intent.ACTION_SEARCH);
   1413         queryIntent.setComponent(searchActivity);
   1414         PendingIntent pending = PendingIntent.getActivity(getContext(), 0, queryIntent,
   1415                 PendingIntent.FLAG_ONE_SHOT);
   1416 
   1417         // Now set up the bundle that will be inserted into the pending intent
   1418         // when it's time to do the search.  We always build it here (even if empty)
   1419         // because the voice search activity will always need to insert "QUERY" into
   1420         // it anyway.
   1421         Bundle queryExtras = new Bundle();
   1422 
   1423         // Now build the intent to launch the voice search.  Add all necessary
   1424         // extras to launch the voice recognizer, and then all the necessary extras
   1425         // to forward the results to the searchable activity
   1426         Intent voiceIntent = new Intent(baseIntent);
   1427 
   1428         // Add all of the configuration options supplied by the searchable's metadata
   1429         String languageModel = RecognizerIntent.LANGUAGE_MODEL_FREE_FORM;
   1430         String prompt = null;
   1431         String language = null;
   1432         int maxResults = 1;
   1433 
   1434         Resources resources = getResources();
   1435         if (searchable.getVoiceLanguageModeId() != 0) {
   1436             languageModel = resources.getString(searchable.getVoiceLanguageModeId());
   1437         }
   1438         if (searchable.getVoicePromptTextId() != 0) {
   1439             prompt = resources.getString(searchable.getVoicePromptTextId());
   1440         }
   1441         if (searchable.getVoiceLanguageId() != 0) {
   1442             language = resources.getString(searchable.getVoiceLanguageId());
   1443         }
   1444         if (searchable.getVoiceMaxResults() != 0) {
   1445             maxResults = searchable.getVoiceMaxResults();
   1446         }
   1447         voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, languageModel);
   1448         voiceIntent.putExtra(RecognizerIntent.EXTRA_PROMPT, prompt);
   1449         voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, language);
   1450         voiceIntent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, maxResults);
   1451         voiceIntent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, searchActivity == null ? null
   1452                 : searchActivity.flattenToShortString());
   1453 
   1454         // Add the values that configure forwarding the results
   1455         voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT, pending);
   1456         voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT_BUNDLE, queryExtras);
   1457 
   1458         return voiceIntent;
   1459     }
   1460 
   1461     /**
   1462      * When a particular suggestion has been selected, perform the various lookups required
   1463      * to use the suggestion.  This includes checking the cursor for suggestion-specific data,
   1464      * and/or falling back to the XML for defaults;  It also creates REST style Uri data when
   1465      * the suggestion includes a data id.
   1466      *
   1467      * @param c The suggestions cursor, moved to the row of the user's selection
   1468      * @param actionKey The key code of the action key that was pressed,
   1469      *        or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
   1470      * @param actionMsg The message for the action key that was pressed,
   1471      *        or <code>null</code> if none.
   1472      * @return An intent for the suggestion at the cursor's position.
   1473      */
   1474     private Intent createIntentFromSuggestion(Cursor c, int actionKey, String actionMsg) {
   1475         try {
   1476             // use specific action if supplied, or default action if supplied, or fixed default
   1477             String action = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_ACTION);
   1478 
   1479             if (action == null) {
   1480                 action = mSearchable.getSuggestIntentAction();
   1481             }
   1482             if (action == null) {
   1483                 action = Intent.ACTION_SEARCH;
   1484             }
   1485 
   1486             // use specific data if supplied, or default data if supplied
   1487             String data = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA);
   1488             if (data == null) {
   1489                 data = mSearchable.getSuggestIntentData();
   1490             }
   1491             // then, if an ID was provided, append it.
   1492             if (data != null) {
   1493                 String id = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID);
   1494                 if (id != null) {
   1495                     data = data + "/" + Uri.encode(id);
   1496                 }
   1497             }
   1498             Uri dataUri = (data == null) ? null : Uri.parse(data);
   1499 
   1500             String query = getColumnString(c, SearchManager.SUGGEST_COLUMN_QUERY);
   1501             String extraData = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA);
   1502 
   1503             return createIntent(action, dataUri, extraData, query, actionKey, actionMsg);
   1504         } catch (RuntimeException e ) {
   1505             int rowNum;
   1506             try {                       // be really paranoid now
   1507                 rowNum = c.getPosition();
   1508             } catch (RuntimeException e2 ) {
   1509                 rowNum = -1;
   1510             }
   1511             Log.w(LOG_TAG, "Search Suggestions cursor at row " + rowNum +
   1512                             " returned exception" + e.toString());
   1513             return null;
   1514         }
   1515     }
   1516 
   1517     private void forceSuggestionQuery() {
   1518         mQueryTextView.doBeforeTextChanged();
   1519         mQueryTextView.doAfterTextChanged();
   1520     }
   1521 
   1522     static boolean isLandscapeMode(Context context) {
   1523         return context.getResources().getConfiguration().orientation
   1524                 == Configuration.ORIENTATION_LANDSCAPE;
   1525     }
   1526 
   1527     /**
   1528      * Callback to watch the text field for empty/non-empty
   1529      */
   1530     private TextWatcher mTextWatcher = new TextWatcher() {
   1531 
   1532         public void beforeTextChanged(CharSequence s, int start, int before, int after) { }
   1533 
   1534         public void onTextChanged(CharSequence s, int start,
   1535                 int before, int after) {
   1536             SearchView.this.onTextChanged(s);
   1537         }
   1538 
   1539         public void afterTextChanged(Editable s) {
   1540         }
   1541     };
   1542 
   1543     /**
   1544      * Local subclass for AutoCompleteTextView.
   1545      * @hide
   1546      */
   1547     public static class SearchAutoComplete extends AutoCompleteTextView {
   1548 
   1549         private int mThreshold;
   1550         private SearchView mSearchView;
   1551 
   1552         public SearchAutoComplete(Context context) {
   1553             super(context);
   1554             mThreshold = getThreshold();
   1555         }
   1556 
   1557         public SearchAutoComplete(Context context, AttributeSet attrs) {
   1558             super(context, attrs);
   1559             mThreshold = getThreshold();
   1560         }
   1561 
   1562         public SearchAutoComplete(Context context, AttributeSet attrs, int defStyle) {
   1563             super(context, attrs, defStyle);
   1564             mThreshold = getThreshold();
   1565         }
   1566 
   1567         void setSearchView(SearchView searchView) {
   1568             mSearchView = searchView;
   1569         }
   1570 
   1571         @Override
   1572         public void setThreshold(int threshold) {
   1573             super.setThreshold(threshold);
   1574             mThreshold = threshold;
   1575         }
   1576 
   1577         /**
   1578          * Returns true if the text field is empty, or contains only whitespace.
   1579          */
   1580         private boolean isEmpty() {
   1581             return TextUtils.getTrimmedLength(getText()) == 0;
   1582         }
   1583 
   1584         /**
   1585          * We override this method to avoid replacing the query box text when a
   1586          * suggestion is clicked.
   1587          */
   1588         @Override
   1589         protected void replaceText(CharSequence text) {
   1590         }
   1591 
   1592         /**
   1593          * We override this method to avoid an extra onItemClick being called on
   1594          * the drop-down's OnItemClickListener by
   1595          * {@link AutoCompleteTextView#onKeyUp(int, KeyEvent)} when an item is
   1596          * clicked with the trackball.
   1597          */
   1598         @Override
   1599         public void performCompletion() {
   1600         }
   1601 
   1602         /**
   1603          * We override this method to be sure and show the soft keyboard if
   1604          * appropriate when the TextView has focus.
   1605          */
   1606         @Override
   1607         public void onWindowFocusChanged(boolean hasWindowFocus) {
   1608             super.onWindowFocusChanged(hasWindowFocus);
   1609 
   1610             if (hasWindowFocus && mSearchView.hasFocus() && getVisibility() == VISIBLE) {
   1611                 InputMethodManager inputManager = (InputMethodManager) getContext()
   1612                         .getSystemService(Context.INPUT_METHOD_SERVICE);
   1613                 inputManager.showSoftInput(this, 0);
   1614                 // If in landscape mode, then make sure that
   1615                 // the ime is in front of the dropdown.
   1616                 if (isLandscapeMode(getContext())) {
   1617                     ensureImeVisible(true);
   1618                 }
   1619             }
   1620         }
   1621 
   1622         @Override
   1623         protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
   1624             super.onFocusChanged(focused, direction, previouslyFocusedRect);
   1625             mSearchView.onTextFocusChanged();
   1626         }
   1627 
   1628         /**
   1629          * We override this method so that we can allow a threshold of zero,
   1630          * which ACTV does not.
   1631          */
   1632         @Override
   1633         public boolean enoughToFilter() {
   1634             return mThreshold <= 0 || super.enoughToFilter();
   1635         }
   1636 
   1637         @Override
   1638         public boolean onKeyPreIme(int keyCode, KeyEvent event) {
   1639             if (keyCode == KeyEvent.KEYCODE_BACK) {
   1640                 // special case for the back key, we do not even try to send it
   1641                 // to the drop down list but instead, consume it immediately
   1642                 if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
   1643                     KeyEvent.DispatcherState state = getKeyDispatcherState();
   1644                     if (state != null) {
   1645                         state.startTracking(event, this);
   1646                     }
   1647                     return true;
   1648                 } else if (event.getAction() == KeyEvent.ACTION_UP) {
   1649                     KeyEvent.DispatcherState state = getKeyDispatcherState();
   1650                     if (state != null) {
   1651                         state.handleUpEvent(event);
   1652                     }
   1653                     if (event.isTracking() && !event.isCanceled()) {
   1654                         mSearchView.clearFocus();
   1655                         mSearchView.setImeVisibility(false);
   1656                         return true;
   1657                     }
   1658                 }
   1659             }
   1660             return super.onKeyPreIme(keyCode, event);
   1661         }
   1662 
   1663     }
   1664 }
   1665