FileDocCategorySizeDatePackage
SearchableInfo.javaAPI DocAndroid 1.5 API34681Wed May 06 22:41:56 BST 2009android.server.search

SearchableInfo.java

/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.server.search;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
import android.content.pm.ResolveInfo;
import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.InputType;
import android.util.AttributeSet;
import android.util.Log;
import android.util.Xml;
import android.view.inputmethod.EditorInfo;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

public final class SearchableInfo implements Parcelable {

    // general debugging support
    final static String LOG_TAG = "SearchableInfo";
    
    // set this flag to 1 to prevent any apps from providing suggestions
    final static int DBG_INHIBIT_SUGGESTIONS = 0;

    // static strings used for XML lookups, etc.
    // TODO how should these be documented for the developer, in a more structured way than 
    // the current long wordy javadoc in SearchManager.java ?
    private static final String MD_LABEL_DEFAULT_SEARCHABLE = "android.app.default_searchable";
    private static final String MD_LABEL_SEARCHABLE = "android.app.searchable";
    private static final String MD_SEARCHABLE_SYSTEM_SEARCH = "*";
    private static final String MD_XML_ELEMENT_SEARCHABLE = "searchable";
    private static final String MD_XML_ELEMENT_SEARCHABLE_ACTION_KEY = "actionkey";

    // class maintenance and general shared data
    private static HashMap<ComponentName, SearchableInfo> sSearchablesMap = null;
    private static ArrayList<SearchableInfo> sSearchablesList = null;
    private static SearchableInfo sDefaultSearchable = null;
    
    // true member variables - what we know about the searchability
    // TO-DO replace public with getters
    public boolean mSearchable = false;
    private int mLabelId = 0;
    public ComponentName mSearchActivity = null;
    private int mHintId = 0;
    private int mSearchMode = 0;
    public boolean mBadgeLabel = false;
    public boolean mBadgeIcon = false;
    public boolean mQueryRewriteFromData = false;
    public boolean mQueryRewriteFromText = false;
    private int mIconId = 0;
    private int mSearchButtonText = 0;
    private int mSearchInputType = 0;
    private int mSearchImeOptions = 0;
    private String mSuggestAuthority = null;
    private String mSuggestPath = null;
    private String mSuggestSelection = null;
    private String mSuggestIntentAction = null;
    private String mSuggestIntentData = null;
    private ActionKeyInfo mActionKeyList = null;
    private String mSuggestProviderPackage = null;
    private Context mCacheActivityContext = null;   // use during setup only - don't hold memory!
    
    // Flag values for Searchable_voiceSearchMode
    private static int VOICE_SEARCH_SHOW_BUTTON = 1;
    private static int VOICE_SEARCH_LAUNCH_WEB_SEARCH = 2;
    private static int VOICE_SEARCH_LAUNCH_RECOGNIZER = 4;
    private int mVoiceSearchMode = 0;
    private int mVoiceLanguageModeId;       // voiceLanguageModel
    private int mVoicePromptTextId;         // voicePromptText
    private int mVoiceLanguageId;           // voiceLanguage
    private int mVoiceMaxResults;           // voiceMaxResults
    
    /**
     * Set the default searchable activity (when none is specified).
     */
    public static void setDefaultSearchable(Context context, 
                                            ComponentName activity) {
        synchronized (SearchableInfo.class) {
            SearchableInfo si = null;
            if (activity != null) {
                si = getSearchableInfo(context, activity);
                if (si != null) {
                    // move to front of list
                    sSearchablesList.remove(si);
                    sSearchablesList.add(0, si);
                }
            }
            sDefaultSearchable = si;
        }
    }
    
    /**
     * Provides the system-default search activity, which you can use
     * whenever getSearchableInfo() returns null;
     * 
     * @return Returns the system-default search activity, null if never defined
     */
    public static SearchableInfo getDefaultSearchable() {
        synchronized (SearchableInfo.class) {
            return sDefaultSearchable;
        }
    }
    
    /**
     * Retrieve the authority for obtaining search suggestions.
     * 
     * @return Returns a string containing the suggestions authority.
     */
    public String getSuggestAuthority() {
        return mSuggestAuthority;
    }
    
    /**
     * Retrieve the path for obtaining search suggestions.
     * 
     * @return Returns a string containing the suggestions path, or null if not provided.
     */
    public String getSuggestPath() {
        return mSuggestPath;
    }
    
    /**
     * Retrieve the selection pattern for obtaining search suggestions.  This must
     * include a single ? which will be used for the user-typed characters.
     * 
     * @return Returns a string containing the suggestions authority.
     */
    public String getSuggestSelection() {
        return mSuggestSelection;
    }
    
    /**
     * Retrieve the (optional) intent action for use with these suggestions.  This is
     * useful if all intents will have the same action (e.g. "android.intent.action.VIEW").
     * 
     * Can be overriden in any given suggestion via the AUTOSUGGEST_COLUMN_INTENT_ACTION column.
     * 
     * @return Returns a string containing the default intent action.
     */
    public String getSuggestIntentAction() {
        return mSuggestIntentAction;
    }
    
    /**
     * Retrieve the (optional) intent data for use with these suggestions.  This is
     * useful if all intents will have similar data URIs (e.g. "android.intent.action.VIEW"), 
     * but you'll likely need to provide a specific ID as well via the column
     * AUTOSUGGEST_COLUMN_INTENT_DATA_ID, which will be appended to the intent data URI.
     * 
     * Can be overriden in any given suggestion via the AUTOSUGGEST_COLUMN_INTENT_DATA column.
     * 
     * @return Returns a string containing the default intent data.
     */
    public String getSuggestIntentData() {
        return mSuggestIntentData;
    }
    
    /**
     * Get the context for the searchable activity.  
     * 
     * This is fairly expensive so do it on the original scan, or when an app is
     * selected, but don't hang on to the result forever.
     * 
     * @param context You need to supply a context to start with
     * @return Returns a context related to the searchable activity
     */
    public Context getActivityContext(Context context) {
        Context theirContext = null;
        try {
            theirContext = context.createPackageContext(mSearchActivity.getPackageName(), 0);
        } catch (PackageManager.NameNotFoundException e) {
            // unexpected, but we deal with this by null-checking theirContext
        } catch (java.lang.SecurityException e) {
            // unexpected, but we deal with this by null-checking theirContext
        }
        
        return theirContext;
    }
    
    /**
     * Get the context for the suggestions provider.  
     * 
     * This is fairly expensive so do it on the original scan, or when an app is
     * selected, but don't hang on to the result forever.
     * 
     * @param context You need to supply a context to start with
     * @param activityContext If we can determine that the provider and the activity are the
     * same, we'll just return this one.
     * @return Returns a context related to the context provider
     */
    public Context getProviderContext(Context context, Context activityContext) {
        Context theirContext = null;
        if (mSearchActivity.getPackageName().equals(mSuggestProviderPackage)) {
            return activityContext;
        }
        if (mSuggestProviderPackage != null)
        try {
            theirContext = context.createPackageContext(mSuggestProviderPackage, 0);
        } catch (PackageManager.NameNotFoundException e) {
            // unexpected, but we deal with this by null-checking theirContext
        } catch (java.lang.SecurityException e) {
            // unexpected, but we deal with this by null-checking theirContext
        }
        
        return theirContext;
    }
    
    /**
     * Factory.  Look up, or construct, based on the activity.
     * 
     * The activities fall into three cases, based on meta-data found in 
     * the manifest entry:
     * <ol>
     * <li>The activity itself implements search.  This is indicated by the
     * presence of a "android.app.searchable" meta-data attribute.
     * The value is a reference to an XML file containing search information.</li>
     * <li>A related activity implements search.  This is indicated by the
     * presence of a "android.app.default_searchable" meta-data attribute.
     * The value is a string naming the activity implementing search.  In this
     * case the factory will "redirect" and return the searchable data.</li>
     * <li>No searchability data is provided.  We return null here and other
     * code will insert the "default" (e.g. contacts) search.
     * 
     * TODO: cache the result in the map, and check the map first.
     * TODO: it might make sense to implement the searchable reference as
     * an application meta-data entry.  This way we don't have to pepper each
     * and every activity.
     * TODO: can we skip the constructor step if it's a non-searchable?
     * TODO: does it make sense to plug the default into a slot here for 
     * automatic return?  Probably not, but it's one way to do it.
     *
     * @param activity The name of the current activity, or null if the 
     * activity does not define any explicit searchable metadata.
     */
    public static SearchableInfo getSearchableInfo(Context context, 
                                                   ComponentName activity) {
        // Step 1.  Is the result already hashed?  (case 1)
        SearchableInfo result;
        synchronized (SearchableInfo.class) {
            result = sSearchablesMap.get(activity);
            if (result != null) return result;
        }
        
        // Step 2.  See if the current activity references a searchable.
        // Note:  Conceptually, this could be a while(true) loop, but there's
        // no point in implementing reference chaining here and risking a loop.  
        // References must point directly to searchable activities.
       
        ActivityInfo ai = null;
        XmlPullParser xml = null;
        try {
            ai = context.getPackageManager().
                       getActivityInfo(activity, PackageManager.GET_META_DATA );
            String refActivityName = null;
            
            // First look for activity-specific reference
            Bundle md = ai.metaData;
            if (md != null) {
                refActivityName = md.getString(MD_LABEL_DEFAULT_SEARCHABLE);
            }
            // If not found, try for app-wide reference
            if (refActivityName == null) {
                md = ai.applicationInfo.metaData;
                if (md != null) {
                    refActivityName = md.getString(MD_LABEL_DEFAULT_SEARCHABLE);
                }
            }
            
            // Irrespective of source, if a reference was found, follow it.
            if (refActivityName != null)
            {
                // An app or activity can declare that we should simply launch 
                // "system default search" if search is invoked.
                if (refActivityName.equals(MD_SEARCHABLE_SYSTEM_SEARCH)) {
                    return getDefaultSearchable();
                }
                String pkg = activity.getPackageName();
                ComponentName referredActivity;
                if (refActivityName.charAt(0) == '.') {
                    referredActivity = new ComponentName(pkg, pkg + refActivityName);
                } else {
                    referredActivity = new ComponentName(pkg, refActivityName);
                }

                // Now try the referred activity, and if found, cache
                // it against the original name so we can skip the check
                synchronized (SearchableInfo.class) {
                    result = sSearchablesMap.get(referredActivity);
                    if (result != null) {
                        sSearchablesMap.put(activity, result);
                        return result;
                    }
                }
            }
        } catch (PackageManager.NameNotFoundException e) {
            // case 3: no metadata
        }
 
        // Step 3.  None found. Return null.
        return null;
        
    }
    
    /**
     * Super-factory.  Builds an entire list (suitable for display) of 
     * activities that are searchable, by iterating the entire set of 
     * ACTION_SEARCH intents.  
     * 
     * Also clears the hash of all activities -> searches which will
     * refill as the user clicks "search".
     * 
     * This should only be done at startup and again if we know that the
     * list has changed.
     * 
     * TODO: every activity that provides a ACTION_SEARCH intent should
     * also provide searchability meta-data.  There are a bunch of checks here
     * that, if data is not found, silently skip to the next activity.  This
     * won't help a developer trying to figure out why their activity isn't
     * showing up in the list, but an exception here is too rough.  I would
     * like to find a better notification mechanism.
     * 
     * TODO: sort the list somehow?  UI choice.
     * 
     * @param context a context we can use during this work
     */
    public static void buildSearchableList(Context context) {
        
        // create empty hash & list
        HashMap<ComponentName, SearchableInfo> newSearchablesMap 
                                = new HashMap<ComponentName, SearchableInfo>();
        ArrayList<SearchableInfo> newSearchablesList
                                = new ArrayList<SearchableInfo>();

        // use intent resolver to generate list of ACTION_SEARCH receivers
        final PackageManager pm = context.getPackageManager();
        List<ResolveInfo> infoList;
        final Intent intent = new Intent(Intent.ACTION_SEARCH);
        infoList = pm.queryIntentActivities(intent, PackageManager.GET_META_DATA);
        
        // analyze each one, generate a Searchables record, and record
        if (infoList != null) {
            int count = infoList.size();
            for (int ii = 0; ii < count; ii++) {
                // for each component, try to find metadata
                ResolveInfo info = infoList.get(ii);
                ActivityInfo ai = info.activityInfo;
                XmlResourceParser xml = ai.loadXmlMetaData(context.getPackageManager(), 
                                                       MD_LABEL_SEARCHABLE);
                if (xml == null) {
                    continue;
                }
                ComponentName cName = new ComponentName(
                        info.activityInfo.packageName, 
                        info.activityInfo.name);
                
                SearchableInfo searchable = getActivityMetaData(context, xml, cName);
                xml.close();
                
                if (searchable != null) {
                    // no need to keep the context any longer.  setup time is over.
                    searchable.mCacheActivityContext  = null;
                    
                    newSearchablesList.add(searchable);
                    newSearchablesMap.put(cName, searchable);
                }
            }
        }
        
        // record the final values as a coherent pair
        synchronized (SearchableInfo.class) {
            sSearchablesList = newSearchablesList;
            sSearchablesMap = newSearchablesMap;
        }
    }
    
    /**
     * Constructor
     * 
     * Given a ComponentName, get the searchability info
     * and build a local copy of it.  Use the factory, not this.
     * 
     * @param context runtime context
     * @param attr The attribute set we found in the XML file, contains the values that are used to
     * construct the object.
     * @param cName The component name of the searchable activity
     */
    private SearchableInfo(Context context, AttributeSet attr, final ComponentName cName) {
        // initialize as an "unsearchable" object
        mSearchable = false;
        mSearchActivity = cName;
        
        // to access another activity's resources, I need its context.
        // BE SURE to release the cache sometime after construction - it's a large object to hold
        mCacheActivityContext = getActivityContext(context);
        if (mCacheActivityContext != null) {
            TypedArray a = mCacheActivityContext.obtainStyledAttributes(attr,
                    com.android.internal.R.styleable.Searchable);
            mSearchMode = a.getInt(com.android.internal.R.styleable.Searchable_searchMode, 0);
            mLabelId = a.getResourceId(com.android.internal.R.styleable.Searchable_label, 0);
            mHintId = a.getResourceId(com.android.internal.R.styleable.Searchable_hint, 0);
            mIconId = a.getResourceId(com.android.internal.R.styleable.Searchable_icon, 0);
            mSearchButtonText = a.getResourceId(
                    com.android.internal.R.styleable.Searchable_searchButtonText, 0);
            mSearchInputType = a.getInt(com.android.internal.R.styleable.Searchable_inputType, 
                    InputType.TYPE_CLASS_TEXT |
                    InputType.TYPE_TEXT_VARIATION_NORMAL);
            mSearchImeOptions = a.getInt(com.android.internal.R.styleable.Searchable_imeOptions, 
                    EditorInfo.IME_ACTION_SEARCH);

            setSearchModeFlags();
            if (DBG_INHIBIT_SUGGESTIONS == 0) {
                mSuggestAuthority = a.getString(
                        com.android.internal.R.styleable.Searchable_searchSuggestAuthority);
                mSuggestPath = a.getString(
                        com.android.internal.R.styleable.Searchable_searchSuggestPath);
                mSuggestSelection = a.getString(
                        com.android.internal.R.styleable.Searchable_searchSuggestSelection);
                mSuggestIntentAction = a.getString(
                        com.android.internal.R.styleable.Searchable_searchSuggestIntentAction);
                mSuggestIntentData = a.getString(
                        com.android.internal.R.styleable.Searchable_searchSuggestIntentData);
            }
            mVoiceSearchMode = 
                a.getInt(com.android.internal.R.styleable.Searchable_voiceSearchMode, 0);
            // TODO this didn't work - came back zero from YouTube
            mVoiceLanguageModeId = 
                a.getResourceId(com.android.internal.R.styleable.Searchable_voiceLanguageModel, 0);
            mVoicePromptTextId = 
                a.getResourceId(com.android.internal.R.styleable.Searchable_voicePromptText, 0);
            mVoiceLanguageId = 
                a.getResourceId(com.android.internal.R.styleable.Searchable_voiceLanguage, 0);
            mVoiceMaxResults = 
                a.getInt(com.android.internal.R.styleable.Searchable_voiceMaxResults, 0);

            a.recycle();

            // get package info for suggestions provider (if any)
            if (mSuggestAuthority != null) {
                ProviderInfo pi =
                    context.getPackageManager().resolveContentProvider(mSuggestAuthority,
                            0);
                if (pi != null) {
                    mSuggestProviderPackage = pi.packageName;
                }
            }
        }

        // for now, implement some form of rules - minimal data
        if (mLabelId != 0) {
            mSearchable = true;
        } else {
            // Provide some help for developers instead of just silently discarding
            Log.w(LOG_TAG, "Insufficient metadata to configure searchability for " + 
                    cName.flattenToShortString());
        }
    }

    /**
     * Convert searchmode to flags.
     */
    private void setSearchModeFlags() {
        mBadgeLabel = (0 != (mSearchMode & 4));
        mBadgeIcon = (0 != (mSearchMode & 8)) && (mIconId != 0);
        mQueryRewriteFromData = (0 != (mSearchMode & 0x10));
        mQueryRewriteFromText = (0 != (mSearchMode & 0x20));
    }
    
    /**
     * Private class used to hold the "action key" configuration
     */
    public class ActionKeyInfo implements Parcelable {
        
        public int mKeyCode = 0;
        public String mQueryActionMsg;
        public String mSuggestActionMsg;
        public String mSuggestActionMsgColumn;
        private ActionKeyInfo mNext;
        
        /**
         * Create one object using attributeset as input data.
         * @param context runtime context
         * @param attr The attribute set we found in the XML file, contains the values that are used to
         * construct the object.
         * @param next We'll build these up using a simple linked list (since there are usually
         * just zero or one).
         */
        public ActionKeyInfo(Context context, AttributeSet attr, ActionKeyInfo next) {
            TypedArray a = mCacheActivityContext.obtainStyledAttributes(attr,
                    com.android.internal.R.styleable.SearchableActionKey);

            mKeyCode = a.getInt(
                    com.android.internal.R.styleable.SearchableActionKey_keycode, 0);
            mQueryActionMsg = a.getString(
                    com.android.internal.R.styleable.SearchableActionKey_queryActionMsg);
            if (DBG_INHIBIT_SUGGESTIONS == 0) {
                mSuggestActionMsg = a.getString(
                        com.android.internal.R.styleable.SearchableActionKey_suggestActionMsg);
                mSuggestActionMsgColumn = a.getString(
                        com.android.internal.R.styleable.SearchableActionKey_suggestActionMsgColumn);
            }
            a.recycle();

            // initialize any other fields
            mNext = next;

            // sanity check.  must have at least one action message, or invalidate the object.
            if ((mQueryActionMsg == null) && 
                    (mSuggestActionMsg == null) && 
                    (mSuggestActionMsgColumn == null)) {
                mKeyCode = 0;
            }           
        }

        /**
         * Instantiate a new ActionKeyInfo from the data in a Parcel that was
         * previously written with {@link #writeToParcel(Parcel, int)}.
         *
         * @param in The Parcel containing the previously written ActionKeyInfo,
         * positioned at the location in the buffer where it was written.
         * @param next The value to place in mNext, creating a linked list
         */
        public ActionKeyInfo(Parcel in, ActionKeyInfo next) {
            mKeyCode = in.readInt();
            mQueryActionMsg = in.readString();
            mSuggestActionMsg = in.readString();
            mSuggestActionMsgColumn = in.readString();
            mNext = next;
        }

        public int describeContents() {
            return 0;
        }

        public void writeToParcel(Parcel dest, int flags) {
            dest.writeInt(mKeyCode);
            dest.writeString(mQueryActionMsg);
            dest.writeString(mSuggestActionMsg);
            dest.writeString(mSuggestActionMsgColumn);
        }
    }
    
    /**
     * If any action keys were defined for this searchable activity, look up and return.
     * 
     * @param keyCode The key that was pressed
     * @return Returns the ActionKeyInfo record, or null if none defined
     */
    public ActionKeyInfo findActionKey(int keyCode) {
        ActionKeyInfo info = mActionKeyList;
        while (info != null) {
            if (info.mKeyCode == keyCode) {
                return info;
            }
            info = info.mNext;
        }
        return null;
    }
    
    /**
     * Get the metadata for a given activity
     * 
     * TODO: clean up where we return null vs. where we throw exceptions.
     * 
     * @param context runtime context
     * @param xml XML parser for reading attributes
     * @param cName The component name of the searchable activity
     * 
     * @result A completely constructed SearchableInfo, or null if insufficient XML data for it
     */
    private static SearchableInfo getActivityMetaData(Context context, XmlPullParser xml,
            final ComponentName cName)  {
        SearchableInfo result = null;
        
        // in order to use the attributes mechanism, we have to walk the parser
        // forward through the file until it's reading the tag of interest.
        try {
            int tagType = xml.next();
            while (tagType != XmlPullParser.END_DOCUMENT) {
                if (tagType == XmlPullParser.START_TAG) {
                    if (xml.getName().equals(MD_XML_ELEMENT_SEARCHABLE)) {
                        AttributeSet attr = Xml.asAttributeSet(xml);
                        if (attr != null) {
                            result = new SearchableInfo(context, attr, cName);
                            // if the constructor returned a bad object, exit now.
                            if (! result.mSearchable) {
                                return null;
                            }
                        }
                    } else if (xml.getName().equals(MD_XML_ELEMENT_SEARCHABLE_ACTION_KEY)) {
                        if (result == null) {
                            // Can't process an embedded element if we haven't seen the enclosing
                            return null;
                        }
                        AttributeSet attr = Xml.asAttributeSet(xml);
                        if (attr != null) {
                            ActionKeyInfo keyInfo = result.new ActionKeyInfo(context, attr, 
                                    result.mActionKeyList);
                            // only add to list if it is was useable
                            if (keyInfo.mKeyCode != 0) {
                                result.mActionKeyList = keyInfo;
                            }
                        }
                    }
                }
                tagType = xml.next();
            }
        } catch (XmlPullParserException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return result;
    }
    
    /**
     * Return the "label" (user-visible name) of this searchable context.  This must be 
     * accessed using the target (searchable) Activity's resources, not simply the context of the
     * caller.
     * 
     * @return Returns the resource Id
     */
    public int getLabelId() {
        return mLabelId;
    }
    
    /**
     * Return the resource Id of the hint text.  This must be 
     * accessed using the target (searchable) Activity's resources, not simply the context of the
     * caller.
     * 
     * @return Returns the resource Id, or 0 if not specified by this package.
     */
    public int getHintId() {
        return mHintId;
    }
    
    /**
     * Return the icon Id specified by the Searchable_icon meta-data entry.  This must be 
     * accessed using the target (searchable) Activity's resources, not simply the context of the
     * caller.
     * 
     * @return Returns the resource id.
     */
    public int getIconId() {
        return mIconId;
    }
    
    /**
     * @return true if android:voiceSearchMode="showVoiceSearchButton"
     */
    public boolean getVoiceSearchEnabled() {
        return 0 != (mVoiceSearchMode & VOICE_SEARCH_SHOW_BUTTON);
    }
    
    /**
     * @return true if android:voiceSearchMode="launchWebSearch"
     */
    public boolean getVoiceSearchLaunchWebSearch() {
        return 0 != (mVoiceSearchMode & VOICE_SEARCH_LAUNCH_WEB_SEARCH);
    }
    
    /**
     * @return true if android:voiceSearchMode="launchRecognizer"
     */
    public boolean getVoiceSearchLaunchRecognizer() {
        return 0 != (mVoiceSearchMode & VOICE_SEARCH_LAUNCH_RECOGNIZER);
    }
    
    /**
     * @return the resource Id of the language model string, if specified in the searchable
     * activity's metadata, or 0 if not specified.  
     */
    public int getVoiceLanguageModeId() {
        return mVoiceLanguageModeId;
    }
    
    /**
     * @return the resource Id of the voice prompt text string, if specified in the searchable
     * activity's metadata, or 0 if not specified.  
     */
    public int getVoicePromptTextId() {
        return mVoicePromptTextId;
    }
    
    /**
     * @return the resource Id of the spoken langauge, if specified in the searchable
     * activity's metadata, or 0 if not specified.  
     */
    public int getVoiceLanguageId() {
        return mVoiceLanguageId;
    }
    
    /**
     * @return the max results count, if specified in the searchable
     * activity's metadata, or 0 if not specified.  
     */
    public int getVoiceMaxResults() {
        return mVoiceMaxResults;
    }
    
    /**
     * Return the resource Id of replacement text for the "Search" button.
     * 
     * @return Returns the resource Id, or 0 if not specified by this package.
     */
    public int getSearchButtonText() {
        return mSearchButtonText;
    }
    
    /**
     * Return the input type as specified in the searchable attributes.  This will default to
     * InputType.TYPE_CLASS_TEXT if not specified (which is appropriate for free text input).
     * 
     * @return the input type
     */
    public int getInputType() {
        return mSearchInputType;
    }
    
    /**
     * Return the input method options specified in the searchable attributes.
     * This will default to EditorInfo.ACTION_SEARCH if not specified (which is
     * appropriate for a search box).
     * 
     * @return the input type
     */
    public int getImeOptions() {
        return mSearchImeOptions;
    }
    
    /**
     * Return the list of searchable activities, for use in the drop-down.
     */
    public static ArrayList<SearchableInfo> getSearchablesList() {
        synchronized (SearchableInfo.class) {
            ArrayList<SearchableInfo> result = new ArrayList<SearchableInfo>(sSearchablesList);
            return result;
        }
    }
    
    /**
     * Support for parcelable and aidl operations.
     */
    public static final Parcelable.Creator<SearchableInfo> CREATOR
    = new Parcelable.Creator<SearchableInfo>() {
        public SearchableInfo createFromParcel(Parcel in) {
            return new SearchableInfo(in);
        }

        public SearchableInfo[] newArray(int size) {
            return new SearchableInfo[size];
        }
    };

    /**
     * Instantiate a new SearchableInfo from the data in a Parcel that was
     * previously written with {@link #writeToParcel(Parcel, int)}.
     *
     * @param in The Parcel containing the previously written SearchableInfo,
     * positioned at the location in the buffer where it was written.
     */
    public SearchableInfo(Parcel in) {
        mSearchable = in.readInt() != 0;
        mLabelId = in.readInt();
        mSearchActivity = ComponentName.readFromParcel(in);
        mHintId = in.readInt();
        mSearchMode = in.readInt();
        mIconId = in.readInt();
        mSearchButtonText = in.readInt();
        mSearchInputType = in.readInt();
        mSearchImeOptions = in.readInt();
        setSearchModeFlags();

        mSuggestAuthority = in.readString();
        mSuggestPath = in.readString();
        mSuggestSelection = in.readString();
        mSuggestIntentAction = in.readString();
        mSuggestIntentData = in.readString();

        mActionKeyList = null;
        int count = in.readInt();
        while (count-- > 0) {
            mActionKeyList = new ActionKeyInfo(in, mActionKeyList);
        }
        
        mSuggestProviderPackage = in.readString();
        
        mVoiceSearchMode = in.readInt();
        mVoiceLanguageModeId = in.readInt();
        mVoicePromptTextId = in.readInt();
        mVoiceLanguageId = in.readInt();
        mVoiceMaxResults = in.readInt();
    }

    public int describeContents() {
        return 0;
    }

    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(mSearchable ? 1 : 0);
        dest.writeInt(mLabelId);
        mSearchActivity.writeToParcel(dest, flags);
        dest.writeInt(mHintId);
        dest.writeInt(mSearchMode);
        dest.writeInt(mIconId);
        dest.writeInt(mSearchButtonText);
        dest.writeInt(mSearchInputType);
        dest.writeInt(mSearchImeOptions);
        
        dest.writeString(mSuggestAuthority);
        dest.writeString(mSuggestPath);
        dest.writeString(mSuggestSelection);
        dest.writeString(mSuggestIntentAction);
        dest.writeString(mSuggestIntentData);

        // This is usually a very short linked list so we'll just pre-count it
        ActionKeyInfo nextKeyInfo = mActionKeyList;
        int count = 0;
        while (nextKeyInfo != null) {
            ++count;
            nextKeyInfo = nextKeyInfo.mNext;
        }
        dest.writeInt(count);
        // Now write count of 'em
        nextKeyInfo = mActionKeyList;
        while (count-- > 0) {
            nextKeyInfo.writeToParcel(dest, flags);
        }
        
        dest.writeString(mSuggestProviderPackage);

        dest.writeInt(mVoiceSearchMode);
        dest.writeInt(mVoiceLanguageModeId);
        dest.writeInt(mVoicePromptTextId);
        dest.writeInt(mVoiceLanguageId);
        dest.writeInt(mVoiceMaxResults);
    }
}