Merge branch 'readonly-p4-donut' into donut

@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
344
core/java/android/app/SuggestionsAdapter.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
82
core/java/android/provider/Applications.java
Normal 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() {}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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.
|
||||
*/
|
||||
|
243
core/java/android/server/search/Searchables.java
Normal 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;
|
||||
}
|
||||
}
|
@ -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() {
|
||||
|
@ -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) {
|
||||
|
33
core/res/res/drawable/btn_global_search.xml
Normal 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>
|
BIN
core/res/res/drawable/btn_global_search_normal.9.png
Normal file
After Width: | Height: | Size: 676 B |
33
core/res/res/drawable/btn_search_dialog.xml
Normal 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>
|
BIN
core/res/res/drawable/btn_search_dialog_default.9.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
core/res/res/drawable/btn_search_dialog_pressed.9.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
core/res/res/drawable/btn_search_dialog_selected.9.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
33
core/res/res/drawable/btn_search_dialog_voice.xml
Normal 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>
|
BIN
core/res/res/drawable/btn_search_dialog_voice_default.9.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
core/res/res/drawable/btn_search_dialog_voice_pressed.9.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
core/res/res/drawable/btn_search_dialog_voice_selected.9.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
core/res/res/drawable/search_dropdown_background.9.png
Executable file
After Width: | Height: | Size: 229 B |
BIN
core/res/res/drawable/search_plate_global.9.png
Normal file
After Width: | Height: | Size: 309 B |
31
core/res/res/drawable/textfield_search.xml
Normal 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>
|
||||
|
BIN
core/res/res/drawable/textfield_search_default.9.png
Executable file
After Width: | Height: | Size: 3.3 KiB |
BIN
core/res/res/drawable/textfield_search_pressed.9.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
core/res/res/drawable/textfield_search_selected.9.png
Executable file
After Width: | Height: | Size: 3.3 KiB |
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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" />
|
@ -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"
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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" />
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|