/* * 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); } }