1package android.support.v7.widget;
2
3/*
4 * Copyright (C) 2013 The Android Open Source Project
5 *
6 * Licensed under the Apache License, Version 2.0 (the "License");
7 * you may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
9 *
10 *      http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
17 */
18
19import android.app.SearchManager;
20import android.app.SearchableInfo;
21import android.content.ComponentName;
22import android.content.ContentResolver;
23import android.content.Context;
24import android.content.pm.ActivityInfo;
25import android.content.pm.PackageManager;
26import android.content.pm.PackageManager.NameNotFoundException;
27import android.content.res.ColorStateList;
28import android.content.res.Resources;
29import android.database.Cursor;
30import android.graphics.drawable.Drawable;
31import android.net.Uri;
32import android.os.Bundle;
33import android.support.v4.content.ContextCompat;
34import android.support.v4.widget.ResourceCursorAdapter;
35import android.support.v7.appcompat.R;
36import android.text.Spannable;
37import android.text.SpannableString;
38import android.text.TextUtils;
39import android.text.style.TextAppearanceSpan;
40import android.util.Log;
41import android.util.TypedValue;
42import android.view.View;
43import android.view.View.OnClickListener;
44import android.view.ViewGroup;
45import android.widget.ImageView;
46import android.widget.TextView;
47
48import java.io.FileNotFoundException;
49import java.io.IOException;
50import java.io.InputStream;
51import java.util.List;
52import java.util.WeakHashMap;
53
54/**
55 * Provides the contents for the suggestion drop-down list.in {@link SearchView}.
56 * @hide
57 */
58class SuggestionsAdapter extends ResourceCursorAdapter implements OnClickListener {
59
60    private static final boolean DBG = false;
61    private static final String LOG_TAG = "SuggestionsAdapter";
62    private static final int QUERY_LIMIT = 50;
63
64    static final int REFINE_NONE = 0;
65    static final int REFINE_BY_ENTRY = 1;
66    static final int REFINE_ALL = 2;
67
68    private final SearchManager mSearchManager;
69    private final SearchView mSearchView;
70    private final SearchableInfo mSearchable;
71    private final Context mProviderContext;
72    private final WeakHashMap<String, Drawable.ConstantState> mOutsideDrawablesCache;
73    private final int mCommitIconResId;
74    private boolean mClosed = false;
75    private int mQueryRefinement = REFINE_BY_ENTRY;
76
77    // URL color
78    private ColorStateList mUrlColor;
79
80    static final int INVALID_INDEX = -1;
81
82    // Cached column indexes, updated when the cursor changes.
83    private int mText1Col = INVALID_INDEX;
84    private int mText2Col = INVALID_INDEX;
85    private int mText2UrlCol = INVALID_INDEX;
86    private int mIconName1Col = INVALID_INDEX;
87    private int mIconName2Col = INVALID_INDEX;
88    private int mFlagsCol = INVALID_INDEX;
89
90    // private final Runnable mStartSpinnerRunnable;
91    // private final Runnable mStopSpinnerRunnable;
92
93    public SuggestionsAdapter(Context context, SearchView searchView, SearchableInfo searchable,
94            WeakHashMap<String, Drawable.ConstantState> outsideDrawablesCache) {
95        super(context, searchView.getSuggestionRowLayout(), null /* no initial cursor */,
96                true /* auto-requery */);
97        mSearchManager = (SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE);
98        mSearchView = searchView;
99        mSearchable = searchable;
100        mCommitIconResId = searchView.getSuggestionCommitIconResId();
101
102        // set up provider resources (gives us icons, etc.)
103        mProviderContext = context;
104
105        mOutsideDrawablesCache = outsideDrawablesCache;
106    }
107
108    /**
109     * Enables query refinement for all suggestions. This means that an additional icon
110     * will be shown for each entry. When clicked, the suggested text on that line will be
111     * copied to the query text field.
112     * <p>
113     *
114     * @param refine which queries to refine. Possible values are {@link #REFINE_NONE},
115     * {@link #REFINE_BY_ENTRY}, and {@link #REFINE_ALL}.
116     */
117    public void setQueryRefinement(int refineWhat) {
118        mQueryRefinement = refineWhat;
119    }
120
121    /**
122     * Returns the current query refinement preference.
123     * @return value of query refinement preference
124     */
125    public int getQueryRefinement() {
126        return mQueryRefinement;
127    }
128
129    /**
130     * Overridden to always return <code>false</code>, since we cannot be sure that
131     * suggestion sources return stable IDs.
132     */
133    @Override
134    public boolean hasStableIds() {
135        return false;
136    }
137
138    /**
139     * Use the search suggestions provider to obtain a live cursor.  This will be called
140     * in a worker thread, so it's OK if the query is slow (e.g. round trip for suggestions).
141     * The results will be processed in the UI thread and changeCursor() will be called.
142     */
143    @Override
144    public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
145        if (DBG) Log.d(LOG_TAG, "runQueryOnBackgroundThread(" + constraint + ")");
146        String query = (constraint == null) ? "" : constraint.toString();
147        /**
148         * for in app search we show the progress spinner until the cursor is returned with
149         * the results.
150         */
151        Cursor cursor = null;
152        if (mSearchView.getVisibility() != View.VISIBLE
153                || mSearchView.getWindowVisibility() != View.VISIBLE) {
154            return null;
155        }
156        try {
157            cursor = getSearchManagerSuggestions(mSearchable, query, QUERY_LIMIT);
158            // trigger fill window so the spinner stays up until the results are copied over and
159            // closer to being ready
160            if (cursor != null) {
161                cursor.getCount();
162                return cursor;
163            }
164        } catch (RuntimeException e) {
165            Log.w(LOG_TAG, "Search suggestions query threw an exception.", e);
166        }
167        // If cursor is null or an exception was thrown, stop the spinner and return null.
168        // changeCursor doesn't get called if cursor is null
169        return null;
170    }
171
172    public void close() {
173        if (DBG) Log.d(LOG_TAG, "close()");
174        changeCursor(null);
175        mClosed = true;
176    }
177
178    @Override
179    public void notifyDataSetChanged() {
180        if (DBG) Log.d(LOG_TAG, "notifyDataSetChanged");
181        super.notifyDataSetChanged();
182
183        updateSpinnerState(getCursor());
184    }
185
186    @Override
187    public void notifyDataSetInvalidated() {
188        if (DBG) Log.d(LOG_TAG, "notifyDataSetInvalidated");
189        super.notifyDataSetInvalidated();
190
191        updateSpinnerState(getCursor());
192    }
193
194    private void updateSpinnerState(Cursor cursor) {
195        Bundle extras = cursor != null ? cursor.getExtras() : null;
196        if (DBG) {
197            Log.d(LOG_TAG, "updateSpinnerState - extra = "
198                + (extras != null
199                        ? extras.getBoolean(SearchManager.CURSOR_EXTRA_KEY_IN_PROGRESS)
200                        : null));
201        }
202        // Check if the Cursor indicates that the query is not complete and show the spinner
203        if (extras != null
204                && extras.getBoolean(SearchManager.CURSOR_EXTRA_KEY_IN_PROGRESS)) {
205            return;
206        }
207        // If cursor is null or is done, stop the spinner
208    }
209
210    /**
211     * Cache columns.
212     */
213    @Override
214    public void changeCursor(Cursor c) {
215        if (DBG) Log.d(LOG_TAG, "changeCursor(" + c + ")");
216
217        if (mClosed) {
218            Log.w(LOG_TAG, "Tried to change cursor after adapter was closed.");
219            if (c != null) c.close();
220            return;
221        }
222
223        try {
224            super.changeCursor(c);
225
226            if (c != null) {
227                mText1Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1);
228                mText2Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2);
229                mText2UrlCol = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2_URL);
230                mIconName1Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_1);
231                mIconName2Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_2);
232                mFlagsCol = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_FLAGS);
233            }
234        } catch (Exception e) {
235            Log.e(LOG_TAG, "error changing cursor and caching columns", e);
236        }
237    }
238
239    /**
240     * Tags the view with cached child view look-ups.
241     */
242    @Override
243    public View newView(Context context, Cursor cursor, ViewGroup parent) {
244        final View v = super.newView(context, cursor, parent);
245        v.setTag(new ChildViewCache(v));
246
247        // Set up icon.
248        final ImageView iconRefine = (ImageView) v.findViewById(R.id.edit_query);
249        iconRefine.setImageResource(mCommitIconResId);
250        return v;
251    }
252
253    /**
254     * Cache of the child views of drop-drown list items, to avoid looking up the children
255     * each time the contents of a list item are changed.
256     */
257    private final static class ChildViewCache {
258        public final TextView mText1;
259        public final TextView mText2;
260        public final ImageView mIcon1;
261        public final ImageView mIcon2;
262        public final ImageView mIconRefine;
263
264        public ChildViewCache(View v) {
265            mText1 = (TextView) v.findViewById(android.R.id.text1);
266            mText2 = (TextView) v.findViewById(android.R.id.text2);
267            mIcon1 = (ImageView) v.findViewById(android.R.id.icon1);
268            mIcon2 = (ImageView) v.findViewById(android.R.id.icon2);
269            mIconRefine = (ImageView) v.findViewById(R.id.edit_query);
270        }
271    }
272
273    @Override
274    public void bindView(View view, Context context, Cursor cursor) {
275        ChildViewCache views = (ChildViewCache) view.getTag();
276
277        int flags = 0;
278        if (mFlagsCol != INVALID_INDEX) {
279            flags = cursor.getInt(mFlagsCol);
280        }
281        if (views.mText1 != null) {
282            String text1 = getStringOrNull(cursor, mText1Col);
283            setViewText(views.mText1, text1);
284        }
285        if (views.mText2 != null) {
286            // First check TEXT_2_URL
287            CharSequence text2 = getStringOrNull(cursor, mText2UrlCol);
288            if (text2 != null) {
289                text2 = formatUrl(text2);
290            } else {
291                text2 = getStringOrNull(cursor, mText2Col);
292            }
293
294            // If no second line of text is indicated, allow the first line of text
295            // to be up to two lines if it wants to be.
296            if (TextUtils.isEmpty(text2)) {
297                if (views.mText1 != null) {
298                    views.mText1.setSingleLine(false);
299                    views.mText1.setMaxLines(2);
300                }
301            } else {
302                if (views.mText1 != null) {
303                    views.mText1.setSingleLine(true);
304                    views.mText1.setMaxLines(1);
305                }
306            }
307            setViewText(views.mText2, text2);
308        }
309
310        if (views.mIcon1 != null) {
311            setViewDrawable(views.mIcon1, getIcon1(cursor), View.INVISIBLE);
312        }
313        if (views.mIcon2 != null) {
314            setViewDrawable(views.mIcon2, getIcon2(cursor), View.GONE);
315        }
316        if (mQueryRefinement == REFINE_ALL
317                || (mQueryRefinement == REFINE_BY_ENTRY
318                        && (flags & SearchManager.FLAG_QUERY_REFINEMENT) != 0)) {
319            views.mIconRefine.setVisibility(View.VISIBLE);
320            views.mIconRefine.setTag(views.mText1.getText());
321            views.mIconRefine.setOnClickListener(this);
322        } else {
323            views.mIconRefine.setVisibility(View.GONE);
324        }
325    }
326
327    public void onClick(View v) {
328        Object tag = v.getTag();
329        if (tag instanceof CharSequence) {
330            mSearchView.onQueryRefine((CharSequence) tag);
331        }
332    }
333
334    private CharSequence formatUrl(CharSequence url) {
335        if (mUrlColor == null) {
336            // Lazily get the URL color from the current theme.
337            TypedValue colorValue = new TypedValue();
338            mContext.getTheme().resolveAttribute(R.attr.textColorSearchUrl, colorValue, true);
339            mUrlColor = mContext.getResources().getColorStateList(colorValue.resourceId);
340        }
341
342        SpannableString text = new SpannableString(url);
343        text.setSpan(new TextAppearanceSpan(null, 0, 0, mUrlColor, null),
344                0, url.length(),
345                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
346        return text;
347    }
348
349    private void setViewText(TextView v, CharSequence text) {
350        // Set the text even if it's null, since we need to clear any previous text.
351        v.setText(text);
352
353        if (TextUtils.isEmpty(text)) {
354            v.setVisibility(View.GONE);
355        } else {
356            v.setVisibility(View.VISIBLE);
357        }
358    }
359
360    private Drawable getIcon1(Cursor cursor) {
361        if (mIconName1Col == INVALID_INDEX) {
362            return null;
363        }
364        String value = cursor.getString(mIconName1Col);
365        Drawable drawable = getDrawableFromResourceValue(value);
366        if (drawable != null) {
367            return drawable;
368        }
369        return getDefaultIcon1(cursor);
370    }
371
372    private Drawable getIcon2(Cursor cursor) {
373        if (mIconName2Col == INVALID_INDEX) {
374            return null;
375        }
376        String value = cursor.getString(mIconName2Col);
377        return getDrawableFromResourceValue(value);
378    }
379
380    /**
381     * Sets the drawable in an image view, makes sure the view is only visible if there
382     * is a drawable.
383     */
384    private void setViewDrawable(ImageView v, Drawable drawable, int nullVisibility) {
385        // Set the icon even if the drawable is null, since we need to clear any
386        // previous icon.
387        v.setImageDrawable(drawable);
388
389        if (drawable == null) {
390            v.setVisibility(nullVisibility);
391        } else {
392            v.setVisibility(View.VISIBLE);
393
394            // This is a hack to get any animated drawables (like a 'working' spinner)
395            // to animate. You have to setVisible true on an AnimationDrawable to get
396            // it to start animating, but it must first have been false or else the
397            // call to setVisible will be ineffective. We need to clear up the story
398            // about animated drawables in the future, see http://b/1878430.
399            drawable.setVisible(false, false);
400            drawable.setVisible(true, false);
401        }
402    }
403
404    /**
405     * Gets the text to show in the query field when a suggestion is selected.
406     *
407     * @param cursor The Cursor to read the suggestion data from. The Cursor should already
408     *        be moved to the suggestion that is to be read from.
409     * @return The text to show, or <code>null</code> if the query should not be
410     *         changed when selecting this suggestion.
411     */
412    @Override
413    public CharSequence convertToString(Cursor cursor) {
414        if (cursor == null) {
415            return null;
416        }
417
418        String query = getColumnString(cursor, SearchManager.SUGGEST_COLUMN_QUERY);
419        if (query != null) {
420            return query;
421        }
422
423        if (mSearchable.shouldRewriteQueryFromData()) {
424            String data = getColumnString(cursor, SearchManager.SUGGEST_COLUMN_INTENT_DATA);
425            if (data != null) {
426                return data;
427            }
428        }
429
430        if (mSearchable.shouldRewriteQueryFromText()) {
431            String text1 = getColumnString(cursor, SearchManager.SUGGEST_COLUMN_TEXT_1);
432            if (text1 != null) {
433                return text1;
434            }
435        }
436
437        return null;
438    }
439
440    /**
441     * This method is overridden purely to provide a bit of protection against
442     * flaky content providers.
443     *
444     * @see android.widget.ListAdapter#getView(int, View, ViewGroup)
445     */
446    @Override
447    public View getView(int position, View convertView, ViewGroup parent) {
448        try {
449            return super.getView(position, convertView, parent);
450        } catch (RuntimeException e) {
451            Log.w(LOG_TAG, "Search suggestions cursor threw exception.", e);
452            // Put exception string in item title
453            View v = newView(mContext, mCursor, parent);
454            if (v != null) {
455                ChildViewCache views = (ChildViewCache) v.getTag();
456                TextView tv = views.mText1;
457                tv.setText(e.toString());
458            }
459            return v;
460        }
461    }
462
463    /**
464     * Gets a drawable given a value provided by a suggestion provider.
465     *
466     * This value could be just the string value of a resource id
467     * (e.g., "2130837524"), in which case we will try to retrieve a drawable from
468     * the provider's resources. If the value is not an integer, it is
469     * treated as a Uri and opened with
470     * {@link ContentResolver#openOutputStream(android.net.Uri, String)}.
471     *
472     * All resources and URIs are read using the suggestion provider's context.
473     *
474     * If the string is not formatted as expected, or no drawable can be found for
475     * the provided value, this method returns null.
476     *
477     * @param drawableId a string like "2130837524",
478     *        "android.resource://com.android.alarmclock/2130837524",
479     *        or "content://contacts/photos/253".
480     * @return a Drawable, or null if none found
481     */
482    private Drawable getDrawableFromResourceValue(String drawableId) {
483        if (drawableId == null || drawableId.length() == 0 || "0".equals(drawableId)) {
484            return null;
485        }
486        try {
487            // First, see if it's just an integer
488            int resourceId = Integer.parseInt(drawableId);
489            // It's an int, look for it in the cache
490            String drawableUri = ContentResolver.SCHEME_ANDROID_RESOURCE
491                    + "://" + mProviderContext.getPackageName() + "/" + resourceId;
492            // Must use URI as cache key, since ints are app-specific
493            Drawable drawable = checkIconCache(drawableUri);
494            if (drawable != null) {
495                return drawable;
496            }
497            // Not cached, find it by resource ID
498            drawable = ContextCompat.getDrawable(mProviderContext, resourceId);
499            // Stick it in the cache, using the URI as key
500            storeInIconCache(drawableUri, drawable);
501            return drawable;
502        } catch (NumberFormatException nfe) {
503            // It's not an integer, use it as a URI
504            Drawable drawable = checkIconCache(drawableId);
505            if (drawable != null) {
506                return drawable;
507            }
508            Uri uri = Uri.parse(drawableId);
509            drawable = getDrawable(uri);
510            storeInIconCache(drawableId, drawable);
511            return drawable;
512        } catch (Resources.NotFoundException nfe) {
513            // It was an integer, but it couldn't be found, bail out
514            Log.w(LOG_TAG, "Icon resource not found: " + drawableId);
515            return null;
516        }
517    }
518
519    /**
520     * Gets a drawable by URI, without using the cache.
521     *
522     * @return A drawable, or {@code null} if the drawable could not be loaded.
523     */
524    private Drawable getDrawable(Uri uri) {
525        try {
526            String scheme = uri.getScheme();
527            if (ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme)) {
528                // Load drawables through Resources, to get the source density information
529                try {
530                    return getDrawableFromResourceUri(uri);
531                } catch (Resources.NotFoundException ex) {
532                    throw new FileNotFoundException("Resource does not exist: " + uri);
533                }
534            } else {
535                // Let the ContentResolver handle content and file URIs.
536                InputStream stream = mProviderContext.getContentResolver().openInputStream(uri);
537                if (stream == null) {
538                    throw new FileNotFoundException("Failed to open " + uri);
539                }
540                try {
541                    return Drawable.createFromStream(stream, null);
542                } finally {
543                    try {
544                        stream.close();
545                    } catch (IOException ex) {
546                        Log.e(LOG_TAG, "Error closing icon stream for " + uri, ex);
547                    }
548                }
549            }
550        } catch (FileNotFoundException fnfe) {
551            Log.w(LOG_TAG, "Icon not found: " + uri + ", " + fnfe.getMessage());
552            return null;
553        }
554    }
555
556
557
558    private Drawable checkIconCache(String resourceUri) {
559        Drawable.ConstantState cached = mOutsideDrawablesCache.get(resourceUri);
560        if (cached == null) {
561            return null;
562        }
563        if (DBG) Log.d(LOG_TAG, "Found icon in cache: " + resourceUri);
564        return cached.newDrawable();
565    }
566
567    private void storeInIconCache(String resourceUri, Drawable drawable) {
568        if (drawable != null) {
569            mOutsideDrawablesCache.put(resourceUri, drawable.getConstantState());
570        }
571    }
572
573    /**
574     * Gets the left-hand side icon that will be used for the current suggestion
575     * if the suggestion contains an icon column but no icon or a broken icon.
576     *
577     * @param cursor A cursor positioned at the current suggestion.
578     * @return A non-null drawable.
579     */
580    private Drawable getDefaultIcon1(Cursor cursor) {
581        // Check the component that gave us the suggestion
582        Drawable drawable = getActivityIconWithCache(mSearchable.getSearchActivity());
583        if (drawable != null) {
584            return drawable;
585        }
586
587        // Fall back to a default icon
588        return mContext.getPackageManager().getDefaultActivityIcon();
589    }
590
591    /**
592     * Gets the activity or application icon for an activity.
593     * Uses the local icon cache for fast repeated lookups.
594     *
595     * @param component Name of an activity.
596     * @return A drawable, or {@code null} if neither the activity nor the application
597     *         has an icon set.
598     */
599    private Drawable getActivityIconWithCache(ComponentName component) {
600        // First check the icon cache
601        String componentIconKey = component.flattenToShortString();
602        // Using containsKey() since we also store null values.
603        if (mOutsideDrawablesCache.containsKey(componentIconKey)) {
604            Drawable.ConstantState cached = mOutsideDrawablesCache.get(componentIconKey);
605            return cached == null ? null : cached.newDrawable(mProviderContext.getResources());
606        }
607        // Then try the activity or application icon
608        Drawable drawable = getActivityIcon(component);
609        // Stick it in the cache so we don't do this lookup again.
610        Drawable.ConstantState toCache = drawable == null ? null : drawable.getConstantState();
611        mOutsideDrawablesCache.put(componentIconKey, toCache);
612        return drawable;
613    }
614
615    /**
616     * Gets the activity or application icon for an activity.
617     *
618     * @param component Name of an activity.
619     * @return A drawable, or {@code null} if neither the acitivy or the application
620     *         have an icon set.
621     */
622    private Drawable getActivityIcon(ComponentName component) {
623        PackageManager pm = mContext.getPackageManager();
624        final ActivityInfo activityInfo;
625        try {
626            activityInfo = pm.getActivityInfo(component, PackageManager.GET_META_DATA);
627        } catch (NameNotFoundException ex) {
628            Log.w(LOG_TAG, ex.toString());
629            return null;
630        }
631        int iconId = activityInfo.getIconResource();
632        if (iconId == 0) return null;
633        String pkg = component.getPackageName();
634        Drawable drawable = pm.getDrawable(pkg, iconId, activityInfo.applicationInfo);
635        if (drawable == null) {
636            Log.w(LOG_TAG, "Invalid icon resource " + iconId + " for "
637                    + component.flattenToShortString());
638            return null;
639        }
640        return drawable;
641    }
642
643    /**
644     * Gets the value of a string column by name.
645     *
646     * @param cursor Cursor to read the value from.
647     * @param columnName The name of the column to read.
648     * @return The value of the given column, or <code>null</null>
649     *         if the cursor does not contain the given column.
650     */
651    public static String getColumnString(Cursor cursor, String columnName) {
652        int col = cursor.getColumnIndex(columnName);
653        return getStringOrNull(cursor, col);
654    }
655
656    private static String getStringOrNull(Cursor cursor, int col) {
657        if (col == INVALID_INDEX) {
658            return null;
659        }
660        try {
661            return cursor.getString(col);
662        } catch (Exception e) {
663            Log.e(LOG_TAG,
664                    "unexpected error retrieving valid column from cursor, "
665                            + "did the remote process die?", e);
666            return null;
667        }
668    }
669
670    /**
671     * Import of hidden method: ContentResolver.getResourceId(Uri).
672     * Modified to return a drawable, rather than a hidden type.
673     */
674    Drawable getDrawableFromResourceUri(Uri uri) throws FileNotFoundException {
675        String authority = uri.getAuthority();
676        Resources r;
677        if (TextUtils.isEmpty(authority)) {
678            throw new FileNotFoundException("No authority: " + uri);
679        } else {
680            try {
681                r = mContext.getPackageManager().getResourcesForApplication(authority);
682            } catch (NameNotFoundException ex) {
683                throw new FileNotFoundException("No package found for authority: " + uri);
684            }
685        }
686        List<String> path = uri.getPathSegments();
687        if (path == null) {
688            throw new FileNotFoundException("No path: " + uri);
689        }
690        int len = path.size();
691        int id;
692        if (len == 1) {
693            try {
694                id = Integer.parseInt(path.get(0));
695            } catch (NumberFormatException e) {
696                throw new FileNotFoundException("Single path segment is not a resource ID: " + uri);
697            }
698        } else if (len == 2) {
699            id = r.getIdentifier(path.get(1), path.get(0), authority);
700        } else {
701            throw new FileNotFoundException("More than two path segments: " + uri);
702        }
703        if (id == 0) {
704            throw new FileNotFoundException("No resource found for: " + uri);
705        }
706        return r.getDrawable(id);
707    }
708
709    /**
710     * Import of hidden method: SearchManager.getSuggestions(SearchableInfo, String, int).
711     */
712    Cursor getSearchManagerSuggestions(SearchableInfo searchable, String query, int limit) {
713        if (searchable == null) {
714            return null;
715        }
716
717        String authority = searchable.getSuggestAuthority();
718        if (authority == null) {
719            return null;
720        }
721
722        Uri.Builder uriBuilder = new Uri.Builder()
723                .scheme(ContentResolver.SCHEME_CONTENT)
724                .authority(authority)
725                .query("")  // TODO: Remove, workaround for a bug in Uri.writeToParcel()
726                .fragment("");  // TODO: Remove, workaround for a bug in Uri.writeToParcel()
727
728        // if content path provided, insert it now
729        final String contentPath = searchable.getSuggestPath();
730        if (contentPath != null) {
731            uriBuilder.appendEncodedPath(contentPath);
732        }
733
734        // append standard suggestion query path
735        uriBuilder.appendPath(SearchManager.SUGGEST_URI_PATH_QUERY);
736
737        // get the query selection, may be null
738        String selection = searchable.getSuggestSelection();
739        // inject query, either as selection args or inline
740        String[] selArgs = null;
741        if (selection != null) {    // use selection if provided
742            selArgs = new String[] { query };
743        } else {                    // no selection, use REST pattern
744            uriBuilder.appendPath(query);
745        }
746
747        if (limit > 0) {
748            uriBuilder.appendQueryParameter("limit", String.valueOf(limit));
749        }
750
751        Uri uri = uriBuilder.build();
752
753        // finally, make the query
754        return mContext.getContentResolver().query(uri, null, selection, selArgs, null);
755    }
756}