Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 875d50a4 authored by Karl Rosaen's avatar Karl Rosaen Committed by The Android Open Source Project
Browse files

AI 147564: Merge back from search branch to donut. Notes:

  - all public apis and framework changes have been reviewed by relevant folks in our branch (e.g romainguy)
  - all new public apis are @hidden; they will still get reviewed by api council once we're in git
  - other than that, it's mostly GlobalSearch and search dialog stuff, a new apps provider, and some tweaks
  to the contacts provider that was reviewed by jham

Automated import of CL 147564
parent b08971b8
Loading
Loading
Loading
Loading
+687 −827

File changed.

Preview size limit exceeded, changes collapsed.

+205 −19
Original line number Diff line number Diff line
@@ -17,12 +17,17 @@
package android.app;

import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.Configuration;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.server.search.SearchableInfo;
import android.view.KeyEvent;

/**
@@ -439,20 +444,18 @@ import android.view.KeyEvent;
 *     
 *     <tr><th>{@link #SUGGEST_COLUMN_ICON_1}</th>
 *         <td>If your cursor includes this column, then all suggestions will be provided in an
 *             icons+text format.  This value should be a reference (resource ID) of the icon to
 *             icons+text format.  This value should be a reference to the icon to
 *             draw on the left side, or it can be null or zero to indicate no icon in this row.
 *             You must provide both cursor columns, or neither.
 *             </td>
 *         <td align="center">No, but required if you also have {@link #SUGGEST_COLUMN_ICON_2}</td>
 *         <td align="center">No.</td>
 *     </tr>
 *     
 *     <tr><th>{@link #SUGGEST_COLUMN_ICON_2}</th>
 *         <td>If your cursor includes this column, then all suggestions will be provided in an
 *             icons+text format.  This value should be a reference (resource ID) of the icon to
 *             icons+text format.  This value should be a reference to the icon to
 *             draw on the right side, or it can be null or zero to indicate no icon in this row.
 *             You must provide both cursor columns, or neither.
 *             </td>
 *         <td align="center">No, but required if you also have {@link #SUGGEST_COLUMN_ICON_1}</td>
 *         <td align="center">No.</td>
 *     </tr>
 *     
 *     <tr><th>{@link #SUGGEST_COLUMN_INTENT_ACTION}</th>
@@ -1154,6 +1157,13 @@ public class SearchManager
     */
    public final static String ACTION_KEY = "action_key";
    
    /**
     * Intent extra data key: This key will be used for the extra populated by the
     * {@link #SUGGEST_COLUMN_INTENT_EXTRA_DATA} column.
     * {@hide}
     */
    public final static String EXTRA_DATA_KEY = "intent_extra_data_key";
    
    /**
     * Intent extra data key: Use this key with Intent.ACTION_SEARCH and
     * {@link android.content.Intent#getStringExtra content.Intent.getStringExtra()}
@@ -1195,20 +1205,58 @@ public class SearchManager
    public final static String SUGGEST_COLUMN_TEXT_2 = "suggest_text_2";
    /**
     * Column name for suggestions cursor.  <i>Optional.</i>  If your cursor includes this column,
     *  then all suggestions will be provided in format that includes space for two small icons,
     *  then all suggestions will be provided in a format that includes space for two small icons,
     *  one at the left and one at the right of each suggestion.  The data in the column must
     *  be a a resource ID for the icon you wish to have displayed.  If you include this column,
     *  you must also include {@link #SUGGEST_COLUMN_ICON_2}.
     *  be a resource ID of a drawable, or a URI in one of the following formats:
     *
     * <ul>
     * <li>content ({@link android.content.ContentResolver#SCHEME_CONTENT})</li>
     * <li>android.resource ({@link android.content.ContentResolver#SCHEME_ANDROID_RESOURCE})</li>
     * <li>file ({@link android.content.ContentResolver#SCHEME_FILE})</li>
     * </ul>
     *
     * See {@link android.content.ContentResolver#openAssetFileDescriptor(Uri, String)} 
     * for more information on these schemes. 
     */
    public final static String SUGGEST_COLUMN_ICON_1 = "suggest_icon_1";
    /**
     * Column name for suggestions cursor.  <i>Optional.</i>  If your cursor includes this column,
     *  then all suggestions will be provided in format that includes space for two small icons,
     *  then all suggestions will be provided in a format that includes space for two small icons,
     *  one at the left and one at the right of each suggestion.  The data in the column must
     *  be a a resource ID for the icon you wish to have displayed.  If you include this column,
     *  you must also include {@link #SUGGEST_COLUMN_ICON_1}.
     *  be a resource ID of a drawable, or a URI in one of the following formats:
     *
     * <ul>
     * <li>content ({@link android.content.ContentResolver#SCHEME_CONTENT})</li>
     * <li>android.resource ({@link android.content.ContentResolver#SCHEME_ANDROID_RESOURCE})</li>
     * <li>file ({@link android.content.ContentResolver#SCHEME_FILE})</li>
     * </ul>
     *
     * See {@link android.content.ContentResolver#openAssetFileDescriptor(Uri, String)} 
     * for more information on these schemes. 
     */
    public final static String SUGGEST_COLUMN_ICON_2 = "suggest_icon_2";
    /**
     * Column name for suggestions cursor.  <i>Optional.</i>  If your cursor includes this column,
     *  then all suggestions will be provided in a format that includes space for two small icons,
     *  one at the left and one at the right of each suggestion.  The data in the column must
     *  be a blob that contains a bitmap.
     * 
     * This column overrides any icon provided in the {@link #SUGGEST_COLUMN_ICON_1} column.
     *
     * @hide
     */
    public final static String SUGGEST_COLUMN_ICON_1_BITMAP = "suggest_icon_1_bitmap";
    /**
     * Column name for suggestions cursor.  <i>Optional.</i>  If your cursor includes this column,
     *  then all suggestions will be provided in a format that includes space for two small icons,
     *  one at the left and one at the right of each suggestion.  The data in the column must
     *  be a blob that contains a bitmap.
     * 
     * This column overrides any icon provided in the {@link #SUGGEST_COLUMN_ICON_2} column.
     *
     * @hide
     */
    public final static String SUGGEST_COLUMN_ICON_2_BITMAP = "suggest_icon_2_bitmap";
    /**
     * Column name for suggestions cursor.  <i>Optional.</i>  If this column exists <i>and</i>
     * this element exists at the given row, this is the action that will be used when
@@ -1229,6 +1277,14 @@ public class SearchManager
     * it is more efficient to specify it using XML metadata and omit it from the cursor.
     */
    public final static String SUGGEST_COLUMN_INTENT_DATA = "suggest_intent_data";
    /**
     * Column name for suggestions cursor.  <i>Optional.</i>  This column allows suggestions
     *  to provide additional arbitrary data which will be included as an extra under the key
     *  {@link #EXTRA_DATA_KEY}.
     * 
     * @hide pending API council approval
     */
    public final static String SUGGEST_COLUMN_INTENT_EXTRA_DATA = "suggest_intent_extra_data";
    /**
     * Column name for suggestions cursor.  <i>Optional.</i>  If this column exists <i>and</i>
     * this element exists at the given row, then "/" and this value will be appended to the data
@@ -1244,6 +1300,54 @@ public class SearchManager
     */
    public final static String SUGGEST_COLUMN_QUERY = "suggest_intent_query";

    /**
     * If a suggestion has this value in {@link #SUGGEST_COLUMN_INTENT_ACTION},
     * the search dialog will switch to a different suggestion source when the
     * suggestion is clicked. 
     * 
     * {@link #SUGGEST_COLUMN_INTENT_DATA} must contain
     * the flattened {@link ComponentName} of the activity which is to be searched.
     * 
     * TODO: Should {@link #SUGGEST_COLUMN_INTENT_DATA} instead contain a URI in the format
     * used by {@link android.provider.Applications}?
     * 
     * TODO: This intent should be protected by the same permission that we use
     * for replacing the global search provider.
     * 
     * The query text field will be set to the value of {@link #SUGGEST_COLUMN_QUERY}.
     * 
     * @hide Pending API council approval.
     */
    public final static String INTENT_ACTION_CHANGE_SEARCH_SOURCE 
            = "android.search.action.CHANGE_SEARCH_SOURCE";
    
    /**
     * If a suggestion has this value in {@link #SUGGEST_COLUMN_INTENT_ACTION},
     * the search dialog will call {@link Cursor#respond(Bundle)} when the
     * suggestion is clicked. 
     * 
     * The {@link Bundle} argument will be constructed
     * in the same way as the "extra" bundle included in an Intent constructed 
     * from the suggestion.
     * 
     * @hide Pending API council approval.
     */
    public final static String INTENT_ACTION_CURSOR_RESPOND
            = "android.search.action.CURSOR_RESPOND";
    
    /**
     * Intent action for starting the global search settings activity.
     * The global search provider should handle this intent.
     * 
     * @hide Pending API council approval.
     */
    public final static String INTENT_ACTION_SEARCH_SETTINGS 
            = "android.search.action.SEARCH_SETTINGS";
    
    /**
     * Reference to the shared system search service.
     */
    private static ISearchManager sService = getSearchManagerService();

    private final Context mContext;
    private final Handler mHandler;
@@ -1257,12 +1361,6 @@ public class SearchManager
        mContext = context;
        mHandler = handler;
    }
    private static ISearchManager mService;

    static {
        mService = ISearchManager.Stub.asInterface(
                    ServiceManager.getService(Context.SEARCH_SERVICE));
    }
    
    /**
     * Launch search UI.
@@ -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 −0
Original line number Diff line number Diff line
/*
 * 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 −0

File added.

Preview size limit exceeded, changes collapsed.

+29 −20

File changed.

Preview size limit exceeded, changes collapsed.

Loading