Merge branch 'readonly-p4-donut' into donut

This commit is contained in:
Karl Rosaen
2009-04-23 19:01:36 -07:00
committed by The Android Open Source Project
37 changed files with 2634 additions and 1689 deletions

File diff suppressed because it is too large Load Diff

View File

@ -17,12 +17,17 @@
package android.app;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.Configuration;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.server.search.SearchableInfo;
import android.view.KeyEvent;
/**
@ -439,20 +444,18 @@ import android.view.KeyEvent;
*
* <tr><th>{@link #SUGGEST_COLUMN_ICON_1}</th>
* <td>If your cursor includes this column, then all suggestions will be provided in an
* icons+text format. This value should be a reference (resource ID) of the icon to
* icons+text format. This value should be a reference to the icon to
* draw on the left side, or it can be null or zero to indicate no icon in this row.
* You must provide both cursor columns, or neither.
* </td>
* <td align="center">No, but required if you also have {@link #SUGGEST_COLUMN_ICON_2}</td>
* <td align="center">No.</td>
* </tr>
*
* <tr><th>{@link #SUGGEST_COLUMN_ICON_2}</th>
* <td>If your cursor includes this column, then all suggestions will be provided in an
* icons+text format. This value should be a reference (resource ID) of the icon to
* icons+text format. This value should be a reference to the icon to
* draw on the right side, or it can be null or zero to indicate no icon in this row.
* You must provide both cursor columns, or neither.
* </td>
* <td align="center">No, but required if you also have {@link #SUGGEST_COLUMN_ICON_1}</td>
* <td align="center">No.</td>
* </tr>
*
* <tr><th>{@link #SUGGEST_COLUMN_INTENT_ACTION}</th>
@ -1154,6 +1157,13 @@ public class SearchManager
*/
public final static String ACTION_KEY = "action_key";
/**
* Intent extra data key: This key will be used for the extra populated by the
* {@link #SUGGEST_COLUMN_INTENT_EXTRA_DATA} column.
* {@hide}
*/
public final static String EXTRA_DATA_KEY = "intent_extra_data_key";
/**
* Intent extra data key: Use this key with Intent.ACTION_SEARCH and
* {@link android.content.Intent#getStringExtra content.Intent.getStringExtra()}
@ -1195,20 +1205,58 @@ public class SearchManager
public final static String SUGGEST_COLUMN_TEXT_2 = "suggest_text_2";
/**
* Column name for suggestions cursor. <i>Optional.</i> If your cursor includes this column,
* then all suggestions will be provided in format that includes space for two small icons,
* then all suggestions will be provided in a format that includes space for two small icons,
* one at the left and one at the right of each suggestion. The data in the column must
* be a a resource ID for the icon you wish to have displayed. If you include this column,
* you must also include {@link #SUGGEST_COLUMN_ICON_2}.
* be a resource ID of a drawable, or a URI in one of the following formats:
*
* <ul>
* <li>content ({@link android.content.ContentResolver#SCHEME_CONTENT})</li>
* <li>android.resource ({@link android.content.ContentResolver#SCHEME_ANDROID_RESOURCE})</li>
* <li>file ({@link android.content.ContentResolver#SCHEME_FILE})</li>
* </ul>
*
* See {@link android.content.ContentResolver#openAssetFileDescriptor(Uri, String)}
* for more information on these schemes.
*/
public final static String SUGGEST_COLUMN_ICON_1 = "suggest_icon_1";
/**
* Column name for suggestions cursor. <i>Optional.</i> If your cursor includes this column,
* then all suggestions will be provided in format that includes space for two small icons,
* then all suggestions will be provided in a format that includes space for two small icons,
* one at the left and one at the right of each suggestion. The data in the column must
* be a a resource ID for the icon you wish to have displayed. If you include this column,
* you must also include {@link #SUGGEST_COLUMN_ICON_1}.
* be a resource ID of a drawable, or a URI in one of the following formats:
*
* <ul>
* <li>content ({@link android.content.ContentResolver#SCHEME_CONTENT})</li>
* <li>android.resource ({@link android.content.ContentResolver#SCHEME_ANDROID_RESOURCE})</li>
* <li>file ({@link android.content.ContentResolver#SCHEME_FILE})</li>
* </ul>
*
* See {@link android.content.ContentResolver#openAssetFileDescriptor(Uri, String)}
* for more information on these schemes.
*/
public final static String SUGGEST_COLUMN_ICON_2 = "suggest_icon_2";
/**
* Column name for suggestions cursor. <i>Optional.</i> If your cursor includes this column,
* then all suggestions will be provided in a format that includes space for two small icons,
* one at the left and one at the right of each suggestion. The data in the column must
* be a blob that contains a bitmap.
*
* This column overrides any icon provided in the {@link #SUGGEST_COLUMN_ICON_1} column.
*
* @hide
*/
public final static String SUGGEST_COLUMN_ICON_1_BITMAP = "suggest_icon_1_bitmap";
/**
* Column name for suggestions cursor. <i>Optional.</i> If your cursor includes this column,
* then all suggestions will be provided in a format that includes space for two small icons,
* one at the left and one at the right of each suggestion. The data in the column must
* be a blob that contains a bitmap.
*
* This column overrides any icon provided in the {@link #SUGGEST_COLUMN_ICON_2} column.
*
* @hide
*/
public final static String SUGGEST_COLUMN_ICON_2_BITMAP = "suggest_icon_2_bitmap";
/**
* Column name for suggestions cursor. <i>Optional.</i> If this column exists <i>and</i>
* this element exists at the given row, this is the action that will be used when
@ -1229,6 +1277,14 @@ public class SearchManager
* it is more efficient to specify it using XML metadata and omit it from the cursor.
*/
public final static String SUGGEST_COLUMN_INTENT_DATA = "suggest_intent_data";
/**
* Column name for suggestions cursor. <i>Optional.</i> This column allows suggestions
* to provide additional arbitrary data which will be included as an extra under the key
* {@link #EXTRA_DATA_KEY}.
*
* @hide pending API council approval
*/
public final static String SUGGEST_COLUMN_INTENT_EXTRA_DATA = "suggest_intent_extra_data";
/**
* Column name for suggestions cursor. <i>Optional.</i> If this column exists <i>and</i>
* this element exists at the given row, then "/" and this value will be appended to the data
@ -1244,6 +1300,54 @@ public class SearchManager
*/
public final static String SUGGEST_COLUMN_QUERY = "suggest_intent_query";
/**
* If a suggestion has this value in {@link #SUGGEST_COLUMN_INTENT_ACTION},
* the search dialog will switch to a different suggestion source when the
* suggestion is clicked.
*
* {@link #SUGGEST_COLUMN_INTENT_DATA} must contain
* the flattened {@link ComponentName} of the activity which is to be searched.
*
* TODO: Should {@link #SUGGEST_COLUMN_INTENT_DATA} instead contain a URI in the format
* used by {@link android.provider.Applications}?
*
* TODO: This intent should be protected by the same permission that we use
* for replacing the global search provider.
*
* The query text field will be set to the value of {@link #SUGGEST_COLUMN_QUERY}.
*
* @hide Pending API council approval.
*/
public final static String INTENT_ACTION_CHANGE_SEARCH_SOURCE
= "android.search.action.CHANGE_SEARCH_SOURCE";
/**
* If a suggestion has this value in {@link #SUGGEST_COLUMN_INTENT_ACTION},
* the search dialog will call {@link Cursor#respond(Bundle)} when the
* suggestion is clicked.
*
* The {@link Bundle} argument will be constructed
* in the same way as the "extra" bundle included in an Intent constructed
* from the suggestion.
*
* @hide Pending API council approval.
*/
public final static String INTENT_ACTION_CURSOR_RESPOND
= "android.search.action.CURSOR_RESPOND";
/**
* Intent action for starting the global search settings activity.
* The global search provider should handle this intent.
*
* @hide Pending API council approval.
*/
public final static String INTENT_ACTION_SEARCH_SETTINGS
= "android.search.action.SEARCH_SETTINGS";
/**
* Reference to the shared system search service.
*/
private static ISearchManager sService = getSearchManagerService();
private final Context mContext;
private final Handler mHandler;
@ -1257,12 +1361,6 @@ public class SearchManager
mContext = context;
mHandler = handler;
}
private static ISearchManager mService;
static {
mService = ISearchManager.Stub.asInterface(
ServiceManager.getService(Context.SEARCH_SERVICE));
}
/**
* Launch search UI.
@ -1459,5 +1557,93 @@ public class SearchManager
mSearchDialog.onConfigurationChanged(newConfig);
}
}
private static ISearchManager getSearchManagerService() {
return ISearchManager.Stub.asInterface(
ServiceManager.getService(Context.SEARCH_SERVICE));
}
/**
* Gets information about a searchable activity. This method is static so that it can
* be used from non-Activity contexts.
*
* @param componentName The activity to get searchable information for.
* @param globalSearch If <code>false</code>, return information about the given activity.
* If <code>true</code>, return information about the global search activity.
* @return Searchable information, or <code>null</code> if the activity is not searchable.
*
* @hide because SearchableInfo is not part of the API.
*/
public static SearchableInfo getSearchableInfo(ComponentName componentName,
boolean globalSearch) {
try {
return sService.getSearchableInfo(componentName, globalSearch);
} catch (RemoteException e) {
return null;
}
}
/**
* Checks whether the given searchable is the default searchable.
*
* @hide because SearchableInfo is not part of the API.
*/
public static boolean isDefaultSearchable(SearchableInfo searchable) {
SearchableInfo defaultSearchable = SearchManager.getSearchableInfo(null, true);
return defaultSearchable != null
&& defaultSearchable.mSearchActivity.equals(searchable.mSearchActivity);
}
/**
* Gets a cursor with search suggestions. This method is static so that it can
* be used from non-Activity context.
*
* @param searchable Information about how to get the suggestions.
* @param query The search text entered (so far).
* @return a cursor with suggestions, or <code>null</null> the suggestion query failed.
*
* @hide because SearchableInfo is not part of the API.
*/
public static Cursor getSuggestions(Context context, SearchableInfo searchable, String query) {
if (searchable == null) {
return null;
}
String authority = searchable.getSuggestAuthority();
if (authority == null) {
return null;
}
Uri.Builder uriBuilder = new Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT)
.authority(authority);
// if content path provided, insert it now
final String contentPath = searchable.getSuggestPath();
if (contentPath != null) {
uriBuilder.appendEncodedPath(contentPath);
}
// append standard suggestion query path
uriBuilder.appendPath(SearchManager.SUGGEST_URI_PATH_QUERY);
// get the query selection, may be null
String selection = searchable.getSuggestSelection();
// inject query, either as selection args or inline
String[] selArgs = null;
if (selection != null) { // use selection if provided
selArgs = new String[] { query };
} else { // no selection, use REST pattern
uriBuilder.appendPath(query);
}
Uri uri = uriBuilder
.query("") // TODO: Remove, workaround for a bug in Uri.writeToParcel()
.fragment("") // TODO: Remove, workaround for a bug in Uri.writeToParcel()
.build();
// finally, make the query
return context.getContentResolver().query(uri, null, selection, selArgs, null);
}
}

View File

@ -0,0 +1,344 @@
/*
* Copyright (C) 2009 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.app;
import android.content.ContentResolver;
import android.content.Context;
import android.content.res.Resources.NotFoundException;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.server.search.SearchableInfo;
import android.text.Html;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CursorAdapter;
import android.widget.ImageView;
import android.widget.ResourceCursorAdapter;
import android.widget.TextView;
import java.io.FileNotFoundException;
import java.util.WeakHashMap;
/**
* Provides the contents for the suggestion drop-down list.in {@link SearchDialog}.
*
* @hide
*/
class SuggestionsAdapter extends ResourceCursorAdapter {
private static final boolean DBG = false;
private static final String LOG_TAG = "SuggestionsAdapter";
private SearchableInfo mSearchable;
private Context mProviderContext;
private WeakHashMap<String, Drawable> mOutsideDrawablesCache;
// Cached column indexes, updated when the cursor changes.
private int mFormatCol;
private int mText1Col;
private int mText2Col;
private int mIconName1Col;
private int mIconName2Col;
private int mIconBitmap1Col;
private int mIconBitmap2Col;
public SuggestionsAdapter(Context context, SearchableInfo searchable,
WeakHashMap<String, Drawable> outsideDrawablesCache) {
super(context,
com.android.internal.R.layout.search_dropdown_item_icons_2line,
null, // no initial cursor
true); // auto-requery
mSearchable = searchable;
// set up provider resources (gives us icons, etc.)
Context activityContext = mSearchable.getActivityContext(mContext);
mProviderContext = mSearchable.getProviderContext(mContext, activityContext);
mOutsideDrawablesCache = outsideDrawablesCache;
}
/**
* Overridden to always return <code>false</code>, since we cannot be sure that
* suggestion sources return stable IDs.
*/
@Override
public boolean hasStableIds() {
return false;
}
/**
* Use the search suggestions provider to obtain a live cursor. This will be called
* in a worker thread, so it's OK if the query is slow (e.g. round trip for suggestions).
* The results will be processed in the UI thread and changeCursor() will be called.
*/
@Override
public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
if (DBG) Log.d(LOG_TAG, "runQueryOnBackgroundThread(" + constraint + ")");
String query = (constraint == null) ? "" : constraint.toString();
try {
return SearchManager.getSuggestions(mContext, mSearchable, query);
} catch (RuntimeException e) {
Log.w(LOG_TAG, "Search suggestions query threw an exception.", e);
return null;
}
}
/**
* Cache columns.
*/
@Override
public void changeCursor(Cursor c) {
if (DBG) Log.d(LOG_TAG, "changeCursor(" + c + ")");
super.changeCursor(c);
if (c != null) {
mFormatCol = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_FORMAT);
mText1Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1);
mText2Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2);
mIconName1Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_1);
mIconName2Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_2);
mIconBitmap1Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_1_BITMAP);
mIconBitmap2Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_2_BITMAP);
}
}
/**
* Tags the view with cached child view look-ups.
*/
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
View v = super.newView(context, cursor, parent);
v.setTag(new ChildViewCache(v));
return v;
}
/**
* Cache of the child views of drop-drown list items, to avoid looking up the children
* each time the contents of a list item are changed.
*/
private final static class ChildViewCache {
public final TextView mText1;
public final TextView mText2;
public final ImageView mIcon1;
public final ImageView mIcon2;
public ChildViewCache(View v) {
mText1 = (TextView) v.findViewById(com.android.internal.R.id.text1);
mText2 = (TextView) v.findViewById(com.android.internal.R.id.text2);
mIcon1 = (ImageView) v.findViewById(com.android.internal.R.id.icon1);
mIcon2 = (ImageView) v.findViewById(com.android.internal.R.id.icon2);
}
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
ChildViewCache views = (ChildViewCache) view.getTag();
String format = cursor.getString(mFormatCol);
boolean isHtml = "html".equals(format);
setViewText(cursor, views.mText1, mText1Col, isHtml);
setViewText(cursor, views.mText2, mText2Col, isHtml);
setViewIcon(cursor, views.mIcon1, mIconBitmap1Col, mIconName1Col);
setViewIcon(cursor, views.mIcon2, mIconBitmap2Col, mIconName2Col);
}
private void setViewText(Cursor cursor, TextView v, int textCol, boolean isHtml) {
if (v == null) {
return;
}
CharSequence text = null;
if (textCol >= 0) {
String str = cursor.getString(textCol);
text = (str != null && isHtml) ? Html.fromHtml(str) : str;
}
// Set the text even if it's null, since we need to clear any previous text.
v.setText(text);
if (TextUtils.isEmpty(text)) {
v.setVisibility(View.GONE);
} else {
v.setVisibility(View.VISIBLE);
}
}
private void setViewIcon(Cursor cursor, ImageView v, int iconBitmapCol, int iconNameCol) {
if (v == null) {
return;
}
Drawable drawable = null;
// First try the bitmap column
if (iconBitmapCol >= 0) {
byte[] data = cursor.getBlob(iconBitmapCol);
if (data != null) {
Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
if (bitmap != null) {
drawable = new BitmapDrawable(bitmap);
}
}
}
// If there was no bitmap, try the icon resource column.
if (drawable == null && iconNameCol >= 0) {
String value = cursor.getString(iconNameCol);
drawable = getDrawableFromResourceValue(value);
}
// Set the icon even if the drawable is null, since we need to clear any
// previous icon.
v.setImageDrawable(drawable);
if (drawable == null) {
v.setVisibility(View.GONE);
} else {
v.setVisibility(View.VISIBLE);
}
}
/**
* Gets the text to show in the query field when a suggestion is selected.
*
* @param cursor The Cursor to read the suggestion data from. The Cursor should already
* be moved to the suggestion that is to be read from.
* @return The text to show, or <code>null</code> if the query should not be
* changed when selecting this suggestion.
*/
@Override
public CharSequence convertToString(Cursor cursor) {
if (cursor == null) {
return null;
}
String query = getColumnString(cursor, SearchManager.SUGGEST_COLUMN_QUERY);
if (query != null) {
return query;
}
if (mSearchable.mQueryRewriteFromData) {
String data = getColumnString(cursor, SearchManager.SUGGEST_COLUMN_INTENT_DATA);
if (data != null) {
return data;
}
}
if (mSearchable.mQueryRewriteFromText) {
String text1 = getColumnString(cursor, SearchManager.SUGGEST_COLUMN_TEXT_1);
if (text1 != null) {
return text1;
}
}
return null;
}
/**
* This method is overridden purely to provide a bit of protection against
* flaky content providers.
*
* @see android.widget.ListAdapter#getView(int, View, ViewGroup)
*/
@Override
public View getView(int position, View convertView, ViewGroup parent) {
try {
return super.getView(position, convertView, parent);
} catch (RuntimeException e) {
Log.w(LOG_TAG, "Search suggestions cursor threw exception.", e);
// Put exception string in item title
View v = newView(mContext, mCursor, parent);
if (v != null) {
ChildViewCache views = (ChildViewCache) v.getTag();
TextView tv = views.mText1;
tv.setText(e.toString());
}
return v;
}
}
/**
* Gets a drawable given a value provided by a suggestion provider.
*
* This value could be just the string value of a resource id
* (e.g., "2130837524"), in which case we will try to retrieve a drawable from
* the provider's resources. If the value is not an integer, it is
* treated as a Uri and opened with
* {@link ContentResolver#openOutputStream(android.net.Uri, String)}.
*
* All resources and URIs are read using the suggestion provider's context.
*
* If the string is not formatted as expected, or no drawable can be found for
* the provided value, this method returns null.
*
* @param drawableId a string like "2130837524",
* "android.resource://com.android.alarmclock/2130837524",
* or "content://contacts/photos/253".
* @return a Drawable, or null if none found
*/
private Drawable getDrawableFromResourceValue(String drawableId) {
if (drawableId == null || drawableId.length() == 0 || "0".equals(drawableId)) {
return null;
}
// First, check the cache.
Drawable drawable = mOutsideDrawablesCache.get(drawableId);
if (drawable != null) return drawable;
try {
// Not cached, try using it as a plain resource ID in the provider's context.
int resourceId = Integer.parseInt(drawableId);
drawable = mProviderContext.getResources().getDrawable(resourceId);
} catch (NumberFormatException nfe) {
// The id was not an integer resource id.
// Let the ContentResolver handle content, android.resource and file URIs.
try {
Uri uri = Uri.parse(drawableId);
drawable = Drawable.createFromStream(
mProviderContext.getContentResolver().openInputStream(uri),
null);
} catch (FileNotFoundException fnfe) {
// drawable = null;
}
// If we got a drawable for this resource id, then stick it in the
// map so we don't do this lookup again.
if (drawable != null) {
mOutsideDrawablesCache.put(drawableId, drawable);
}
} catch (NotFoundException nfe) {
// Resource could not be found
// drawable = null;
}
return drawable;
}
/**
* Gets the value of a string column by name.
*
* @param cursor Cursor to read the value from.
* @param columnName The name of the column to read.
* @return The value of the given column, or <code>null</null>
* if the cursor does not contain the given column.
*/
public static String getColumnString(Cursor cursor, String columnName) {
int col = cursor.getColumnIndex(columnName);
if (col == -1) {
return null;
}
return cursor.getString(col);
}
}

View File

@ -0,0 +1,82 @@
/*
* Copyright (C) 2009 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.provider;
import android.app.SearchManager;
import android.net.Uri;
import android.widget.SimpleCursorAdapter;
/**
* <p>The Applications provider gives information about installed applications.</p>
*
* <p>This provider provides the following columns:
*
* <table border="2" width="85%" align="center" frame="hsides" rules="rows">
*
* <thead>
* <tr><th>Column Name</th> <th>Description</th> </tr>
* </thead>
*
* <tbody>
* <tr><th>{@link SearchManager#SUGGEST_COLUMN_TEXT_1}</th>
* <td>The application name.</td>
* </tr>
*
* <tr><th>{@link SearchManager#SUGGEST_COLUMN_INTENT_COMPONENT}</th>
* <td>The component to be used when forming the intent.</td>
* </tr>
*
* <tr><th>{@link SearchManager#SUGGEST_COLUMN_ICON_1}</th>
* <td>The application's icon resource id, prepended by its package name and
* separated by a colon, e.g., "com.android.alarmclock:2130837524". The
* package name is required for an activity interpreting this value to
* be able to correctly access the icon drawable, for example, in an override of
* {@link SimpleCursorAdapter#setViewImage(android.widget.ImageView, String)}.</td>
* </tr>
*
* <tr><th>{@link SearchManager#SUGGEST_COLUMN_ICON_2}</th>
* <td><i>Unused - column provided to conform to the {@link SearchManager} stipulation
* that all providers provide either both or neither of
* {@link SearchManager#SUGGEST_COLUMN_ICON_1} and
* {@link SearchManager#SUGGEST_COLUMN_ICON_2}.</td>
* </tr>
*
* @hide pending API council approval - should be unhidden at the same time as
* {@link SearchManager#SUGGEST_COLUMN_INTENT_COMPONENT}
*/
public class Applications {
private static final String TAG = "Applications";
/**
* The content authority for this provider.
*
* @hide
*/
public static final String AUTHORITY = "applications";
/**
* The content:// style URL for this provider
*
* @hide
*/
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY);
/**
* no public constructor since this is a utility class
*/
private Applications() {}
}

View File

@ -22,8 +22,8 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Handler;
import android.util.Config;
/**
* This is a simplified version of the Search Manager service. It no longer handles
@ -36,7 +36,6 @@ public class SearchManagerService extends ISearchManager.Stub
// general debugging support
private static final String TAG = "SearchManagerService";
private static final boolean DEBUG = false;
private static final boolean localLOGV = DEBUG ? Config.LOGD : Config.LOGV;
// configuration choices
private static final boolean IMMEDIATE_SEARCHABLES_UPDATE = true;
@ -45,9 +44,10 @@ public class SearchManagerService extends ISearchManager.Stub
private final Context mContext;
private final Handler mHandler;
private boolean mSearchablesDirty;
private Searchables mSearchables;
/**
* Initialize the Search Manager service in the provided system context.
* Initializes the Search Manager service in the provided system context.
* Only one instance of this object should be created!
*
* @param context to use for accessing DB, window manager, etc.
@ -55,6 +55,8 @@ public class SearchManagerService extends ISearchManager.Stub
public SearchManagerService(Context context) {
mContext = context;
mHandler = new Handler();
mSearchablesDirty = true;
mSearchables = new Searchables(context);
// Setup the infrastructure for updating and maintaining the list
// of searchable activities.
@ -64,7 +66,6 @@ public class SearchManagerService extends ISearchManager.Stub
filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
filter.addDataScheme("package");
mContext.registerReceiver(mIntentReceiver, filter, null, mHandler);
mSearchablesDirty = true;
// After startup settles down, preload the searchables list,
// which will reduce the delay when the search UI is invoked.
@ -109,34 +110,41 @@ public class SearchManagerService extends ISearchManager.Stub
};
/**
* Update the list of searchables, either at startup or in response to
* Updates the list of searchables, either at startup or in response to
* a package add/remove broadcast message.
*/
private void updateSearchables() {
SearchableInfo.buildSearchableList(mContext);
mSearchables.buildSearchableList();
mSearchablesDirty = false;
// TODO This is a hack. This shouldn't be hardcoded here, it's probably
// a policy.
// ComponentName defaultSearch = new ComponentName(
// "com.android.contacts",
// "com.android.contacts.ContactsListActivity" );
ComponentName defaultSearch = new ComponentName(
"com.android.googlesearch",
"com.android.googlesearch.GoogleSearch" );
SearchableInfo.setDefaultSearchable(mContext, defaultSearch);
// TODO SearchableInfo should be the source of truth about whether a searchable exists.
// As it stands, if the package exists but is misconfigured in some way, then this
// would fail, and needs to be fixed.
ComponentName defaultSearch = new ComponentName(
"com.android.globalsearch",
"com.android.globalsearch.GlobalSearch");
try {
mContext.getPackageManager().getActivityInfo(defaultSearch, 0);
} catch (NameNotFoundException e) {
defaultSearch = new ComponentName(
"com.android.googlesearch",
"com.android.googlesearch.GoogleSearch");
}
mSearchables.setDefaultSearchable(defaultSearch);
}
/**
* Return the searchableinfo for a given activity
* Returns the SearchableInfo for a given activity
*
* @param launchActivity The activity from which we're launching this search.
* @return Returns a SearchableInfo record describing the parameters of the search,
* or null if no searchable metadata was available.
* @param globalSearch If false, this will only launch the search that has been specifically
* defined by the application (which is usually defined as a local search). If no default
* search is defined in the current application or activity, no search will be launched.
* If true, this will always launch a platform-global (e.g. web-based) search instead.
* @return Returns a SearchableInfo record describing the parameters of the search,
* or null if no searchable metadata was available.
*/
public SearchableInfo getSearchableInfo(ComponentName launchActivity, boolean globalSearch) {
// final check. however we should try to avoid this, because
@ -146,11 +154,12 @@ public class SearchManagerService extends ISearchManager.Stub
}
SearchableInfo si = null;
if (globalSearch) {
si = SearchableInfo.getDefaultSearchable();
si = mSearchables.getDefaultSearchable();
} else {
si = SearchableInfo.getSearchableInfo(mContext, launchActivity);
si = mSearchables.getSearchableInfo(launchActivity);
}
return si;
}
}

View File

@ -21,14 +21,11 @@ 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;
@ -38,9 +35,6 @@ 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 {
@ -50,19 +44,12 @@ public final class SearchableInfo implements Parcelable {
// 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.
// static strings used for XML lookups.
// 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
@ -86,7 +73,6 @@ public final class SearchableInfo implements Parcelable {
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;
@ -97,37 +83,7 @@ public final class SearchableInfo implements Parcelable {
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.
@ -193,9 +149,16 @@ public final class SearchableInfo implements Parcelable {
* @return Returns a context related to the searchable activity
*/
public Context getActivityContext(Context context) {
return createActivityContext(context, mSearchActivity);
}
/**
* Creates a context for another activity.
*/
private static Context createActivityContext(Context context, ComponentName activity) {
Context theirContext = null;
try {
theirContext = context.createPackageContext(mSearchActivity.getPackageName(), 0);
theirContext = context.createPackageContext(activity.getPackageName(), 0);
} catch (PackageManager.NameNotFoundException e) {
// unexpected, but we deal with this by null-checking theirContext
} catch (java.lang.SecurityException e) {
@ -233,243 +196,69 @@ public final class SearchableInfo implements Parcelable {
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 activityContext runtime context for the activity that the searchable info is about.
* @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) {
private SearchableInfo(Context activityContext, 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);
TypedArray a = activityContext.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);
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();
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;
}
// get package info for suggestions provider (if any)
if (mSuggestAuthority != null) {
PackageManager pm = activityContext.getPackageManager();
ProviderInfo pi = pm.resolveContentProvider(mSuggestAuthority, 0);
if (pi != null) {
mSuggestProviderPackage = pi.packageName;
}
}
@ -496,7 +285,7 @@ public final class SearchableInfo implements Parcelable {
/**
* Private class used to hold the "action key" configuration
*/
public class ActionKeyInfo implements Parcelable {
public static class ActionKeyInfo implements Parcelable {
public int mKeyCode = 0;
public String mQueryActionMsg;
@ -506,14 +295,15 @@ public final class SearchableInfo implements Parcelable {
/**
* Create one object using attributeset as input data.
* @param context runtime context
* @param activityContext runtime context of the activity that the action key information
* is about.
* @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,
public ActionKeyInfo(Context activityContext, AttributeSet attr, ActionKeyInfo next) {
TypedArray a = activityContext.obtainStyledAttributes(attr,
com.android.internal.R.styleable.SearchableActionKey);
mKeyCode = a.getInt(
@ -584,6 +374,20 @@ public final class SearchableInfo implements Parcelable {
return null;
}
public static SearchableInfo getActivityMetaData(Context context, ActivityInfo activityInfo) {
// for each component, try to find metadata
XmlResourceParser xml =
activityInfo.loadXmlMetaData(context.getPackageManager(), MD_LABEL_SEARCHABLE);
if (xml == null) {
return null;
}
ComponentName cName = new ComponentName(activityInfo.packageName, activityInfo.name);
SearchableInfo searchable = getActivityMetaData(context, xml, cName);
xml.close();
return searchable;
}
/**
* Get the metadata for a given activity
*
@ -598,6 +402,7 @@ public final class SearchableInfo implements Parcelable {
private static SearchableInfo getActivityMetaData(Context context, XmlPullParser xml,
final ComponentName cName) {
SearchableInfo result = null;
Context activityContext = createActivityContext(context, cName);
// 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.
@ -608,7 +413,7 @@ public final class SearchableInfo implements Parcelable {
if (xml.getName().equals(MD_XML_ELEMENT_SEARCHABLE)) {
AttributeSet attr = Xml.asAttributeSet(xml);
if (attr != null) {
result = new SearchableInfo(context, attr, cName);
result = new SearchableInfo(activityContext, attr, cName);
// if the constructor returned a bad object, exit now.
if (! result.mSearchable) {
return null;
@ -621,7 +426,7 @@ public final class SearchableInfo implements Parcelable {
}
AttributeSet attr = Xml.asAttributeSet(xml);
if (attr != null) {
ActionKeyInfo keyInfo = result.new ActionKeyInfo(context, attr,
ActionKeyInfo keyInfo = new ActionKeyInfo(activityContext, attr,
result.mActionKeyList);
// only add to list if it is was useable
if (keyInfo.mKeyCode != 0) {
@ -637,6 +442,7 @@ public final class SearchableInfo implements Parcelable {
} catch (IOException e) {
throw new RuntimeException(e);
}
return result;
}
@ -756,16 +562,6 @@ public final class SearchableInfo implements Parcelable {
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.
*/

View File

@ -0,0 +1,243 @@
/*
* Copyright (C) 2009 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 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.ResolveInfo;
import android.os.Bundle;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
/**
* This class maintains the information about all searchable activities.
*/
public class Searchables {
// 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_SEARCHABLE_SYSTEM_SEARCH = "*";
private Context mContext;
private HashMap<ComponentName, SearchableInfo> mSearchablesMap = null;
private ArrayList<SearchableInfo> mSearchablesList = null;
private SearchableInfo mDefaultSearchable = null;
/**
*
* @param context Context to use for looking up activities etc.
*/
public Searchables (Context context) {
mContext = context;
}
/**
* 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 SearchableInfo getSearchableInfo(ComponentName activity) {
// Step 1. Is the result already hashed? (case 1)
SearchableInfo result;
synchronized (this) {
result = mSearchablesMap.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;
try {
ai = mContext.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 (this) {
result = mSearchablesMap.get(referredActivity);
if (result != null) {
mSearchablesMap.put(activity, result);
return result;
}
}
}
} catch (PackageManager.NameNotFoundException e) {
// case 3: no metadata
}
// Step 3. None found. Return null.
return null;
}
/**
* Set the default searchable activity (when none is specified).
*/
public synchronized void setDefaultSearchable(ComponentName activity) {
SearchableInfo si = null;
if (activity != null) {
si = getSearchableInfo(activity);
if (si != null) {
// move to front of list
mSearchablesList.remove(si);
mSearchablesList.add(0, si);
}
}
mDefaultSearchable = 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 synchronized SearchableInfo getDefaultSearchable() {
return mDefaultSearchable;
}
public synchronized boolean isDefaultSearchable(SearchableInfo searchable) {
return searchable == mDefaultSearchable;
}
/**
* 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.
*/
public void buildSearchableList() {
// 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 = mContext.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;
SearchableInfo searchable = SearchableInfo.getActivityMetaData(mContext, ai);
if (searchable != null) {
newSearchablesList.add(searchable);
newSearchablesMap.put(searchable.mSearchActivity, searchable);
}
}
}
// record the final values as a coherent pair
synchronized (this) {
mSearchablesList = newSearchablesList;
mSearchablesMap = newSearchablesMap;
}
}
/**
* Returns the list of searchable activities.
*/
public synchronized ArrayList<SearchableInfo> getSearchablesList() {
ArrayList<SearchableInfo> result = new ArrayList<SearchableInfo>(mSearchablesList);
return result;
}
}

View File

@ -983,18 +983,6 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
mSelectorRect.setEmpty();
invalidate();
}
/**
* The list is empty and we need to change the layout, so *really* clear everything out.
* @hide - for AutoCompleteTextView & SearchDialog only
*/
/* package */ void resetListAndClearViews() {
rememberSyncState();
removeAllViewsInLayout();
mRecycler.clear();
mRecycler.setViewTypeCount(mAdapter.getViewTypeCount());
requestLayout();
}
@Override
protected int computeVerticalScrollExtent() {

View File

@ -110,6 +110,10 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
private final DropDownItemClickListener mDropDownItemClickListener =
new DropDownItemClickListener();
private boolean mDropDownAlwaysVisible = false;
private boolean mDropDownDismissedOnCompletion = true;
private int mLastKeyCode = KeyEvent.KEYCODE_UNKNOWN;
private boolean mOpenBefore;
@ -211,6 +215,8 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
* {@link ViewGroup.LayoutParams#WRAP_CONTENT} to fit the width of its anchor view.</p>
*
* @return the width for the drop down list
*
* @attr ref android.R.styleable#AutoCompleteTextView_dropDownWidth
*/
public int getDropDownWidth() {
return mDropDownWidth;
@ -222,6 +228,8 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
* {@link ViewGroup.LayoutParams#WRAP_CONTENT} to fit the width of its anchor view.</p>
*
* @param width the width to use
*
* @attr ref android.R.styleable#AutoCompleteTextView_dropDownWidth
*/
public void setDropDownWidth(int width) {
mDropDownWidth = width;
@ -231,6 +239,8 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
* <p>Returns the id for the view that the auto-complete drop down list is anchored to.</p>
*
* @return the view's id, or {@link View#NO_ID} if none specified
*
* @attr ref android.R.styleable#AutoCompleteTextView_dropDownAnchor
*/
public int getDropDownAnchor() {
return mDropDownAnchorId;
@ -242,12 +252,172 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
* loading a view which is not yet instantiated.</p>
*
* @param id the id to anchor the drop down list view to
*
* @attr ref android.R.styleable#AutoCompleteTextView_dropDownAnchor
*/
public void setDropDownAnchor(int id) {
mDropDownAnchorId = id;
mDropDownAnchorView = null;
}
/**
* <p>Gets the background of the auto-complete drop-down list.</p>
*
* @return the background drawable
*
* @attr ref android.R.styleable#PopupWindow_popupBackground
*
* @hide Pending API council approval
*/
public Drawable getDropDownBackground() {
return mPopup.getBackground();
}
/**
* <p>Sets the background of the auto-complete drop-down list.</p>
*
* @param d the drawable to set as the background
*
* @attr ref android.R.styleable#PopupWindow_popupBackground
*
* @hide Pending API council approval
*/
public void setDropDownBackgroundDrawable(Drawable d) {
mPopup.setBackgroundDrawable(d);
}
/**
* <p>Sets the background of the auto-complete drop-down list.</p>
*
* @param id the id of the drawable to set as the background
*
* @attr ref android.R.styleable#PopupWindow_popupBackground
*
* @hide Pending API council approval
*/
public void setDropDownBackgroundResource(int id) {
mPopup.setBackgroundDrawable(getResources().getDrawable(id));
}
/**
* <p>Sets the animation style of the auto-complete drop-down list.</p>
*
* <p>If the drop-down is showing, calling this method will take effect only
* the next time the drop-down is shown.</p>
*
* @param animationStyle animation style to use when the drop-down appears
* and disappears. Set to -1 for the default animation, 0 for no
* animation, or a resource identifier for an explicit animation.
*
* @hide Pending API council approval
*/
public void setDropDownAnimationStyle(int animationStyle) {
mPopup.setAnimationStyle(animationStyle);
}
/**
* <p>Returns the animation style that is used when the drop-down list appears and disappears
* </p>
*
* @return the animation style that is used when the drop-down list appears and disappears
*
* @hide Pending API council approval
*/
public int getDropDownAnimationStyle() {
return mPopup.getAnimationStyle();
}
/**
* <p>Sets the vertical offset used for the auto-complete drop-down list.</p>
*
* @param offset the vertical offset
*
* @hide Pending API council approval
*/
public void setDropDownVerticalOffset(int offset) {
mDropDownVerticalOffset = offset;
}
/**
* <p>Gets the vertical offset used for the auto-complete drop-down list.</p>
*
* @return the vertical offset
*
* @hide Pending API council approval
*/
public int getDropDownVerticalOffset() {
return mDropDownVerticalOffset;
}
/**
* <p>Sets the horizontal offset used for the auto-complete drop-down list.</p>
*
* @param offset the horizontal offset
*
* @hide Pending API council approval
*/
public void setDropDownHorizontalOffset(int offset) {
mDropDownHorizontalOffset = offset;
}
/**
* <p>Gets the horizontal offset used for the auto-complete drop-down list.</p>
*
* @return the horizontal offset
*
* @hide Pending API council approval
*/
public int getDropDownHorizontalOffset() {
return mDropDownHorizontalOffset;
}
/**
* @return Whether the drop-down is visible as long as there is {@link #enoughToFilter()}
*
* @hide Pending API council approval
*/
public boolean isDropDownAlwaysVisible() {
return mDropDownAlwaysVisible;
}
/**
* Sets whether the drop-down should remain visible as long as there is there is
* {@link #enoughToFilter()}. This is useful if an unknown number of results are expected
* to show up in the adapter sometime in the future.
*
* The drop-down will occupy the entire screen below {@link #getDropDownAnchor} regardless
* of the size or content of the list. {@link #getDropDownBackground()} will fill any space
* that is not used by the list.
*
* @param dropDownAlwaysVisible Whether to keep the drop-down visible.
*
* @hide Pending API council approval
*/
public void setDropDownAlwaysVisible(boolean dropDownAlwaysVisible) {
mDropDownAlwaysVisible = dropDownAlwaysVisible;
}
/**
* Checks whether the drop-down is dismissed when a suggestion is clicked.
*
* @hide Pending API council approval
*/
public boolean isDropDownDismissedOnCompletion() {
return mDropDownDismissedOnCompletion;
}
/**
* Sets whether the drop-down is dismissed when a suggestion is clicked. This is
* true by default.
*
* @param dropDownDismissedOnCompletion Whether to dismiss the drop-down.
*
* @hide Pending API council approval
*/
public void setDropDownDismissedOnCompletion(boolean dropDownDismissedOnCompletion) {
mDropDownDismissedOnCompletion = dropDownDismissedOnCompletion;
}
/**
* <p>Returns the number of characters the user must type before the drop
* down list is shown.</p>
@ -628,16 +798,6 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
}
return ListView.INVALID_POSITION;
}
/**
* We're changing the adapter and its views so really, really clear everything out
* @hide - for SearchDialog only
*/
public void resetListAndClearViews() {
if (mDropDownList != null) {
mDropDownList.resetListAndClearViews();
}
}
/**
* <p>Starts filtering the content of the drop down list. The filtering
@ -709,7 +869,9 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
}
}
dismissDropDown();
if (mDropDownDismissedOnCompletion) {
dismissDropDown();
}
}
/**
@ -720,6 +882,42 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
return mBlockCompletion;
}
/**
* Like {@link #setText(CharSequence)}, except that it can disable filtering.
*
* @param filter If <code>false</code>, no filtering will be performed
* as a result of this call.
*
* @hide Pending API council approval.
*/
public void setText(CharSequence text, boolean filter) {
if (filter) {
setText(text);
} else {
mBlockCompletion = true;
setText(text);
mBlockCompletion = false;
}
}
/**
* Like {@link #setTextKeepState(CharSequence)}, except that it can disable filtering.
*
* @param filter If <code>false</code>, no filtering will be performed
* as a result of this call.
*
* @hide Pending API council approval.
*/
public void setTextKeepState(CharSequence text, boolean filter) {
if (filter) {
setTextKeepState(text);
} else {
mBlockCompletion = true;
setTextKeepState(text);
mBlockCompletion = false;
}
}
/**
* <p>Performs the text completion by replacing the current text by the
* selected item. Subclasses should override this method to avoid replacing
@ -734,6 +932,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
Selection.setSelection(spannable, spannable.length());
}
/** {@inheritDoc} */
public void onFilterComplete(int count) {
if (mAttachCount <= 0) return;
@ -744,7 +943,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
* to filter.
*/
if (count > 0 && enoughToFilter()) {
if ((count > 0 || mDropDownAlwaysVisible) && enoughToFilter()) {
if (hasFocus() && hasWindowFocus()) {
showDropDown();
}
@ -808,22 +1007,6 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
return result;
}
/**
* Set the horizontal offset with respect to {@link #setDropDownAnchor(int)}
* @hide pending API council review
*/
public void setDropDownHorizontalOffset(int horizontalOffset) {
mDropDownHorizontalOffset = horizontalOffset;
}
/**
* Set the vertical offset with respect to {@link #setDropDownAnchor(int)}
* @hide pending API council review
*/
public void setDropDownVerticalOffset(int verticalOffset) {
mDropDownVerticalOffset = verticalOffset;
}
/**
* <p>Used for lazy instantiation of the anchor view from the id we have. If the value of
* the id is NO_ID or we can't find a view for the given id, we return this TextView as
@ -856,10 +1039,9 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
mDropDownVerticalOffset, widthSpec, height);
} else {
if (mDropDownWidth == ViewGroup.LayoutParams.FILL_PARENT) {
mPopup.setWindowLayoutMode(ViewGroup.LayoutParams.FILL_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
mPopup.setWindowLayoutMode(ViewGroup.LayoutParams.FILL_PARENT, 0);
} else {
mPopup.setWindowLayoutMode(0, ViewGroup.LayoutParams.WRAP_CONTENT);
mPopup.setWindowLayoutMode(0, 0);
if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) {
mPopup.setWidth(getDropDownAnchorView().getWidth());
} else {
@ -966,8 +1148,10 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
final int maxHeight = mPopup.getMaxAvailableHeight(this, mDropDownVerticalOffset);
//otherHeights += dropDownView.getPaddingTop() + dropDownView.getPaddingBottom();
return mDropDownList.measureHeightOfChildren(MeasureSpec.UNSPECIFIED,
final int measuredHeight = mDropDownList.measureHeightOfChildren(MeasureSpec.UNSPECIFIED,
0, ListView.NO_POSITION, maxHeight - otherHeights, 2) + otherHeights;
return mDropDownAlwaysVisible ? maxHeight : measuredHeight;
}
private View getHintView(Context context) {

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2009 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.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- TODO Need different assets for some of these button states. -->
 <item android:state_window_focused="false" android:state_enabled="true"
android:drawable="@drawable/btn_global_search_normal" />
 <item android:state_window_focused="false" android:state_enabled="false"
android:drawable="@drawable/btn_global_search_normal" />
<item android:state_pressed="true"
android:drawable="@drawable/btn_default_pressed" />
<item android:state_focused="true" android:state_enabled="true"
android:drawable="@drawable/btn_default_selected" />
<item android:state_enabled="true"
android:drawable="@drawable/btn_global_search_normal" />
<item android:state_focused="true"
android:drawable="@drawable/btn_global_search_normal" />
<item
android:drawable="@drawable/btn_global_search_normal" />
</selector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 676 B

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2009 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.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
 <item android:state_window_focused="false" android:state_enabled="true"
android:drawable="@drawable/btn_search_dialog_default" />
<item android:state_pressed="true"
android:drawable="@drawable/btn_search_dialog_pressed" />
<item android:state_focused="true" android:state_enabled="true"
android:drawable="@drawable/btn_search_dialog_selected" />
<item android:state_enabled="true"
android:drawable="@drawable/btn_search_dialog_default" />
<item
android:drawable="@drawable/btn_search_dialog_default" />
</selector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2009 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.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
 <item android:state_window_focused="false" android:state_enabled="true"
android:drawable="@drawable/btn_search_dialog_voice_default" />
<item android:state_pressed="true"
android:drawable="@drawable/btn_search_dialog_voice_pressed" />
<item android:state_focused="true" android:state_enabled="true"
android:drawable="@drawable/btn_search_dialog_voice_selected" />
<item android:state_enabled="true"
android:drawable="@drawable/btn_search_dialog_voice_default" />
<item
android:drawable="@drawable/btn_search_dialog_voice_default" />
</selector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 B

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2009 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.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
 <item android:state_window_focused="false" android:state_enabled="true"
android:drawable="@drawable/textfield_search_default" />
<item android:state_pressed="true"
android:drawable="@drawable/textfield_search_pressed" />
<item android:state_enabled="true" android:state_focused="true"
android:drawable="@drawable/textfield_search_selected" />
<item android:drawable="@drawable/textfield_search_default" />
</selector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -23,7 +23,7 @@
android:minHeight="?android:attr/listPreferredItemHeight"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:paddingLeft="14dip"
android:paddingLeft="10dip"
android:paddingRight="15dip">
<!-- Activity icon when presenting dialog -->
@ -42,13 +42,13 @@
android:textAppearance="?android:attr/textAppearanceLargeInverse"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="6dip" />
android:paddingLeft="10dip" />
<!-- Extended activity info to distinguish between duplicate activity names -->
<TextView android:id="@android:id/text2"
android:textAppearance="?android:attr/textAppearanceMediumInverse"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="6dip" />
android:paddingLeft="10dip" />
</LinearLayout>
</LinearLayout>

View File

@ -26,79 +26,67 @@
android:orientation="vertical"
android:focusable="true"
android:descendantFocusability="afterDescendants">
<!-- android:paddingBottom="14dip" TODO MUST FIX - it's a hack to get the popup to show -->
<!-- android:paddingBottom="200dip" TODO MUST FIX - it's a hack to get the popup to show -->
<!-- Outer layout defines the entire search bar at the top of the screen -->
<!-- Bottom padding of 16 is due to the graphic, with 9 extra pixels of drop
shadow, plus the desired padding of "8" against the user-visible (grey)
pixels, minus "1" to correct for positioning of the edittext & button. -->
<LinearLayout
android:id="@+id/search_plate"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingLeft="8dip"
android:paddingRight="8dip"
android:paddingTop="6dip"
android:paddingLeft="12dip"
android:paddingRight="12dip"
android:paddingTop="7dip"
android:paddingBottom="16dip"
android:baselineAligned="false"
android:background="@android:drawable/search_plate"
android:addStatesFromChildren="true" >
android:background="@drawable/search_plate_global" >
<!-- This is actually used for the badge icon *or* the badge label (or neither) -->
<TextView
android:id="@+id/search_badge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="2dip"
android:layout_marginBottom="2dip"
android:drawablePadding="0dip"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorPrimary" />
android:textColor="?android:attr/textColorPrimaryInverse" />
<!-- Inner layout contains the button(s) and EditText -->
<!-- The layout_marginTop of "1" corrects for the extra 1 pixel of padding at the top of
textfield_selected.9.png. The "real" margin as displayed is "2". -->
<!-- The layout_marginBottom of "-5" corrects for the spacing we see at the
bottom of the edittext and button images. The "real" margin as displayed is "8" -->
<LinearLayout
android:id="@+id/search_edit_frame"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginTop="1dip"
android:layout_marginBottom="-5dip"
android:orientation="horizontal"
android:addStatesFromChildren="true"
android:gravity="center_vertical"
android:baselineAligned="false" >
android:orientation="horizontal">
<view class="android.app.SearchDialog$SearchAutoComplete"
android:id="@+id/search_src_text"
android:background="@drawable/textfield_search"
android:layout_height="wrap_content"
android:layout_width="0dip"
android:layout_weight="1.0"
android:paddingLeft="8dip"
android:paddingRight="6dip"
android:singleLine="true"
android:inputType="text|textAutoComplete"
android:dropDownWidth="fill_parent"
android:dropDownAnchor="@id/search_plate"
android:dropDownVerticalOffset="-15dip"
android:dropDownVerticalOffset="-9dip"
android:popupBackground="@android:drawable/search_dropdown_background"
/>
<!-- android:focusableInTouchMode="false" -->
<!-- android:singleLine="true" -->
<!-- android:selectAllOnFocus="true" -->
<!-- This button can switch between text and icon "modes" -->
<Button
android:id="@+id/search_go_btn"
android:layout_marginLeft="1dip"
android:background="@drawable/btn_search_dialog"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawableLeft="@android:drawable/ic_btn_search"
android:layout_height="fill_parent"
/>
<ImageButton android:id="@+id/search_voice_btn"
<ImageButton
android:id="@+id/search_voice_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_height="fill_parent"
android:layout_marginLeft="8dip"
android:background="@drawable/btn_search_dialog_voice"
android:src="@android:drawable/ic_btn_speak_now"
/>
</LinearLayout>

View File

@ -20,7 +20,7 @@
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@android:id/text1"
style="?android:attr/dropDownItemStyle"
android:textAppearance="?android:attr/textAppearanceMediumInverse"
android:textAppearance="?android:attr/textAppearanceSearchResultTitle"
android:singleLine="true"
android:layout_width="fill_parent"
android:layout_height="?android:attr/listPreferredItemHeight" />
android:layout_height="?android:attr/searchResultListItemHeight" />

View File

@ -20,15 +20,16 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="?android:attr/listPreferredItemHeight"
android:layout_height="?android:attr/searchResultListItemHeight"
android:orientation="horizontal"
android:gravity="center_vertical"
android:baselineAligned="false"
>
<TwoLineListItem
android:paddingTop="2dip"
android:paddingBottom="2dip"
android:paddingTop="1dip"
android:paddingBottom="1dip"
android:gravity="center_vertical"
android:layout_width="0dip"
android:layout_weight="1"
android:layout_height="wrap_content"
@ -37,7 +38,7 @@
<TextView
android:id="@android:id/text1"
style="?android:attr/dropDownItemStyle"
android:textAppearance="?android:attr/textAppearanceMediumInverse"
android:textAppearance="?android:attr/textAppearanceSearchResultTitle"
android:singleLine="true"
android:layout_width="fill_parent"
android:layout_height="wrap_content" />
@ -45,7 +46,7 @@
<TextView
android:id="@android:id/text2"
style="?android:attr/dropDownItemStyle"
android:textAppearance="?android:attr/textAppearanceSmallInverse"
android:textAppearance="?android:attr/textAppearanceSearchResultSubtitle"
android:textColor="?android:attr/textColorSecondaryInverse"
android:singleLine="true"
android:layout_width="fill_parent"

View File

@ -22,31 +22,33 @@
<!-- of the text element in apps/common/res/layout/simple_dropdown_item_1line.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:paddingLeft="4dip"
android:paddingRight="2dip"
android:layout_width="fill_parent"
android:layout_height="?android:attr/listPreferredItemHeight"
android:layout_height="?android:attr/searchResultListItemHeight"
android:orientation="horizontal"
android:gravity="center_vertical"
android:baselineAligned="false"
>
<ImageView android:id="@android:id/icon1"
android:layout_width="32dip"
android:layout_height="32dip"
android:layout_width="48dip"
android:layout_height="48dip"
android:layout_gravity="center_vertical"
android:scaleType="fitCenter" />
android:scaleType="centerInside" />
<TextView android:id="@android:id/text1"
style="?android:attr/dropDownItemStyle"
android:textAppearance="?android:attr/textAppearanceMediumInverse"
android:textAppearance="?android:attr/textAppearanceSearchResultTitle"
android:singleLine="true"
android:layout_height="wrap_content"
android:layout_width="0dip"
android:layout_weight="1" />
<ImageView android:id="@android:id/icon2"
android:layout_width="32dip"
android:layout_height="32dip"
android:layout_width="48dip"
android:layout_height="48dip"
android:layout_gravity="center_vertical"
android:scaleType="fitCenter" />
android:scaleType="centerInside" />
</LinearLayout>

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/* //device/apps/common/assets/res/any/layout/simple_spinner_item.xml
/*
**
** Copyright 2008, The Android Open Source Project
**
@ -18,56 +18,62 @@
*/
-->
<!-- NOTE: The appearance of the inner text element must match the appearance -->
<!-- of the text element in apps/common/res/layout/simple_dropdown_item_2line.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:paddingLeft="4dip"
android:paddingRight="2dip"
android:layout_width="fill_parent"
android:layout_height="?android:attr/listPreferredItemHeight"
android:orientation="horizontal"
android:gravity="center_vertical"
android:baselineAligned="false"
>
<ImageView android:id="@android:id/icon1"
android:layout_width="32dip"
android:layout_height="32dip"
android:layout_gravity="center_vertical"
android:scaleType="fitCenter" />
android:layout_height="?android:attr/searchResultListItemHeight" >
<TwoLineListItem
android:paddingTop="2dip"
android:paddingBottom="2dip"
android:layout_width="0dip"
android:layout_weight="1"
android:layout_height="wrap_content"
android:mode="twoLine" >
<TextView
android:id="@android:id/text1"
style="?android:attr/dropDownItemStyle"
android:textAppearance="?android:attr/textAppearanceMediumInverse"
android:singleLine="true"
android:layout_width="fill_parent"
android:layout_height="wrap_content" />
<TextView
android:id="@android:id/text2"
style="?android:attr/dropDownItemStyle"
android:textAppearance="?android:attr/textAppearanceSmallInverse"
android:textColor="?android:attr/textColorSecondaryInverse"
android:singleLine="true"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_below="@android:id/text1"
android:layout_alignLeft="@android:id/text1" />
</TwoLineListItem>
<!-- Icons come first in the layout, since their placement doesn't depend on
the placement of the text views. -->
<ImageView android:id="@android:id/icon1"
android:layout_width="48dip"
android:layout_height="48dip"
android:scaleType="centerInside"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:layout_alignParentBottom="true"
android:visibility="gone" />
<ImageView android:id="@android:id/icon2"
android:layout_width="32dip"
android:layout_height="32dip"
android:layout_gravity="center_vertical"
android:scaleType="fitCenter" />
android:layout_width="48dip"
android:layout_height="48dip"
android:scaleType="centerInside"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:layout_alignParentBottom="true"
android:visibility="gone" />
</LinearLayout>
<!-- The subtitle comes before the title, since the height of the title depends on whether the
subtitle is visible or gone. -->
<TextView android:id="@android:id/text2"
style="?android:attr/dropDownItemStyle"
android:textAppearance="?android:attr/textAppearanceSearchResultSubtitle"
android:singleLine="true"
android:layout_width="fill_parent"
android:layout_height="29dip"
android:paddingBottom="4dip"
android:gravity="top"
android:layout_toRightOf="@android:id/icon1"
android:layout_toLeftOf="@android:id/icon2"
android:layout_alignWithParentIfMissing="true"
android:layout_alignParentBottom="true"
android:visibility="gone" />
<!-- The title is placed above the subtitle, if there is one. If there is no
subtitle, it fills the parent. -->
<TextView android:id="@android:id/text1"
style="?android:attr/dropDownItemStyle"
android:textAppearance="?android:attr/textAppearanceSearchResultTitle"
android:singleLine="true"
android:layout_width="fill_parent"
android:layout_height="29dip"
android:paddingTop="4dip"
android:gravity="center_vertical"
android:layout_alignParentTop="true"
android:layout_toRightOf="@android:id/icon1"
android:layout_toLeftOf="@android:id/icon2"
android:layout_above="@android:id/text2"
android:layout_alignWithParentIfMissing="true" />
</RelativeLayout>

View File

@ -94,6 +94,11 @@
<!-- Text color, typeface, size, and style for "small" inverse text. Defaults to secondary inverse text color. -->
<attr name="textAppearanceSmallInverse" format="reference" />
<!-- Text color, typeface, size, and style for system search result title. Defaults to primary inverse text color. @hide -->
<attr name="textAppearanceSearchResultTitle" format="reference" />
<!-- Text color, typeface, size, and style for system search result subtitle. Defaults to primary inverse text color. @hide -->
<attr name="textAppearanceSearchResultSubtitle" format="reference" />
<!-- Text color, typeface, size, and style for the text inside of a button. -->
<attr name="textAppearanceButton" format="reference" />
@ -147,6 +152,8 @@
<!-- The preferred list item height -->
<attr name="listPreferredItemHeight" format="dimension" />
<!-- The drawable for the list divider -->
<!-- The list item height for search results. @hide -->
<attr name="searchResultListItemHeight" format="dimension" />
<attr name="listDivider" format="reference" />
<!-- TextView style for list separators. -->
<attr name="listSeparatorTextViewStyle" format="reference" />

View File

@ -138,6 +138,7 @@
</style>
<!-- Window animations that are applied to the search bar overlay window.
Previously used, but currently unused.
{@hide Pending API council approval} -->
<style name="Animation.SearchBar">
<item name="windowEnterAnimation">@anim/search_bar_enter</item>
@ -574,6 +575,24 @@
<item name="android:textColor">@android:color/primary_text_light_disable_only</item>
</style>
<!-- @hide -->
<style name="TextAppearance.SearchResult">
<item name="android:textStyle">normal</item>
<item name="android:textColor">?textColorPrimaryInverse</item>
<item name="android:textColorHint">?textColorHintInverse</item>
</style>
<!-- @hide -->
<style name="TextAppearance.SearchResult.Title">
<item name="android:textSize">16sp</item>
</style>
<!-- @hide -->
<style name="TextAppearance.SearchResult.Subtitle">
<item name="android:textSize">13sp</item>
<item name="android:textColor">?textColorSecondaryInverse</item>
</style>
<style name="TextAppearance.WindowTitle">
<item name="android:textColor">#fff</item>
<item name="android:textSize">14sp</item>

View File

@ -57,6 +57,12 @@
<item name="textAppearanceLargeInverse">@android:style/TextAppearance.Large.Inverse</item>
<item name="textAppearanceMediumInverse">@android:style/TextAppearance.Medium.Inverse</item>
<item name="textAppearanceSmallInverse">@android:style/TextAppearance.Small.Inverse</item>
<!-- @hide -->
<item name="textAppearanceSearchResultTitle">@android:style/TextAppearance.SearchResult.Title</item>
<!-- @hide -->
<item name="textAppearanceSearchResultSubtitle">@android:style/TextAppearance.SearchResult.Subtitle</item>
<item name="textAppearanceButton">@android:style/TextAppearance.Widget.Button</item>
@ -75,6 +81,8 @@
<!-- List attributes -->
<item name="listPreferredItemHeight">64dip</item>
<!-- @hide -->
<item name="searchResultListItemHeight">58dip</item>
<item name="listDivider">@drawable/divider_horizontal_dark</item>
<item name="listSeparatorTextViewStyle">@android:style/Widget.TextView.ListSeparator</item>
@ -355,7 +363,6 @@
<!-- Theme for the search input bar. -->
<style name="Theme.SearchBar" parent="Theme.Panel">
<item name="android:backgroundDimEnabled">true</item>
<item name="android:windowAnimationStyle">@android:style/Animation.SearchBar</item>
<item name="windowContentOverlay">@null</item>
</style>

View File

@ -23,27 +23,11 @@ import android.app.ISearchManager;
import android.app.SearchManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.content.res.XmlResourceParser;
import android.os.ServiceManager;
import android.server.search.SearchableInfo;
import android.server.search.SearchableInfo.ActionKeyInfo;
import android.test.ActivityInstrumentationTestCase;
import android.test.MoreAsserts;
import android.test.mock.MockContext;
import android.test.mock.MockPackageManager;
import android.test.ActivityInstrumentationTestCase2;
import android.test.suitebuilder.annotation.LargeTest;
import android.test.suitebuilder.annotation.MediumTest;
import android.util.AndroidRuntimeException;
import android.view.KeyEvent;
import java.util.ArrayList;
import java.util.List;
/**
* To launch this test from the command line:
@ -52,7 +36,7 @@ import java.util.List;
* -e class com.android.unit_tests.SearchManagerTest \
* com.android.unit_tests/android.test.InstrumentationTestRunner
*/
public class SearchManagerTest extends ActivityInstrumentationTestCase<LocalActivity> {
public class SearchManagerTest extends ActivityInstrumentationTestCase2<LocalActivity> {
// If non-zero, enable a set of tests that start and stop the search manager.
// This is currently disabled because it's causing an unwanted jump from the unit test
@ -71,18 +55,6 @@ public class SearchManagerTest extends ActivityInstrumentationTestCase<LocalActi
* testSearchManagerInvocations()
* FIX - make it work again
* stress test with a very long string
*
* SearchableInfo tests
* Mock the context so I can provide very specific input data
* Confirm OK with "zero" searchables
* Confirm "good" metadata read properly
* Confirm "bad" metadata skipped properly
* Confirm ordering of searchables
* Confirm "good" actionkeys
* confirm "bad" actionkeys are rejected
* confirm XML ordering enforced (will fail today - bug in SearchableInfo)
* findActionKey works
* getIcon works
*
* SearchManager tests
* confirm proper identification of "default" activity based on policy, not hardcoded contacts
@ -195,348 +167,6 @@ public class SearchManagerTest extends ActivityInstrumentationTestCase<LocalActi
searchManager.stopSearch();
}
}
/**
* The goal of this test is to confirm proper operation of the
* SearchableInfo helper class.
*
* TODO: The metadata source needs to be mocked out because adding
* searchability metadata via this test is causing it to leak into the
* real system. So for now I'm just going to test for existence of the
* GoogleSearch app (which is searchable).
*/
@LargeTest
public void testSearchableGoogleSearch() {
// test basic array & hashmap
SearchableInfo.buildSearchableList(mContext);
// test linkage from another activity
// TODO inject this via mocking into the package manager.
// TODO for now, just check for searchable GoogleSearch app (this isn't really a unit test)
ComponentName thisActivity = new ComponentName(
"com.android.googlesearch",
"com.android.googlesearch.GoogleSearch");
SearchableInfo si = SearchableInfo.getSearchableInfo(mContext, thisActivity);
assertNotNull(si);
assertTrue(si.mSearchable);
assertEquals(thisActivity, si.mSearchActivity);
Context appContext = si.getActivityContext(mContext);
assertNotNull(appContext);
MoreAsserts.assertNotEqual(appContext, mContext);
assertEquals("Google Search", appContext.getString(si.getHintId()));
assertEquals("Google", appContext.getString(si.getLabelId()));
}
/**
* Test that non-searchable activities return no searchable info (this would typically
* trigger the use of the default searchable e.g. contacts)
*/
@LargeTest
public void testNonSearchable() {
// test basic array & hashmap
SearchableInfo.buildSearchableList(mContext);
// confirm that we return null for non-searchy activities
ComponentName nonActivity = new ComponentName(
"com.android.unit_tests",
"com.android.unit_tests.NO_SEARCH_ACTIVITY");
SearchableInfo si = SearchableInfo.getSearchableInfo(mContext, nonActivity);
assertNull(si);
}
/**
* This is an attempt to run the searchable info list with a mocked context. Here are some
* things I'd like to test.
*
* Confirm OK with "zero" searchables
* Confirm "good" metadata read properly
* Confirm "bad" metadata skipped properly
* Confirm ordering of searchables
* Confirm "good" actionkeys
* confirm "bad" actionkeys are rejected
* confirm XML ordering enforced (will fail today - bug in SearchableInfo)
* findActionKey works
* getIcon works
*/
@LargeTest
public void testSearchableMocked() {
MyMockPackageManager mockPM = new MyMockPackageManager(mContext.getPackageManager());
MyMockContext mockContext = new MyMockContext(mContext, mockPM);
ArrayList<SearchableInfo> searchables;
int count;
// build item list with real-world source data
mockPM.setSearchablesMode(MyMockPackageManager.SEARCHABLES_PASSTHROUGH);
SearchableInfo.buildSearchableList(mockContext);
// tests with "real" searchables (deprecate, this should be a unit test)
searchables = SearchableInfo.getSearchablesList();
count = searchables.size();
assertTrue(count >= 1); // this isn't really a unit test
checkSearchables(searchables);
// build item list with mocked search data
// this round of tests confirms good operations with "zero" searchables found
// This should return either a null pointer or an empty list
mockPM.setSearchablesMode(MyMockPackageManager.SEARCHABLES_MOCK_ZERO);
SearchableInfo.buildSearchableList(mockContext);
searchables = SearchableInfo.getSearchablesList();
if (searchables != null) {
count = searchables.size();
assertTrue(count == 0);
}
}
/**
* Generic health checker for an array of searchables.
*
* This is designed to pass for any semi-legal searchable, without knowing much about
* the format of the underlying data. It's fairly easy for a non-compliant application
* to provide meta-data that will pass here (e.g. a non-existent suggestions authority).
*
* @param searchables The list of searchables to examine.
*/
private void checkSearchables(ArrayList<SearchableInfo> searchablesList) {
assertNotNull(searchablesList);
int count = searchablesList.size();
for (int ii = 0; ii < count; ii++) {
SearchableInfo si = searchablesList.get(ii);
assertNotNull(si);
assertTrue(si.mSearchable);
assertTrue(si.getLabelId() != 0); // This must be a useable string
assertNotEmpty(si.mSearchActivity.getClassName());
assertNotEmpty(si.mSearchActivity.getPackageName());
if (si.getSuggestAuthority() != null) {
// The suggestion fields are largely optional, so we'll just confirm basic health
assertNotEmpty(si.getSuggestAuthority());
assertNullOrNotEmpty(si.getSuggestPath());
assertNullOrNotEmpty(si.getSuggestSelection());
assertNullOrNotEmpty(si.getSuggestIntentAction());
assertNullOrNotEmpty(si.getSuggestIntentData());
}
/* Add a way to get the entire action key list, then explicitly test its elements */
/* For now, test the most common action key (CALL) */
ActionKeyInfo ai = si.findActionKey(KeyEvent.KEYCODE_CALL);
if (ai != null) {
assertEquals(ai.mKeyCode, KeyEvent.KEYCODE_CALL);
// one of these three fields must be non-null & non-empty
boolean m1 = (ai.mQueryActionMsg != null) && (ai.mQueryActionMsg.length() > 0);
boolean m2 = (ai.mSuggestActionMsg != null) && (ai.mSuggestActionMsg.length() > 0);
boolean m3 = (ai.mSuggestActionMsgColumn != null) &&
(ai.mSuggestActionMsgColumn.length() > 0);
assertTrue(m1 || m2 || m3);
}
/*
* Find ways to test these:
*
* private int mSearchMode
* private Drawable mIcon
*/
/*
* Explicitly not tested here:
*
* Can be null, so not much to see:
* public String mSearchHint
* private String mZeroQueryBanner
*
* To be deprecated/removed, so don't bother:
* public boolean mFilterMode
* public boolean mQuickStart
* private boolean mIconResized
* private int mIconResizeWidth
* private int mIconResizeHeight
*
* All of these are "internal" working variables, not part of any contract
* private ActivityInfo mActivityInfo
* private Rect mTempRect
* private String mSuggestProviderPackage
* private String mCacheActivityContext
*/
}
}
/**
* Combo assert for "string not null and not empty"
*/
private void assertNotEmpty(final String s) {
assertNotNull(s);
MoreAsserts.assertNotEqual(s, "");
}
/**
* Combo assert for "string null or (not null and not empty)"
*/
private void assertNullOrNotEmpty(final String s) {
if (s != null) {
MoreAsserts.assertNotEqual(s, "");
}
}
/**
* This is a mock for context. Used to perform a true unit test on SearchableInfo.
*
*/
private class MyMockContext extends MockContext {
protected Context mRealContext;
protected PackageManager mPackageManager;
/**
* Constructor.
*
* @param realContext Please pass in a real context for some pass-throughs to function.
*/
MyMockContext(Context realContext, PackageManager packageManager) {
mRealContext = realContext;
mPackageManager = packageManager;
}
/**
* Resources. Pass through for now.
*/
@Override
public Resources getResources() {
return mRealContext.getResources();
}
/**
* Package manager. Pass through for now.
*/
@Override
public PackageManager getPackageManager() {
return mPackageManager;
}
/**
* Package manager. Pass through for now.
*/
@Override
public Context createPackageContext(String packageName, int flags)
throws PackageManager.NameNotFoundException {
return mRealContext.createPackageContext(packageName, flags);
}
}
/**
* This is a mock for package manager. Used to perform a true unit test on SearchableInfo.
*
*/
private class MyMockPackageManager extends MockPackageManager {
public final static int SEARCHABLES_PASSTHROUGH = 0;
public final static int SEARCHABLES_MOCK_ZERO = 1;
public final static int SEARCHABLES_MOCK_ONEGOOD = 2;
public final static int SEARCHABLES_MOCK_ONEGOOD_ONEBAD = 3;
protected PackageManager mRealPackageManager;
protected int mSearchablesMode;
public MyMockPackageManager(PackageManager realPM) {
mRealPackageManager = realPM;
mSearchablesMode = SEARCHABLES_PASSTHROUGH;
}
/**
* Set the mode for various tests.
*/
public void setSearchablesMode(int newMode) {
switch (newMode) {
case SEARCHABLES_PASSTHROUGH:
case SEARCHABLES_MOCK_ZERO:
mSearchablesMode = newMode;
break;
default:
throw new UnsupportedOperationException();
}
}
/**
* Find activities that support a given intent.
*
* Retrieve all activities that can be performed for the given intent.
*
* @param intent The desired intent as per resolveActivity().
* @param flags Additional option flags. The most important is
* MATCH_DEFAULT_ONLY, to limit the resolution to only
* those activities that support the CATEGORY_DEFAULT.
*
* @return A List<ResolveInfo> containing one entry for each matching
* Activity. These are ordered from best to worst match -- that
* is, the first item in the list is what is returned by
* resolveActivity(). If there are no matching activities, an empty
* list is returned.
*/
@Override
public List<ResolveInfo> queryIntentActivities(Intent intent, int flags) {
assertNotNull(intent);
assertEquals(intent.getAction(), Intent.ACTION_SEARCH);
switch (mSearchablesMode) {
case SEARCHABLES_PASSTHROUGH:
return mRealPackageManager.queryIntentActivities(intent, flags);
case SEARCHABLES_MOCK_ZERO:
return null;
default:
throw new UnsupportedOperationException();
}
}
/**
* Retrieve an XML file from a package. This is a low-level API used to
* retrieve XML meta data.
*
* @param packageName The name of the package that this xml is coming from.
* Can not be null.
* @param resid The resource identifier of the desired xml. Can not be 0.
* @param appInfo Overall information about <var>packageName</var>. This
* may be null, in which case the application information will be retrieved
* for you if needed; if you already have this information around, it can
* be much more efficient to supply it here.
*
* @return Returns an XmlPullParser allowing you to parse out the XML
* data. Returns null if the xml resource could not be found for any
* reason.
*/
@Override
public XmlResourceParser getXml(String packageName, int resid, ApplicationInfo appInfo) {
assertNotNull(packageName);
MoreAsserts.assertNotEqual(packageName, "");
MoreAsserts.assertNotEqual(resid, 0);
switch (mSearchablesMode) {
case SEARCHABLES_PASSTHROUGH:
return mRealPackageManager.getXml(packageName, resid, appInfo);
case SEARCHABLES_MOCK_ZERO:
default:
throw new UnsupportedOperationException();
}
}
/**
* Find a single content provider by its base path name.
*
* @param name The name of the provider to find.
* @param flags Additional option flags. Currently should always be 0.
*
* @return ContentProviderInfo Information about the provider, if found,
* else null.
*/
@Override
public ProviderInfo resolveContentProvider(String name, int flags) {
assertNotNull(name);
MoreAsserts.assertNotEqual(name, "");
assertEquals(flags, 0);
switch (mSearchablesMode) {
case SEARCHABLES_PASSTHROUGH:
return mRealPackageManager.resolveContentProvider(name, flags);
case SEARCHABLES_MOCK_ZERO:
default:
throw new UnsupportedOperationException();
}
}
}
}

View File

@ -0,0 +1,411 @@
/*
* Copyright (C) 2009 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 com.android.unit_tests;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.content.res.XmlResourceParser;
import android.server.search.SearchableInfo;
import android.server.search.Searchables;
import android.server.search.SearchableInfo.ActionKeyInfo;
import android.test.AndroidTestCase;
import android.test.MoreAsserts;
import android.test.mock.MockContext;
import android.test.mock.MockPackageManager;
import android.test.suitebuilder.annotation.SmallTest;
import android.view.KeyEvent;
import java.util.ArrayList;
import java.util.List;
/**
* To launch this test from the command line:
*
* adb shell am instrument -w \
* -e class com.android.unit_tests.SearchablesTest \
* com.android.unit_tests/android.test.InstrumentationTestRunner
*/
@SmallTest
public class SearchablesTest extends AndroidTestCase {
/*
* SearchableInfo tests
* Mock the context so I can provide very specific input data
* Confirm OK with "zero" searchables
* Confirm "good" metadata read properly
* Confirm "bad" metadata skipped properly
* Confirm ordering of searchables
* Confirm "good" actionkeys
* confirm "bad" actionkeys are rejected
* confirm XML ordering enforced (will fail today - bug in SearchableInfo)
* findActionKey works
* getIcon works
*/
/**
* The goal of this test is to confirm proper operation of the
* SearchableInfo helper class.
*
* TODO: The metadata source needs to be mocked out because adding
* searchability metadata via this test is causing it to leak into the
* real system. So for now I'm just going to test for existence of the
* GoogleSearch app (which is searchable).
*/
public void testSearchableGoogleSearch() {
// test basic array & hashmap
Searchables searchables = new Searchables(mContext);
searchables.buildSearchableList();
// test linkage from another activity
// TODO inject this via mocking into the package manager.
// TODO for now, just check for searchable GoogleSearch app (this isn't really a unit test)
ComponentName thisActivity = new ComponentName(
"com.android.googlesearch",
"com.android.googlesearch.GoogleSearch");
SearchableInfo si = searchables.getSearchableInfo(thisActivity);
assertNotNull(si);
assertTrue(si.mSearchable);
assertEquals(thisActivity, si.mSearchActivity);
Context appContext = si.getActivityContext(mContext);
assertNotNull(appContext);
MoreAsserts.assertNotEqual(appContext, mContext);
assertEquals("Google Search", appContext.getString(si.getHintId()));
assertEquals("Google", appContext.getString(si.getLabelId()));
}
/**
* Test that non-searchable activities return no searchable info (this would typically
* trigger the use of the default searchable e.g. contacts)
*/
public void testNonSearchable() {
// test basic array & hashmap
Searchables searchables = new Searchables(mContext);
searchables.buildSearchableList();
// confirm that we return null for non-searchy activities
ComponentName nonActivity = new ComponentName(
"com.android.unit_tests",
"com.android.unit_tests.NO_SEARCH_ACTIVITY");
SearchableInfo si = searchables.getSearchableInfo(nonActivity);
assertNull(si);
}
/**
* This is an attempt to run the searchable info list with a mocked context. Here are some
* things I'd like to test.
*
* Confirm OK with "zero" searchables
* Confirm "good" metadata read properly
* Confirm "bad" metadata skipped properly
* Confirm ordering of searchables
* Confirm "good" actionkeys
* confirm "bad" actionkeys are rejected
* confirm XML ordering enforced (will fail today - bug in SearchableInfo)
* findActionKey works
* getIcon works
*/
public void testSearchableMocked() {
MyMockPackageManager mockPM = new MyMockPackageManager(mContext.getPackageManager());
MyMockContext mockContext = new MyMockContext(mContext, mockPM);
Searchables searchables;
ArrayList<SearchableInfo> searchablesList;
int count;
// build item list with real-world source data
mockPM.setSearchablesMode(MyMockPackageManager.SEARCHABLES_PASSTHROUGH);
searchables = new Searchables(mockContext);
searchables.buildSearchableList();
// tests with "real" searchables (deprecate, this should be a unit test)
searchablesList = searchables.getSearchablesList();
count = searchablesList.size();
assertTrue(count >= 1); // this isn't really a unit test
checkSearchables(searchablesList);
// build item list with mocked search data
// this round of tests confirms good operations with "zero" searchables found
// This should return either a null pointer or an empty list
mockPM.setSearchablesMode(MyMockPackageManager.SEARCHABLES_MOCK_ZERO);
searchables = new Searchables(mockContext);
searchables.buildSearchableList();
searchablesList = searchables.getSearchablesList();
if (searchablesList != null) {
count = searchablesList.size();
assertTrue(count == 0);
}
}
/**
* Generic health checker for an array of searchables.
*
* This is designed to pass for any semi-legal searchable, without knowing much about
* the format of the underlying data. It's fairly easy for a non-compliant application
* to provide meta-data that will pass here (e.g. a non-existent suggestions authority).
*
* @param searchables The list of searchables to examine.
*/
private void checkSearchables(ArrayList<SearchableInfo> searchablesList) {
assertNotNull(searchablesList);
int count = searchablesList.size();
for (int ii = 0; ii < count; ii++) {
SearchableInfo si = searchablesList.get(ii);
assertNotNull(si);
assertTrue(si.mSearchable);
assertTrue(si.getLabelId() != 0); // This must be a useable string
assertNotEmpty(si.mSearchActivity.getClassName());
assertNotEmpty(si.mSearchActivity.getPackageName());
if (si.getSuggestAuthority() != null) {
// The suggestion fields are largely optional, so we'll just confirm basic health
assertNotEmpty(si.getSuggestAuthority());
assertNullOrNotEmpty(si.getSuggestPath());
assertNullOrNotEmpty(si.getSuggestSelection());
assertNullOrNotEmpty(si.getSuggestIntentAction());
assertNullOrNotEmpty(si.getSuggestIntentData());
}
/* Add a way to get the entire action key list, then explicitly test its elements */
/* For now, test the most common action key (CALL) */
ActionKeyInfo ai = si.findActionKey(KeyEvent.KEYCODE_CALL);
if (ai != null) {
assertEquals(ai.mKeyCode, KeyEvent.KEYCODE_CALL);
// one of these three fields must be non-null & non-empty
boolean m1 = (ai.mQueryActionMsg != null) && (ai.mQueryActionMsg.length() > 0);
boolean m2 = (ai.mSuggestActionMsg != null) && (ai.mSuggestActionMsg.length() > 0);
boolean m3 = (ai.mSuggestActionMsgColumn != null) &&
(ai.mSuggestActionMsgColumn.length() > 0);
assertTrue(m1 || m2 || m3);
}
/*
* Find ways to test these:
*
* private int mSearchMode
* private Drawable mIcon
*/
/*
* Explicitly not tested here:
*
* Can be null, so not much to see:
* public String mSearchHint
* private String mZeroQueryBanner
*
* To be deprecated/removed, so don't bother:
* public boolean mFilterMode
* public boolean mQuickStart
* private boolean mIconResized
* private int mIconResizeWidth
* private int mIconResizeHeight
*
* All of these are "internal" working variables, not part of any contract
* private ActivityInfo mActivityInfo
* private Rect mTempRect
* private String mSuggestProviderPackage
* private String mCacheActivityContext
*/
}
}
/**
* Combo assert for "string not null and not empty"
*/
private void assertNotEmpty(final String s) {
assertNotNull(s);
MoreAsserts.assertNotEqual(s, "");
}
/**
* Combo assert for "string null or (not null and not empty)"
*/
private void assertNullOrNotEmpty(final String s) {
if (s != null) {
MoreAsserts.assertNotEqual(s, "");
}
}
/**
* This is a mock for context. Used to perform a true unit test on SearchableInfo.
*
*/
private class MyMockContext extends MockContext {
protected Context mRealContext;
protected PackageManager mPackageManager;
/**
* Constructor.
*
* @param realContext Please pass in a real context for some pass-throughs to function.
*/
MyMockContext(Context realContext, PackageManager packageManager) {
mRealContext = realContext;
mPackageManager = packageManager;
}
/**
* Resources. Pass through for now.
*/
@Override
public Resources getResources() {
return mRealContext.getResources();
}
/**
* Package manager. Pass through for now.
*/
@Override
public PackageManager getPackageManager() {
return mPackageManager;
}
/**
* Package manager. Pass through for now.
*/
@Override
public Context createPackageContext(String packageName, int flags)
throws PackageManager.NameNotFoundException {
return mRealContext.createPackageContext(packageName, flags);
}
}
/**
* This is a mock for package manager. Used to perform a true unit test on SearchableInfo.
*
*/
private class MyMockPackageManager extends MockPackageManager {
public final static int SEARCHABLES_PASSTHROUGH = 0;
public final static int SEARCHABLES_MOCK_ZERO = 1;
public final static int SEARCHABLES_MOCK_ONEGOOD = 2;
public final static int SEARCHABLES_MOCK_ONEGOOD_ONEBAD = 3;
protected PackageManager mRealPackageManager;
protected int mSearchablesMode;
public MyMockPackageManager(PackageManager realPM) {
mRealPackageManager = realPM;
mSearchablesMode = SEARCHABLES_PASSTHROUGH;
}
/**
* Set the mode for various tests.
*/
public void setSearchablesMode(int newMode) {
switch (newMode) {
case SEARCHABLES_PASSTHROUGH:
case SEARCHABLES_MOCK_ZERO:
mSearchablesMode = newMode;
break;
default:
throw new UnsupportedOperationException();
}
}
/**
* Find activities that support a given intent.
*
* Retrieve all activities that can be performed for the given intent.
*
* @param intent The desired intent as per resolveActivity().
* @param flags Additional option flags. The most important is
* MATCH_DEFAULT_ONLY, to limit the resolution to only
* those activities that support the CATEGORY_DEFAULT.
*
* @return A List<ResolveInfo> containing one entry for each matching
* Activity. These are ordered from best to worst match -- that
* is, the first item in the list is what is returned by
* resolveActivity(). If there are no matching activities, an empty
* list is returned.
*/
@Override
public List<ResolveInfo> queryIntentActivities(Intent intent, int flags) {
assertNotNull(intent);
assertEquals(intent.getAction(), Intent.ACTION_SEARCH);
switch (mSearchablesMode) {
case SEARCHABLES_PASSTHROUGH:
return mRealPackageManager.queryIntentActivities(intent, flags);
case SEARCHABLES_MOCK_ZERO:
return null;
default:
throw new UnsupportedOperationException();
}
}
/**
* Retrieve an XML file from a package. This is a low-level API used to
* retrieve XML meta data.
*
* @param packageName The name of the package that this xml is coming from.
* Can not be null.
* @param resid The resource identifier of the desired xml. Can not be 0.
* @param appInfo Overall information about <var>packageName</var>. This
* may be null, in which case the application information will be retrieved
* for you if needed; if you already have this information around, it can
* be much more efficient to supply it here.
*
* @return Returns an XmlPullParser allowing you to parse out the XML
* data. Returns null if the xml resource could not be found for any
* reason.
*/
@Override
public XmlResourceParser getXml(String packageName, int resid, ApplicationInfo appInfo) {
assertNotNull(packageName);
MoreAsserts.assertNotEqual(packageName, "");
MoreAsserts.assertNotEqual(resid, 0);
switch (mSearchablesMode) {
case SEARCHABLES_PASSTHROUGH:
return mRealPackageManager.getXml(packageName, resid, appInfo);
case SEARCHABLES_MOCK_ZERO:
default:
throw new UnsupportedOperationException();
}
}
/**
* Find a single content provider by its base path name.
*
* @param name The name of the provider to find.
* @param flags Additional option flags. Currently should always be 0.
*
* @return ContentProviderInfo Information about the provider, if found,
* else null.
*/
@Override
public ProviderInfo resolveContentProvider(String name, int flags) {
assertNotNull(name);
MoreAsserts.assertNotEqual(name, "");
assertEquals(flags, 0);
switch (mSearchablesMode) {
case SEARCHABLES_PASSTHROUGH:
return mRealPackageManager.resolveContentProvider(name, flags);
case SEARCHABLES_MOCK_ZERO:
default:
throw new UnsupportedOperationException();
}
}
}
}

View File

@ -147,4 +147,56 @@ public class AutoCompleteTextViewPopup
// now try moving "down" - nothing should happen since there's no longer an adapter
sendKeys("DPAD_DOWN");
}
/** Test the show/hide behavior of the drop-down. */
@MediumTest
public void testPopupShow() throws Throwable {
AutoCompleteTextViewSimple theActivity = getActivity();
final AutoCompleteTextView textView = theActivity.getTextView();
final Instrumentation instrumentation = getInstrumentation();
// Drop-down should not be showing when no text has been entered
assertFalse("isPopupShowing() on start", textView.isPopupShowing());
// focus and type
textView.requestFocus();
instrumentation.waitForIdleSync();
sendKeys("A");
// Drop-down should now be visible
assertTrue("isPopupShowing() after typing", textView.isPopupShowing());
// Clear the text
runTestOnUiThread(new Runnable() {
public void run() {
textView.setText("");
}
});
instrumentation.waitForIdleSync();
// Drop-down should be hidden when text is cleared
assertFalse("isPopupShowing() after text cleared", textView.isPopupShowing());
// Set the text, without filtering
runTestOnUiThread(new Runnable() {
public void run() {
textView.setText("a", false);
}
});
instrumentation.waitForIdleSync();
// Drop-down should still be hidden
assertFalse("isPopupShowing() after setText(\"a\", false)", textView.isPopupShowing());
// Set the text, now with filtering
runTestOnUiThread(new Runnable() {
public void run() {
textView.setText("a");
}
});
instrumentation.waitForIdleSync();
// Drop-down should show up after setText() with filtering
assertTrue("isPopupShowing() after text set", textView.isPopupShowing());
}
}