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