Merge branch 'readonly-p4-donut' into donut

@ -17,12 +17,17 @@
|
|||||||
package android.app;
|
package android.app;
|
||||||
|
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
|
import android.content.ContentResolver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
|
import android.os.RemoteException;
|
||||||
import android.os.ServiceManager;
|
import android.os.ServiceManager;
|
||||||
|
import android.server.search.SearchableInfo;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -439,20 +444,18 @@ import android.view.KeyEvent;
|
|||||||
*
|
*
|
||||||
* <tr><th>{@link #SUGGEST_COLUMN_ICON_1}</th>
|
* <tr><th>{@link #SUGGEST_COLUMN_ICON_1}</th>
|
||||||
* <td>If your cursor includes this column, then all suggestions will be provided in an
|
* <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.
|
* 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>
|
||||||
* <td align="center">No, but required if you also have {@link #SUGGEST_COLUMN_ICON_2}</td>
|
* <td align="center">No.</td>
|
||||||
* </tr>
|
* </tr>
|
||||||
*
|
*
|
||||||
* <tr><th>{@link #SUGGEST_COLUMN_ICON_2}</th>
|
* <tr><th>{@link #SUGGEST_COLUMN_ICON_2}</th>
|
||||||
* <td>If your cursor includes this column, then all suggestions will be provided in an
|
* <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.
|
* 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>
|
||||||
* <td align="center">No, but required if you also have {@link #SUGGEST_COLUMN_ICON_1}</td>
|
* <td align="center">No.</td>
|
||||||
* </tr>
|
* </tr>
|
||||||
*
|
*
|
||||||
* <tr><th>{@link #SUGGEST_COLUMN_INTENT_ACTION}</th>
|
* <tr><th>{@link #SUGGEST_COLUMN_INTENT_ACTION}</th>
|
||||||
@ -1154,6 +1157,13 @@ public class SearchManager
|
|||||||
*/
|
*/
|
||||||
public final static String ACTION_KEY = "action_key";
|
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
|
* Intent extra data key: Use this key with Intent.ACTION_SEARCH and
|
||||||
* {@link android.content.Intent#getStringExtra content.Intent.getStringExtra()}
|
* {@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";
|
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,
|
* 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
|
* 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,
|
* be a resource ID of a drawable, or a URI in one of the following formats:
|
||||||
* you must also include {@link #SUGGEST_COLUMN_ICON_2}.
|
*
|
||||||
|
* <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";
|
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,
|
* 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
|
* 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,
|
* be a resource ID of a drawable, or a URI in one of the following formats:
|
||||||
* you must also include {@link #SUGGEST_COLUMN_ICON_1}.
|
*
|
||||||
|
* <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";
|
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>
|
* 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
|
* 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.
|
* 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";
|
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>
|
* 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
|
* 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";
|
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 Context mContext;
|
||||||
private final Handler mHandler;
|
private final Handler mHandler;
|
||||||
@ -1257,12 +1361,6 @@ public class SearchManager
|
|||||||
mContext = context;
|
mContext = context;
|
||||||
mHandler = handler;
|
mHandler = handler;
|
||||||
}
|
}
|
||||||
private static ISearchManager mService;
|
|
||||||
|
|
||||||
static {
|
|
||||||
mService = ISearchManager.Stub.asInterface(
|
|
||||||
ServiceManager.getService(Context.SEARCH_SERVICE));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Launch search UI.
|
* Launch search UI.
|
||||||
@ -1460,4 +1558,92 @@ public class SearchManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.IntentFilter;
|
import android.content.IntentFilter;
|
||||||
|
import android.content.pm.PackageManager.NameNotFoundException;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.util.Config;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is a simplified version of the Search Manager service. It no longer handles
|
* 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
|
// general debugging support
|
||||||
private static final String TAG = "SearchManagerService";
|
private static final String TAG = "SearchManagerService";
|
||||||
private static final boolean DEBUG = false;
|
private static final boolean DEBUG = false;
|
||||||
private static final boolean localLOGV = DEBUG ? Config.LOGD : Config.LOGV;
|
|
||||||
|
|
||||||
// configuration choices
|
// configuration choices
|
||||||
private static final boolean IMMEDIATE_SEARCHABLES_UPDATE = true;
|
private static final boolean IMMEDIATE_SEARCHABLES_UPDATE = true;
|
||||||
@ -45,9 +44,10 @@ public class SearchManagerService extends ISearchManager.Stub
|
|||||||
private final Context mContext;
|
private final Context mContext;
|
||||||
private final Handler mHandler;
|
private final Handler mHandler;
|
||||||
private boolean mSearchablesDirty;
|
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!
|
* Only one instance of this object should be created!
|
||||||
*
|
*
|
||||||
* @param context to use for accessing DB, window manager, etc.
|
* @param context to use for accessing DB, window manager, etc.
|
||||||
@ -55,6 +55,8 @@ public class SearchManagerService extends ISearchManager.Stub
|
|||||||
public SearchManagerService(Context context) {
|
public SearchManagerService(Context context) {
|
||||||
mContext = context;
|
mContext = context;
|
||||||
mHandler = new Handler();
|
mHandler = new Handler();
|
||||||
|
mSearchablesDirty = true;
|
||||||
|
mSearchables = new Searchables(context);
|
||||||
|
|
||||||
// Setup the infrastructure for updating and maintaining the list
|
// Setup the infrastructure for updating and maintaining the list
|
||||||
// of searchable activities.
|
// of searchable activities.
|
||||||
@ -64,7 +66,6 @@ public class SearchManagerService extends ISearchManager.Stub
|
|||||||
filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
|
filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
|
||||||
filter.addDataScheme("package");
|
filter.addDataScheme("package");
|
||||||
mContext.registerReceiver(mIntentReceiver, filter, null, mHandler);
|
mContext.registerReceiver(mIntentReceiver, filter, null, mHandler);
|
||||||
mSearchablesDirty = true;
|
|
||||||
|
|
||||||
// After startup settles down, preload the searchables list,
|
// After startup settles down, preload the searchables list,
|
||||||
// which will reduce the delay when the search UI is invoked.
|
// 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.
|
* a package add/remove broadcast message.
|
||||||
*/
|
*/
|
||||||
private void updateSearchables() {
|
private void updateSearchables() {
|
||||||
SearchableInfo.buildSearchableList(mContext);
|
mSearchables.buildSearchableList();
|
||||||
mSearchablesDirty = false;
|
mSearchablesDirty = false;
|
||||||
|
|
||||||
// TODO This is a hack. This shouldn't be hardcoded here, it's probably
|
// TODO SearchableInfo should be the source of truth about whether a searchable exists.
|
||||||
// a policy.
|
// As it stands, if the package exists but is misconfigured in some way, then this
|
||||||
// ComponentName defaultSearch = new ComponentName(
|
// would fail, and needs to be fixed.
|
||||||
// "com.android.contacts",
|
|
||||||
// "com.android.contacts.ContactsListActivity" );
|
|
||||||
ComponentName defaultSearch = new ComponentName(
|
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",
|
||||||
"com.android.googlesearch.GoogleSearch" );
|
"com.android.googlesearch.GoogleSearch");
|
||||||
SearchableInfo.setDefaultSearchable(mContext, defaultSearch);
|
}
|
||||||
|
|
||||||
|
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.
|
* @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
|
* @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
|
* 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.
|
* 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.
|
* 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) {
|
public SearchableInfo getSearchableInfo(ComponentName launchActivity, boolean globalSearch) {
|
||||||
// final check. however we should try to avoid this, because
|
// final check. however we should try to avoid this, because
|
||||||
@ -146,11 +154,12 @@ public class SearchManagerService extends ISearchManager.Stub
|
|||||||
}
|
}
|
||||||
SearchableInfo si = null;
|
SearchableInfo si = null;
|
||||||
if (globalSearch) {
|
if (globalSearch) {
|
||||||
si = SearchableInfo.getDefaultSearchable();
|
si = mSearchables.getDefaultSearchable();
|
||||||
} else {
|
} else {
|
||||||
si = SearchableInfo.getSearchableInfo(mContext, launchActivity);
|
si = mSearchables.getSearchableInfo(launchActivity);
|
||||||
}
|
}
|
||||||
|
|
||||||
return si;
|
return si;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -21,14 +21,11 @@ import org.xmlpull.v1.XmlPullParserException;
|
|||||||
|
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.pm.ActivityInfo;
|
import android.content.pm.ActivityInfo;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.content.pm.ProviderInfo;
|
import android.content.pm.ProviderInfo;
|
||||||
import android.content.pm.ResolveInfo;
|
|
||||||
import android.content.res.TypedArray;
|
import android.content.res.TypedArray;
|
||||||
import android.content.res.XmlResourceParser;
|
import android.content.res.XmlResourceParser;
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.Parcel;
|
import android.os.Parcel;
|
||||||
import android.os.Parcelable;
|
import android.os.Parcelable;
|
||||||
import android.text.InputType;
|
import android.text.InputType;
|
||||||
@ -38,9 +35,6 @@ import android.util.Xml;
|
|||||||
import android.view.inputmethod.EditorInfo;
|
import android.view.inputmethod.EditorInfo;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public final class SearchableInfo implements Parcelable {
|
public final class SearchableInfo implements Parcelable {
|
||||||
|
|
||||||
@ -50,20 +44,13 @@ public final class SearchableInfo implements Parcelable {
|
|||||||
// set this flag to 1 to prevent any apps from providing suggestions
|
// set this flag to 1 to prevent any apps from providing suggestions
|
||||||
final static int DBG_INHIBIT_SUGGESTIONS = 0;
|
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
|
// TODO how should these be documented for the developer, in a more structured way than
|
||||||
// the current long wordy javadoc in SearchManager.java ?
|
// 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_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 = "searchable";
|
||||||
private static final String MD_XML_ELEMENT_SEARCHABLE_ACTION_KEY = "actionkey";
|
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
|
// true member variables - what we know about the searchability
|
||||||
// TO-DO replace public with getters
|
// TO-DO replace public with getters
|
||||||
public boolean mSearchable = false;
|
public boolean mSearchable = false;
|
||||||
@ -86,7 +73,6 @@ public final class SearchableInfo implements Parcelable {
|
|||||||
private String mSuggestIntentData = null;
|
private String mSuggestIntentData = null;
|
||||||
private ActionKeyInfo mActionKeyList = null;
|
private ActionKeyInfo mActionKeyList = null;
|
||||||
private String mSuggestProviderPackage = null;
|
private String mSuggestProviderPackage = null;
|
||||||
private Context mCacheActivityContext = null; // use during setup only - don't hold memory!
|
|
||||||
|
|
||||||
// Flag values for Searchable_voiceSearchMode
|
// Flag values for Searchable_voiceSearchMode
|
||||||
private static int VOICE_SEARCH_SHOW_BUTTON = 1;
|
private static int VOICE_SEARCH_SHOW_BUTTON = 1;
|
||||||
@ -98,36 +84,6 @@ public final class SearchableInfo implements Parcelable {
|
|||||||
private int mVoiceLanguageId; // voiceLanguage
|
private int mVoiceLanguageId; // voiceLanguage
|
||||||
private int mVoiceMaxResults; // voiceMaxResults
|
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.
|
* 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
|
* @return Returns a context related to the searchable activity
|
||||||
*/
|
*/
|
||||||
public Context getActivityContext(Context context) {
|
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;
|
Context theirContext = null;
|
||||||
try {
|
try {
|
||||||
theirContext = context.createPackageContext(mSearchActivity.getPackageName(), 0);
|
theirContext = context.createPackageContext(activity.getPackageName(), 0);
|
||||||
} catch (PackageManager.NameNotFoundException e) {
|
} catch (PackageManager.NameNotFoundException e) {
|
||||||
// unexpected, but we deal with this by null-checking theirContext
|
// unexpected, but we deal with this by null-checking theirContext
|
||||||
} catch (java.lang.SecurityException e) {
|
} catch (java.lang.SecurityException e) {
|
||||||
@ -233,195 +196,23 @@ public final class SearchableInfo implements Parcelable {
|
|||||||
return theirContext;
|
return theirContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Factory. Look up, or construct, based on the activity.
|
|
||||||
*
|
|
||||||
* The activities fall into three cases, based on meta-data found in
|
|
||||||
* the manifest entry:
|
|
||||||
* <ol>
|
|
||||||
* <li>The activity itself implements search. This is indicated by the
|
|
||||||
* presence of a "android.app.searchable" meta-data attribute.
|
|
||||||
* The value is a reference to an XML file containing search information.</li>
|
|
||||||
* <li>A related activity implements search. This is indicated by the
|
|
||||||
* presence of a "android.app.default_searchable" meta-data attribute.
|
|
||||||
* The value is a string naming the activity implementing search. In this
|
|
||||||
* case the factory will "redirect" and return the searchable data.</li>
|
|
||||||
* <li>No searchability data is provided. We return null here and other
|
|
||||||
* code will insert the "default" (e.g. contacts) search.
|
|
||||||
*
|
|
||||||
* TODO: cache the result in the map, and check the map first.
|
|
||||||
* TODO: it might make sense to implement the searchable reference as
|
|
||||||
* an application meta-data entry. This way we don't have to pepper each
|
|
||||||
* and every activity.
|
|
||||||
* TODO: can we skip the constructor step if it's a non-searchable?
|
|
||||||
* TODO: does it make sense to plug the default into a slot here for
|
|
||||||
* automatic return? Probably not, but it's one way to do it.
|
|
||||||
*
|
|
||||||
* @param activity The name of the current activity, or null if the
|
|
||||||
* activity does not define any explicit searchable metadata.
|
|
||||||
*/
|
|
||||||
public static SearchableInfo getSearchableInfo(Context context,
|
|
||||||
ComponentName activity) {
|
|
||||||
// Step 1. Is the result already hashed? (case 1)
|
|
||||||
SearchableInfo result;
|
|
||||||
synchronized (SearchableInfo.class) {
|
|
||||||
result = sSearchablesMap.get(activity);
|
|
||||||
if (result != null) return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 2. See if the current activity references a searchable.
|
|
||||||
// Note: Conceptually, this could be a while(true) loop, but there's
|
|
||||||
// no point in implementing reference chaining here and risking a loop.
|
|
||||||
// References must point directly to searchable activities.
|
|
||||||
|
|
||||||
ActivityInfo ai = null;
|
|
||||||
XmlPullParser xml = null;
|
|
||||||
try {
|
|
||||||
ai = context.getPackageManager().
|
|
||||||
getActivityInfo(activity, PackageManager.GET_META_DATA );
|
|
||||||
String refActivityName = null;
|
|
||||||
|
|
||||||
// First look for activity-specific reference
|
|
||||||
Bundle md = ai.metaData;
|
|
||||||
if (md != null) {
|
|
||||||
refActivityName = md.getString(MD_LABEL_DEFAULT_SEARCHABLE);
|
|
||||||
}
|
|
||||||
// If not found, try for app-wide reference
|
|
||||||
if (refActivityName == null) {
|
|
||||||
md = ai.applicationInfo.metaData;
|
|
||||||
if (md != null) {
|
|
||||||
refActivityName = md.getString(MD_LABEL_DEFAULT_SEARCHABLE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Irrespective of source, if a reference was found, follow it.
|
|
||||||
if (refActivityName != null)
|
|
||||||
{
|
|
||||||
// An app or activity can declare that we should simply launch
|
|
||||||
// "system default search" if search is invoked.
|
|
||||||
if (refActivityName.equals(MD_SEARCHABLE_SYSTEM_SEARCH)) {
|
|
||||||
return getDefaultSearchable();
|
|
||||||
}
|
|
||||||
String pkg = activity.getPackageName();
|
|
||||||
ComponentName referredActivity;
|
|
||||||
if (refActivityName.charAt(0) == '.') {
|
|
||||||
referredActivity = new ComponentName(pkg, pkg + refActivityName);
|
|
||||||
} else {
|
|
||||||
referredActivity = new ComponentName(pkg, refActivityName);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now try the referred activity, and if found, cache
|
|
||||||
// it against the original name so we can skip the check
|
|
||||||
synchronized (SearchableInfo.class) {
|
|
||||||
result = sSearchablesMap.get(referredActivity);
|
|
||||||
if (result != null) {
|
|
||||||
sSearchablesMap.put(activity, result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (PackageManager.NameNotFoundException e) {
|
|
||||||
// case 3: no metadata
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 3. None found. Return null.
|
|
||||||
return null;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Super-factory. Builds an entire list (suitable for display) of
|
|
||||||
* activities that are searchable, by iterating the entire set of
|
|
||||||
* ACTION_SEARCH intents.
|
|
||||||
*
|
|
||||||
* Also clears the hash of all activities -> searches which will
|
|
||||||
* refill as the user clicks "search".
|
|
||||||
*
|
|
||||||
* This should only be done at startup and again if we know that the
|
|
||||||
* list has changed.
|
|
||||||
*
|
|
||||||
* TODO: every activity that provides a ACTION_SEARCH intent should
|
|
||||||
* also provide searchability meta-data. There are a bunch of checks here
|
|
||||||
* that, if data is not found, silently skip to the next activity. This
|
|
||||||
* won't help a developer trying to figure out why their activity isn't
|
|
||||||
* showing up in the list, but an exception here is too rough. I would
|
|
||||||
* like to find a better notification mechanism.
|
|
||||||
*
|
|
||||||
* TODO: sort the list somehow? UI choice.
|
|
||||||
*
|
|
||||||
* @param context a context we can use during this work
|
|
||||||
*/
|
|
||||||
public static void buildSearchableList(Context context) {
|
|
||||||
|
|
||||||
// create empty hash & list
|
|
||||||
HashMap<ComponentName, SearchableInfo> newSearchablesMap
|
|
||||||
= new HashMap<ComponentName, SearchableInfo>();
|
|
||||||
ArrayList<SearchableInfo> newSearchablesList
|
|
||||||
= new ArrayList<SearchableInfo>();
|
|
||||||
|
|
||||||
// use intent resolver to generate list of ACTION_SEARCH receivers
|
|
||||||
final PackageManager pm = context.getPackageManager();
|
|
||||||
List<ResolveInfo> infoList;
|
|
||||||
final Intent intent = new Intent(Intent.ACTION_SEARCH);
|
|
||||||
infoList = pm.queryIntentActivities(intent, PackageManager.GET_META_DATA);
|
|
||||||
|
|
||||||
// analyze each one, generate a Searchables record, and record
|
|
||||||
if (infoList != null) {
|
|
||||||
int count = infoList.size();
|
|
||||||
for (int ii = 0; ii < count; ii++) {
|
|
||||||
// for each component, try to find metadata
|
|
||||||
ResolveInfo info = infoList.get(ii);
|
|
||||||
ActivityInfo ai = info.activityInfo;
|
|
||||||
XmlResourceParser xml = ai.loadXmlMetaData(context.getPackageManager(),
|
|
||||||
MD_LABEL_SEARCHABLE);
|
|
||||||
if (xml == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
ComponentName cName = new ComponentName(
|
|
||||||
info.activityInfo.packageName,
|
|
||||||
info.activityInfo.name);
|
|
||||||
|
|
||||||
SearchableInfo searchable = getActivityMetaData(context, xml, cName);
|
|
||||||
xml.close();
|
|
||||||
|
|
||||||
if (searchable != null) {
|
|
||||||
// no need to keep the context any longer. setup time is over.
|
|
||||||
searchable.mCacheActivityContext = null;
|
|
||||||
|
|
||||||
newSearchablesList.add(searchable);
|
|
||||||
newSearchablesMap.put(cName, searchable);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// record the final values as a coherent pair
|
|
||||||
synchronized (SearchableInfo.class) {
|
|
||||||
sSearchablesList = newSearchablesList;
|
|
||||||
sSearchablesMap = newSearchablesMap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
*
|
*
|
||||||
* Given a ComponentName, get the searchability info
|
* Given a ComponentName, get the searchability info
|
||||||
* and build a local copy of it. Use the factory, not this.
|
* 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
|
* @param attr The attribute set we found in the XML file, contains the values that are used to
|
||||||
* construct the object.
|
* construct the object.
|
||||||
* @param cName The component name of the searchable activity
|
* @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
|
// initialize as an "unsearchable" object
|
||||||
mSearchable = false;
|
mSearchable = false;
|
||||||
mSearchActivity = cName;
|
mSearchActivity = cName;
|
||||||
|
|
||||||
// to access another activity's resources, I need its context.
|
TypedArray a = activityContext.obtainStyledAttributes(attr,
|
||||||
// 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);
|
com.android.internal.R.styleable.Searchable);
|
||||||
mSearchMode = a.getInt(com.android.internal.R.styleable.Searchable_searchMode, 0);
|
mSearchMode = a.getInt(com.android.internal.R.styleable.Searchable_searchMode, 0);
|
||||||
mLabelId = a.getResourceId(com.android.internal.R.styleable.Searchable_label, 0);
|
mLabelId = a.getResourceId(com.android.internal.R.styleable.Searchable_label, 0);
|
||||||
@ -464,14 +255,12 @@ public final class SearchableInfo implements Parcelable {
|
|||||||
|
|
||||||
// get package info for suggestions provider (if any)
|
// get package info for suggestions provider (if any)
|
||||||
if (mSuggestAuthority != null) {
|
if (mSuggestAuthority != null) {
|
||||||
ProviderInfo pi =
|
PackageManager pm = activityContext.getPackageManager();
|
||||||
context.getPackageManager().resolveContentProvider(mSuggestAuthority,
|
ProviderInfo pi = pm.resolveContentProvider(mSuggestAuthority, 0);
|
||||||
0);
|
|
||||||
if (pi != null) {
|
if (pi != null) {
|
||||||
mSuggestProviderPackage = pi.packageName;
|
mSuggestProviderPackage = pi.packageName;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// for now, implement some form of rules - minimal data
|
// for now, implement some form of rules - minimal data
|
||||||
if (mLabelId != 0) {
|
if (mLabelId != 0) {
|
||||||
@ -496,7 +285,7 @@ public final class SearchableInfo implements Parcelable {
|
|||||||
/**
|
/**
|
||||||
* Private class used to hold the "action key" configuration
|
* 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 int mKeyCode = 0;
|
||||||
public String mQueryActionMsg;
|
public String mQueryActionMsg;
|
||||||
@ -506,14 +295,15 @@ public final class SearchableInfo implements Parcelable {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Create one object using attributeset as input data.
|
* 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
|
* @param attr The attribute set we found in the XML file, contains the values that are used to
|
||||||
* construct the object.
|
* construct the object.
|
||||||
* @param next We'll build these up using a simple linked list (since there are usually
|
* @param next We'll build these up using a simple linked list (since there are usually
|
||||||
* just zero or one).
|
* just zero or one).
|
||||||
*/
|
*/
|
||||||
public ActionKeyInfo(Context context, AttributeSet attr, ActionKeyInfo next) {
|
public ActionKeyInfo(Context activityContext, AttributeSet attr, ActionKeyInfo next) {
|
||||||
TypedArray a = mCacheActivityContext.obtainStyledAttributes(attr,
|
TypedArray a = activityContext.obtainStyledAttributes(attr,
|
||||||
com.android.internal.R.styleable.SearchableActionKey);
|
com.android.internal.R.styleable.SearchableActionKey);
|
||||||
|
|
||||||
mKeyCode = a.getInt(
|
mKeyCode = a.getInt(
|
||||||
@ -584,6 +374,20 @@ public final class SearchableInfo implements Parcelable {
|
|||||||
return null;
|
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
|
* 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,
|
private static SearchableInfo getActivityMetaData(Context context, XmlPullParser xml,
|
||||||
final ComponentName cName) {
|
final ComponentName cName) {
|
||||||
SearchableInfo result = null;
|
SearchableInfo result = null;
|
||||||
|
Context activityContext = createActivityContext(context, cName);
|
||||||
|
|
||||||
// in order to use the attributes mechanism, we have to walk the parser
|
// 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.
|
// 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)) {
|
if (xml.getName().equals(MD_XML_ELEMENT_SEARCHABLE)) {
|
||||||
AttributeSet attr = Xml.asAttributeSet(xml);
|
AttributeSet attr = Xml.asAttributeSet(xml);
|
||||||
if (attr != null) {
|
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 the constructor returned a bad object, exit now.
|
||||||
if (! result.mSearchable) {
|
if (! result.mSearchable) {
|
||||||
return null;
|
return null;
|
||||||
@ -621,7 +426,7 @@ public final class SearchableInfo implements Parcelable {
|
|||||||
}
|
}
|
||||||
AttributeSet attr = Xml.asAttributeSet(xml);
|
AttributeSet attr = Xml.asAttributeSet(xml);
|
||||||
if (attr != null) {
|
if (attr != null) {
|
||||||
ActionKeyInfo keyInfo = result.new ActionKeyInfo(context, attr,
|
ActionKeyInfo keyInfo = new ActionKeyInfo(activityContext, attr,
|
||||||
result.mActionKeyList);
|
result.mActionKeyList);
|
||||||
// only add to list if it is was useable
|
// only add to list if it is was useable
|
||||||
if (keyInfo.mKeyCode != 0) {
|
if (keyInfo.mKeyCode != 0) {
|
||||||
@ -637,6 +442,7 @@ public final class SearchableInfo implements Parcelable {
|
|||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -756,16 +562,6 @@ public final class SearchableInfo implements Parcelable {
|
|||||||
return mSearchImeOptions;
|
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.
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
@ -984,18 +984,6 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
|
|||||||
invalidate();
|
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
|
@Override
|
||||||
protected int computeVerticalScrollExtent() {
|
protected int computeVerticalScrollExtent() {
|
||||||
final int count = getChildCount();
|
final int count = getChildCount();
|
||||||
|
@ -110,6 +110,10 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
|
|||||||
private final DropDownItemClickListener mDropDownItemClickListener =
|
private final DropDownItemClickListener mDropDownItemClickListener =
|
||||||
new DropDownItemClickListener();
|
new DropDownItemClickListener();
|
||||||
|
|
||||||
|
private boolean mDropDownAlwaysVisible = false;
|
||||||
|
|
||||||
|
private boolean mDropDownDismissedOnCompletion = true;
|
||||||
|
|
||||||
private int mLastKeyCode = KeyEvent.KEYCODE_UNKNOWN;
|
private int mLastKeyCode = KeyEvent.KEYCODE_UNKNOWN;
|
||||||
private boolean mOpenBefore;
|
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>
|
* {@link ViewGroup.LayoutParams#WRAP_CONTENT} to fit the width of its anchor view.</p>
|
||||||
*
|
*
|
||||||
* @return the width for the drop down list
|
* @return the width for the drop down list
|
||||||
|
*
|
||||||
|
* @attr ref android.R.styleable#AutoCompleteTextView_dropDownWidth
|
||||||
*/
|
*/
|
||||||
public int getDropDownWidth() {
|
public int getDropDownWidth() {
|
||||||
return mDropDownWidth;
|
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>
|
* {@link ViewGroup.LayoutParams#WRAP_CONTENT} to fit the width of its anchor view.</p>
|
||||||
*
|
*
|
||||||
* @param width the width to use
|
* @param width the width to use
|
||||||
|
*
|
||||||
|
* @attr ref android.R.styleable#AutoCompleteTextView_dropDownWidth
|
||||||
*/
|
*/
|
||||||
public void setDropDownWidth(int width) {
|
public void setDropDownWidth(int width) {
|
||||||
mDropDownWidth = 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>
|
* <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
|
* @return the view's id, or {@link View#NO_ID} if none specified
|
||||||
|
*
|
||||||
|
* @attr ref android.R.styleable#AutoCompleteTextView_dropDownAnchor
|
||||||
*/
|
*/
|
||||||
public int getDropDownAnchor() {
|
public int getDropDownAnchor() {
|
||||||
return mDropDownAnchorId;
|
return mDropDownAnchorId;
|
||||||
@ -242,12 +252,172 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
|
|||||||
* loading a view which is not yet instantiated.</p>
|
* loading a view which is not yet instantiated.</p>
|
||||||
*
|
*
|
||||||
* @param id the id to anchor the drop down list view to
|
* @param id the id to anchor the drop down list view to
|
||||||
|
*
|
||||||
|
* @attr ref android.R.styleable#AutoCompleteTextView_dropDownAnchor
|
||||||
*/
|
*/
|
||||||
public void setDropDownAnchor(int id) {
|
public void setDropDownAnchor(int id) {
|
||||||
mDropDownAnchorId = id;
|
mDropDownAnchorId = id;
|
||||||
mDropDownAnchorView = null;
|
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
|
* <p>Returns the number of characters the user must type before the drop
|
||||||
* down list is shown.</p>
|
* down list is shown.</p>
|
||||||
@ -629,16 +799,6 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
|
|||||||
return ListView.INVALID_POSITION;
|
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
|
* <p>Starts filtering the content of the drop down list. The filtering
|
||||||
* pattern is the content of the edit box. Subclasses should override this
|
* pattern is the content of the edit box. Subclasses should override this
|
||||||
@ -709,8 +869,10 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mDropDownDismissedOnCompletion) {
|
||||||
dismissDropDown();
|
dismissDropDown();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Identifies whether the view is currently performing a text completion, so subclasses
|
* Identifies whether the view is currently performing a text completion, so subclasses
|
||||||
@ -720,6 +882,42 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
|
|||||||
return mBlockCompletion;
|
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
|
* <p>Performs the text completion by replacing the current text by the
|
||||||
* selected item. Subclasses should override this method to avoid replacing
|
* 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());
|
Selection.setSelection(spannable, spannable.length());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** {@inheritDoc} */
|
||||||
public void onFilterComplete(int count) {
|
public void onFilterComplete(int count) {
|
||||||
if (mAttachCount <= 0) return;
|
if (mAttachCount <= 0) return;
|
||||||
|
|
||||||
@ -744,7 +943,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
|
|||||||
* to filter.
|
* to filter.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
if (count > 0 && enoughToFilter()) {
|
if ((count > 0 || mDropDownAlwaysVisible) && enoughToFilter()) {
|
||||||
if (hasFocus() && hasWindowFocus()) {
|
if (hasFocus() && hasWindowFocus()) {
|
||||||
showDropDown();
|
showDropDown();
|
||||||
}
|
}
|
||||||
@ -808,22 +1007,6 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
|
|||||||
return result;
|
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
|
* <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
|
* 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);
|
mDropDownVerticalOffset, widthSpec, height);
|
||||||
} else {
|
} else {
|
||||||
if (mDropDownWidth == ViewGroup.LayoutParams.FILL_PARENT) {
|
if (mDropDownWidth == ViewGroup.LayoutParams.FILL_PARENT) {
|
||||||
mPopup.setWindowLayoutMode(ViewGroup.LayoutParams.FILL_PARENT,
|
mPopup.setWindowLayoutMode(ViewGroup.LayoutParams.FILL_PARENT, 0);
|
||||||
ViewGroup.LayoutParams.WRAP_CONTENT);
|
|
||||||
} else {
|
} else {
|
||||||
mPopup.setWindowLayoutMode(0, ViewGroup.LayoutParams.WRAP_CONTENT);
|
mPopup.setWindowLayoutMode(0, 0);
|
||||||
if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) {
|
if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) {
|
||||||
mPopup.setWidth(getDropDownAnchorView().getWidth());
|
mPopup.setWidth(getDropDownAnchorView().getWidth());
|
||||||
} else {
|
} else {
|
||||||
@ -966,8 +1148,10 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
|
|||||||
final int maxHeight = mPopup.getMaxAvailableHeight(this, mDropDownVerticalOffset);
|
final int maxHeight = mPopup.getMaxAvailableHeight(this, mDropDownVerticalOffset);
|
||||||
//otherHeights += dropDownView.getPaddingTop() + dropDownView.getPaddingBottom();
|
//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;
|
0, ListView.NO_POSITION, maxHeight - otherHeights, 2) + otherHeights;
|
||||||
|
|
||||||
|
return mDropDownAlwaysVisible ? maxHeight : measuredHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
private View getHintView(Context context) {
|
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:minHeight="?android:attr/listPreferredItemHeight"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_width="fill_parent"
|
android:layout_width="fill_parent"
|
||||||
android:paddingLeft="14dip"
|
android:paddingLeft="10dip"
|
||||||
android:paddingRight="15dip">
|
android:paddingRight="15dip">
|
||||||
|
|
||||||
<!-- Activity icon when presenting dialog -->
|
<!-- Activity icon when presenting dialog -->
|
||||||
@ -42,13 +42,13 @@
|
|||||||
android:textAppearance="?android:attr/textAppearanceLargeInverse"
|
android:textAppearance="?android:attr/textAppearanceLargeInverse"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:paddingLeft="6dip" />
|
android:paddingLeft="10dip" />
|
||||||
<!-- Extended activity info to distinguish between duplicate activity names -->
|
<!-- Extended activity info to distinguish between duplicate activity names -->
|
||||||
<TextView android:id="@android:id/text2"
|
<TextView android:id="@android:id/text2"
|
||||||
android:textAppearance="?android:attr/textAppearanceMediumInverse"
|
android:textAppearance="?android:attr/textAppearanceMediumInverse"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:paddingLeft="6dip" />
|
android:paddingLeft="10dip" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
@ -26,79 +26,67 @@
|
|||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
android:descendantFocusability="afterDescendants">
|
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 -->
|
<!-- 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
|
<LinearLayout
|
||||||
android:id="@+id/search_plate"
|
android:id="@+id/search_plate"
|
||||||
android:layout_width="fill_parent"
|
android:layout_width="fill_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:paddingLeft="8dip"
|
android:paddingLeft="12dip"
|
||||||
android:paddingRight="8dip"
|
android:paddingRight="12dip"
|
||||||
android:paddingTop="6dip"
|
android:paddingTop="7dip"
|
||||||
android:paddingBottom="16dip"
|
android:paddingBottom="16dip"
|
||||||
android:baselineAligned="false"
|
android:background="@drawable/search_plate_global" >
|
||||||
android:background="@android:drawable/search_plate"
|
|
||||||
android:addStatesFromChildren="true" >
|
|
||||||
|
|
||||||
<!-- This is actually used for the badge icon *or* the badge label (or neither) -->
|
<!-- This is actually used for the badge icon *or* the badge label (or neither) -->
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/search_badge"
|
android:id="@+id/search_badge"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:paddingLeft="2dip"
|
android:layout_marginBottom="2dip"
|
||||||
android:drawablePadding="0dip"
|
android:drawablePadding="0dip"
|
||||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||||
android:textColor="?android:attr/textColorPrimary" />
|
android:textColor="?android:attr/textColorPrimaryInverse" />
|
||||||
|
|
||||||
<!-- Inner layout contains the button(s) and EditText -->
|
<!-- 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
|
<LinearLayout
|
||||||
android:id="@+id/search_edit_frame"
|
android:id="@+id/search_edit_frame"
|
||||||
android:layout_width="fill_parent"
|
android:layout_width="fill_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="1dip"
|
android:orientation="horizontal">
|
||||||
android:layout_marginBottom="-5dip"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:addStatesFromChildren="true"
|
|
||||||
android:gravity="center_vertical"
|
|
||||||
android:baselineAligned="false" >
|
|
||||||
|
|
||||||
<view class="android.app.SearchDialog$SearchAutoComplete"
|
<view class="android.app.SearchDialog$SearchAutoComplete"
|
||||||
android:id="@+id/search_src_text"
|
android:id="@+id/search_src_text"
|
||||||
|
android:background="@drawable/textfield_search"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_width="0dip"
|
android:layout_width="0dip"
|
||||||
android:layout_weight="1.0"
|
android:layout_weight="1.0"
|
||||||
android:paddingLeft="8dip"
|
android:paddingLeft="8dip"
|
||||||
android:paddingRight="6dip"
|
android:paddingRight="6dip"
|
||||||
|
android:singleLine="true"
|
||||||
android:inputType="text|textAutoComplete"
|
android:inputType="text|textAutoComplete"
|
||||||
android:dropDownWidth="fill_parent"
|
android:dropDownWidth="fill_parent"
|
||||||
android:dropDownAnchor="@id/search_plate"
|
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" -->
|
<!-- This button can switch between text and icon "modes" -->
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/search_go_btn"
|
android:id="@+id/search_go_btn"
|
||||||
android:layout_marginLeft="1dip"
|
android:background="@drawable/btn_search_dialog"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="fill_parent"
|
||||||
android:drawableLeft="@android:drawable/ic_btn_search"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ImageButton android:id="@+id/search_voice_btn"
|
<ImageButton
|
||||||
|
android:id="@+id/search_voice_btn"
|
||||||
android:layout_width="wrap_content"
|
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"
|
android:src="@android:drawable/ic_btn_speak_now"
|
||||||
/>
|
/>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:id="@android:id/text1"
|
android:id="@android:id/text1"
|
||||||
style="?android:attr/dropDownItemStyle"
|
style="?android:attr/dropDownItemStyle"
|
||||||
android:textAppearance="?android:attr/textAppearanceMediumInverse"
|
android:textAppearance="?android:attr/textAppearanceSearchResultTitle"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:layout_width="fill_parent"
|
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"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:layout_width="fill_parent"
|
android:layout_width="fill_parent"
|
||||||
android:layout_height="?android:attr/listPreferredItemHeight"
|
android:layout_height="?android:attr/searchResultListItemHeight"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:baselineAligned="false"
|
android:baselineAligned="false"
|
||||||
>
|
>
|
||||||
|
|
||||||
<TwoLineListItem
|
<TwoLineListItem
|
||||||
android:paddingTop="2dip"
|
android:paddingTop="1dip"
|
||||||
android:paddingBottom="2dip"
|
android:paddingBottom="1dip"
|
||||||
|
android:gravity="center_vertical"
|
||||||
android:layout_width="0dip"
|
android:layout_width="0dip"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
@ -37,7 +38,7 @@
|
|||||||
<TextView
|
<TextView
|
||||||
android:id="@android:id/text1"
|
android:id="@android:id/text1"
|
||||||
style="?android:attr/dropDownItemStyle"
|
style="?android:attr/dropDownItemStyle"
|
||||||
android:textAppearance="?android:attr/textAppearanceMediumInverse"
|
android:textAppearance="?android:attr/textAppearanceSearchResultTitle"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:layout_width="fill_parent"
|
android:layout_width="fill_parent"
|
||||||
android:layout_height="wrap_content" />
|
android:layout_height="wrap_content" />
|
||||||
@ -45,7 +46,7 @@
|
|||||||
<TextView
|
<TextView
|
||||||
android:id="@android:id/text2"
|
android:id="@android:id/text2"
|
||||||
style="?android:attr/dropDownItemStyle"
|
style="?android:attr/dropDownItemStyle"
|
||||||
android:textAppearance="?android:attr/textAppearanceSmallInverse"
|
android:textAppearance="?android:attr/textAppearanceSearchResultSubtitle"
|
||||||
android:textColor="?android:attr/textColorSecondaryInverse"
|
android:textColor="?android:attr/textColorSecondaryInverse"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:layout_width="fill_parent"
|
android:layout_width="fill_parent"
|
||||||
|
@ -22,31 +22,33 @@
|
|||||||
<!-- of the text element in apps/common/res/layout/simple_dropdown_item_1line.xml -->
|
<!-- of the text element in apps/common/res/layout/simple_dropdown_item_1line.xml -->
|
||||||
|
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:paddingLeft="4dip"
|
||||||
|
android:paddingRight="2dip"
|
||||||
android:layout_width="fill_parent"
|
android:layout_width="fill_parent"
|
||||||
android:layout_height="?android:attr/listPreferredItemHeight"
|
android:layout_height="?android:attr/searchResultListItemHeight"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:baselineAligned="false"
|
android:baselineAligned="false"
|
||||||
>
|
>
|
||||||
|
|
||||||
<ImageView android:id="@android:id/icon1"
|
<ImageView android:id="@android:id/icon1"
|
||||||
android:layout_width="32dip"
|
android:layout_width="48dip"
|
||||||
android:layout_height="32dip"
|
android:layout_height="48dip"
|
||||||
android:layout_gravity="center_vertical"
|
android:layout_gravity="center_vertical"
|
||||||
android:scaleType="fitCenter" />
|
android:scaleType="centerInside" />
|
||||||
|
|
||||||
<TextView android:id="@android:id/text1"
|
<TextView android:id="@android:id/text1"
|
||||||
style="?android:attr/dropDownItemStyle"
|
style="?android:attr/dropDownItemStyle"
|
||||||
android:textAppearance="?android:attr/textAppearanceMediumInverse"
|
android:textAppearance="?android:attr/textAppearanceSearchResultTitle"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_width="0dip"
|
android:layout_width="0dip"
|
||||||
android:layout_weight="1" />
|
android:layout_weight="1" />
|
||||||
|
|
||||||
<ImageView android:id="@android:id/icon2"
|
<ImageView android:id="@android:id/icon2"
|
||||||
android:layout_width="32dip"
|
android:layout_width="48dip"
|
||||||
android:layout_height="32dip"
|
android:layout_height="48dip"
|
||||||
android:layout_gravity="center_vertical"
|
android:layout_gravity="center_vertical"
|
||||||
android:scaleType="fitCenter" />
|
android:scaleType="centerInside" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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
|
** Copyright 2008, The Android Open Source Project
|
||||||
**
|
**
|
||||||
@ -18,56 +18,62 @@
|
|||||||
*/
|
*/
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<!-- NOTE: The appearance of the inner text element must match the appearance -->
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
<!-- of the text element in apps/common/res/layout/simple_dropdown_item_2line.xml -->
|
android:paddingLeft="4dip"
|
||||||
|
android:paddingRight="2dip"
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="fill_parent"
|
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"
|
|
||||||
>
|
|
||||||
|
|
||||||
|
<!-- 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"
|
<ImageView android:id="@android:id/icon1"
|
||||||
android:layout_width="32dip"
|
android:layout_width="48dip"
|
||||||
android:layout_height="32dip"
|
android:layout_height="48dip"
|
||||||
android:layout_gravity="center_vertical"
|
android:scaleType="centerInside"
|
||||||
android:scaleType="fitCenter" />
|
android:layout_alignParentLeft="true"
|
||||||
|
android:layout_alignParentTop="true"
|
||||||
<TwoLineListItem
|
android:layout_alignParentBottom="true"
|
||||||
android:paddingTop="2dip"
|
android:visibility="gone" />
|
||||||
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>
|
|
||||||
|
|
||||||
<ImageView android:id="@android:id/icon2"
|
<ImageView android:id="@android:id/icon2"
|
||||||
android:layout_width="32dip"
|
android:layout_width="48dip"
|
||||||
android:layout_height="32dip"
|
android:layout_height="48dip"
|
||||||
android:layout_gravity="center_vertical"
|
android:scaleType="centerInside"
|
||||||
android:scaleType="fitCenter" />
|
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. -->
|
<!-- Text color, typeface, size, and style for "small" inverse text. Defaults to secondary inverse text color. -->
|
||||||
<attr name="textAppearanceSmallInverse" format="reference" />
|
<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. -->
|
<!-- Text color, typeface, size, and style for the text inside of a button. -->
|
||||||
<attr name="textAppearanceButton" format="reference" />
|
<attr name="textAppearanceButton" format="reference" />
|
||||||
|
|
||||||
@ -147,6 +152,8 @@
|
|||||||
<!-- The preferred list item height -->
|
<!-- The preferred list item height -->
|
||||||
<attr name="listPreferredItemHeight" format="dimension" />
|
<attr name="listPreferredItemHeight" format="dimension" />
|
||||||
<!-- The drawable for the list divider -->
|
<!-- The drawable for the list divider -->
|
||||||
|
<!-- The list item height for search results. @hide -->
|
||||||
|
<attr name="searchResultListItemHeight" format="dimension" />
|
||||||
<attr name="listDivider" format="reference" />
|
<attr name="listDivider" format="reference" />
|
||||||
<!-- TextView style for list separators. -->
|
<!-- TextView style for list separators. -->
|
||||||
<attr name="listSeparatorTextViewStyle" format="reference" />
|
<attr name="listSeparatorTextViewStyle" format="reference" />
|
||||||
|
@ -138,6 +138,7 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<!-- Window animations that are applied to the search bar overlay window.
|
<!-- Window animations that are applied to the search bar overlay window.
|
||||||
|
Previously used, but currently unused.
|
||||||
{@hide Pending API council approval} -->
|
{@hide Pending API council approval} -->
|
||||||
<style name="Animation.SearchBar">
|
<style name="Animation.SearchBar">
|
||||||
<item name="windowEnterAnimation">@anim/search_bar_enter</item>
|
<item name="windowEnterAnimation">@anim/search_bar_enter</item>
|
||||||
@ -574,6 +575,24 @@
|
|||||||
<item name="android:textColor">@android:color/primary_text_light_disable_only</item>
|
<item name="android:textColor">@android:color/primary_text_light_disable_only</item>
|
||||||
</style>
|
</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">
|
<style name="TextAppearance.WindowTitle">
|
||||||
<item name="android:textColor">#fff</item>
|
<item name="android:textColor">#fff</item>
|
||||||
<item name="android:textSize">14sp</item>
|
<item name="android:textSize">14sp</item>
|
||||||
|
@ -58,6 +58,12 @@
|
|||||||
<item name="textAppearanceMediumInverse">@android:style/TextAppearance.Medium.Inverse</item>
|
<item name="textAppearanceMediumInverse">@android:style/TextAppearance.Medium.Inverse</item>
|
||||||
<item name="textAppearanceSmallInverse">@android:style/TextAppearance.Small.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>
|
<item name="textAppearanceButton">@android:style/TextAppearance.Widget.Button</item>
|
||||||
|
|
||||||
<item name="candidatesTextStyleSpans">@android:string/candidates_style</item>
|
<item name="candidatesTextStyleSpans">@android:string/candidates_style</item>
|
||||||
@ -75,6 +81,8 @@
|
|||||||
|
|
||||||
<!-- List attributes -->
|
<!-- List attributes -->
|
||||||
<item name="listPreferredItemHeight">64dip</item>
|
<item name="listPreferredItemHeight">64dip</item>
|
||||||
|
<!-- @hide -->
|
||||||
|
<item name="searchResultListItemHeight">58dip</item>
|
||||||
<item name="listDivider">@drawable/divider_horizontal_dark</item>
|
<item name="listDivider">@drawable/divider_horizontal_dark</item>
|
||||||
<item name="listSeparatorTextViewStyle">@android:style/Widget.TextView.ListSeparator</item>
|
<item name="listSeparatorTextViewStyle">@android:style/Widget.TextView.ListSeparator</item>
|
||||||
|
|
||||||
@ -355,7 +363,6 @@
|
|||||||
<!-- Theme for the search input bar. -->
|
<!-- Theme for the search input bar. -->
|
||||||
<style name="Theme.SearchBar" parent="Theme.Panel">
|
<style name="Theme.SearchBar" parent="Theme.Panel">
|
||||||
<item name="android:backgroundDimEnabled">true</item>
|
<item name="android:backgroundDimEnabled">true</item>
|
||||||
<item name="android:windowAnimationStyle">@android:style/Animation.SearchBar</item>
|
|
||||||
<item name="windowContentOverlay">@null</item>
|
<item name="windowContentOverlay">@null</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
@ -23,27 +23,11 @@ import android.app.ISearchManager;
|
|||||||
import android.app.SearchManager;
|
import android.app.SearchManager;
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
import android.content.Context;
|
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.os.ServiceManager;
|
||||||
import android.server.search.SearchableInfo;
|
import android.test.ActivityInstrumentationTestCase2;
|
||||||
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.suitebuilder.annotation.LargeTest;
|
import android.test.suitebuilder.annotation.LargeTest;
|
||||||
import android.test.suitebuilder.annotation.MediumTest;
|
import android.test.suitebuilder.annotation.MediumTest;
|
||||||
import android.util.AndroidRuntimeException;
|
import android.util.AndroidRuntimeException;
|
||||||
import android.view.KeyEvent;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* To launch this test from the command line:
|
* To launch this test from the command line:
|
||||||
@ -52,7 +36,7 @@ import java.util.List;
|
|||||||
* -e class com.android.unit_tests.SearchManagerTest \
|
* -e class com.android.unit_tests.SearchManagerTest \
|
||||||
* com.android.unit_tests/android.test.InstrumentationTestRunner
|
* 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.
|
// 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
|
// This is currently disabled because it's causing an unwanted jump from the unit test
|
||||||
@ -72,18 +56,6 @@ public class SearchManagerTest extends ActivityInstrumentationTestCase<LocalActi
|
|||||||
* FIX - make it work again
|
* FIX - make it work again
|
||||||
* stress test with a very long string
|
* 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
|
* SearchManager tests
|
||||||
* confirm proper identification of "default" activity based on policy, not hardcoded contacts
|
* confirm proper identification of "default" activity based on policy, not hardcoded contacts
|
||||||
*
|
*
|
||||||
@ -196,347 +168,5 @@ public class SearchManagerTest extends ActivityInstrumentationTestCase<LocalActi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
// now try moving "down" - nothing should happen since there's no longer an adapter
|
||||||
sendKeys("DPAD_DOWN");
|
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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|