blob: c8f395e8a03c70eab8c3b2967dd70df019454e87 [file] [log] [blame]
/*
* 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 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 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!
/**
* 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_FLAG_SEARCH |
InputType.TYPE_TEXT_VARIATION_SEARCH_STRING);
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);
}
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() {
// decompose searchMode attribute
// TODO How do I reconcile these hardcoded values with the flag bits defined in
// in attrs.xml? e.g. android.R.id.filterMode = 0x010200a4 instead of just "1"
/* mFilterMode = (0 != (mSearchMode & 1)); */
/* mQuickStart = (0 != (mSearchMode & 2)); */
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 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 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();
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();
}
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.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);
}
}