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 */
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 final SearchManager mSearchManager;
68    private final SearchView mSearchView;
69    private final SearchableInfo mSearchable;
70    private final Context mProviderContext;
71    private final WeakHashMap<String, Drawable.ConstantState> mOutsideDrawablesCache;
72    private final int mCommitIconResId;
73    private boolean mClosed = false;
74    private int mQueryRefinement = REFINE_BY_ENTRY;
75
76    // URL color
77    private ColorStateList mUrlColor;
78
79    static final int INVALID_INDEX = -1;
80
81    // Cached column indexes, updated when the cursor changes.
82    private int mText1Col = INVALID_INDEX;
83    private int mText2Col = INVALID_INDEX;
84    private int mText2UrlCol = INVALID_INDEX;
85    private int mIconName1Col = INVALID_INDEX;
86    private int mIconName2Col = INVALID_INDEX;
87    private int mFlagsCol = INVALID_INDEX;
88
89    // private final Runnable mStartSpinnerRunnable;
90    // private final Runnable mStopSpinnerRunnable;
91
92    public SuggestionsAdapter(Context context, SearchView searchView, SearchableInfo searchable,
93            WeakHashMap<String, Drawable.ConstantState> outsideDrawablesCache) {
94        super(context, searchView.getSuggestionRowLayout(), null /* no initial cursor */,
95                true /* auto-requery */);
96        mSearchManager = (SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE);
97        mSearchView = searchView;
98        mSearchable = searchable;
99        mCommitIconResId = searchView.getSuggestionCommitIconResId();
100
101        // set up provider resources (gives us icons, etc.)
102        mProviderContext = context;
103
104        mOutsideDrawablesCache = outsideDrawablesCache;
105    }
106
107    /**
108     * Enables query refinement for all suggestions. This means that an additional icon
109     * will be shown for each entry. When clicked, the suggested text on that line will be
110     * copied to the query text field.
111     * <p>
112     *
113     * @param refineWhat which queries to refine. Possible values are {@link #REFINE_NONE},
114     * {@link #REFINE_BY_ENTRY}, and {@link #REFINE_ALL}.
115     */
116    public void setQueryRefinement(int refineWhat) {
117        mQueryRefinement = refineWhat;
118    }
119
120    /**
121     * Returns the current query refinement preference.
122     * @return value of query refinement preference
123     */
124    public int getQueryRefinement() {
125        return mQueryRefinement;
126    }
127
128    /**
129     * Overridden to always return <code>false</code>, since we cannot be sure that
130     * suggestion sources return stable IDs.
131     */
132    @Override
133    public boolean hasStableIds() {
134        return false;
135    }
136
137    /**
138     * Use the search suggestions provider to obtain a live cursor.  This will be called
139     * in a worker thread, so it's OK if the query is slow (e.g. round trip for suggestions).
140     * The results will be processed in the UI thread and changeCursor() will be called.
141     */
142    @Override
143    public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
144        if (DBG) Log.d(LOG_TAG, "runQueryOnBackgroundThread(" + constraint + ")");
145        String query = (constraint == null) ? "" : constraint.toString();
146        /**
147         * for in app search we show the progress spinner until the cursor is returned with
148         * the results.
149         */
150        Cursor cursor = null;
151        if (mSearchView.getVisibility() != View.VISIBLE
152                || mSearchView.getWindowVisibility() != View.VISIBLE) {
153            return null;
154        }
155        try {
156            cursor = getSearchManagerSuggestions(mSearchable, query, QUERY_LIMIT);
157            // trigger fill window so the spinner stays up until the results are copied over and
158            // closer to being ready
159            if (cursor != null) {
160                cursor.getCount();
161                return cursor;
162            }
163        } catch (RuntimeException e) {
164            Log.w(LOG_TAG, "Search suggestions query threw an exception.", e);
165        }
166        // If cursor is null or an exception was thrown, stop the spinner and return null.
167        // changeCursor doesn't get called if cursor is null
168        return null;
169    }
170
171    public void close() {
172        if (DBG) Log.d(LOG_TAG, "close()");
173        changeCursor(null);
174        mClosed = true;
175    }
176
177    @Override
178    public void notifyDataSetChanged() {
179        if (DBG) Log.d(LOG_TAG, "notifyDataSetChanged");
180        super.notifyDataSetChanged();
181
182        updateSpinnerState(getCursor());
183    }
184
185    @Override
186    public void notifyDataSetInvalidated() {
187        if (DBG) Log.d(LOG_TAG, "notifyDataSetInvalidated");
188        super.notifyDataSetInvalidated();
189
190        updateSpinnerState(getCursor());
191    }
192
193    private void updateSpinnerState(Cursor cursor) {
194        Bundle extras = cursor != null ? cursor.getExtras() : null;
195        if (DBG) {
196            Log.d(LOG_TAG, "updateSpinnerState - extra = "
197                + (extras != null
198                        ? extras.getBoolean(SearchManager.CURSOR_EXTRA_KEY_IN_PROGRESS)
199                        : null));
200        }
201        // Check if the Cursor indicates that the query is not complete and show the spinner
202        if (extras != null
203                && extras.getBoolean(SearchManager.CURSOR_EXTRA_KEY_IN_PROGRESS)) {
204            return;
205        }
206        // If cursor is null or is done, stop the spinner
207    }
208
209    /**
210     * Cache columns.
211     */
212    @Override
213    public void changeCursor(Cursor c) {
214        if (DBG) Log.d(LOG_TAG, "changeCursor(" + c + ")");
215
216        if (mClosed) {
217            Log.w(LOG_TAG, "Tried to change cursor after adapter was closed.");
218            if (c != null) c.close();
219            return;
220        }
221
222        try {
223            super.changeCursor(c);
224
225            if (c != null) {
226                mText1Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1);
227                mText2Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2);
228                mText2UrlCol = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2_URL);
229                mIconName1Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_1);
230                mIconName2Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_2);
231                mFlagsCol = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_FLAGS);
232            }
233        } catch (Exception e) {
234            Log.e(LOG_TAG, "error changing cursor and caching columns", e);
235        }
236    }
237
238    /**
239     * Tags the view with cached child view look-ups.
240     */
241    @Override
242    public View newView(Context context, Cursor cursor, ViewGroup parent) {
243        final View v = super.newView(context, cursor, parent);
244        v.setTag(new ChildViewCache(v));
245
246        // Set up icon.
247        final ImageView iconRefine = (ImageView) v.findViewById(R.id.edit_query);
248        iconRefine.setImageResource(mCommitIconResId);
249        return v;
250    }
251
252    /**
253     * Cache of the child views of drop-drown list items, to avoid looking up the children
254     * each time the contents of a list item are changed.
255     */
256    private final static class ChildViewCache {
257        public final TextView mText1;
258        public final TextView mText2;
259        public final ImageView mIcon1;
260        public final ImageView mIcon2;
261        public final ImageView mIconRefine;
262
263        public ChildViewCache(View v) {
264            mText1 = (TextView) v.findViewById(android.R.id.text1);
265            mText2 = (TextView) v.findViewById(android.R.id.text2);
266            mIcon1 = (ImageView) v.findViewById(android.R.id.icon1);
267            mIcon2 = (ImageView) v.findViewById(android.R.id.icon2);
268            mIconRefine = (ImageView) v.findViewById(R.id.edit_query);
269        }
270    }
271
272    @Override
273    public void bindView(View view, Context context, Cursor cursor) {
274        ChildViewCache views = (ChildViewCache) view.getTag();
275
276        int flags = 0;
277        if (mFlagsCol != INVALID_INDEX) {
278            flags = cursor.getInt(mFlagsCol);
279        }
280        if (views.mText1 != null) {
281            String text1 = getStringOrNull(cursor, mText1Col);
282            setViewText(views.mText1, text1);
283        }
284        if (views.mText2 != null) {
285            // First check TEXT_2_URL
286            CharSequence text2 = getStringOrNull(cursor, mText2UrlCol);
287            if (text2 != null) {
288                text2 = formatUrl(text2);
289            } else {
290                text2 = getStringOrNull(cursor, mText2Col);
291            }
292
293            // If no second line of text is indicated, allow the first line of text
294            // to be up to two lines if it wants to be.
295            if (TextUtils.isEmpty(text2)) {
296                if (views.mText1 != null) {
297                    views.mText1.setSingleLine(false);
298                    views.mText1.setMaxLines(2);
299                }
300            } else {
301                if (views.mText1 != null) {
302                    views.mText1.setSingleLine(true);
303                    views.mText1.setMaxLines(1);
304                }
305            }
306            setViewText(views.mText2, text2);
307        }
308
309        if (views.mIcon1 != null) {
310            setViewDrawable(views.mIcon1, getIcon1(cursor), View.INVISIBLE);
311        }
312        if (views.mIcon2 != null) {
313            setViewDrawable(views.mIcon2, getIcon2(cursor), View.GONE);
314        }
315        if (mQueryRefinement == REFINE_ALL
316                || (mQueryRefinement == REFINE_BY_ENTRY
317                        && (flags & SearchManager.FLAG_QUERY_REFINEMENT) != 0)) {
318            views.mIconRefine.setVisibility(View.VISIBLE);
319            views.mIconRefine.setTag(views.mText1.getText());
320            views.mIconRefine.setOnClickListener(this);
321        } else {
322            views.mIconRefine.setVisibility(View.GONE);
323        }
324    }
325
326    @Override
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     * This method is overridden purely to provide a bit of protection against
465     * flaky content providers.
466     *
467     * @see android.widget.CursorAdapter#getDropDownView(int, View, ViewGroup)
468     */
469    @Override
470    public View getDropDownView(int position, View convertView, ViewGroup parent) {
471        try {
472            return super.getDropDownView(position, convertView, parent);
473        } catch (RuntimeException e) {
474            Log.w(LOG_TAG, "Search suggestions cursor threw exception.", e);
475            // Put exception string in item title
476            final View v = newDropDownView(mContext, mCursor, parent);
477            if (v != null) {
478                final ChildViewCache views = (ChildViewCache) v.getTag();
479                final TextView tv = views.mText1;
480                tv.setText(e.toString());
481            }
482            return v;
483        }
484    }
485
486    /**
487     * Gets a drawable given a value provided by a suggestion provider.
488     *
489     * This value could be just the string value of a resource id
490     * (e.g., "2130837524"), in which case we will try to retrieve a drawable from
491     * the provider's resources. If the value is not an integer, it is
492     * treated as a Uri and opened with
493     * {@link ContentResolver#openOutputStream(android.net.Uri, String)}.
494     *
495     * All resources and URIs are read using the suggestion provider's context.
496     *
497     * If the string is not formatted as expected, or no drawable can be found for
498     * the provided value, this method returns null.
499     *
500     * @param drawableId a string like "2130837524",
501     *        "android.resource://com.android.alarmclock/2130837524",
502     *        or "content://contacts/photos/253".
503     * @return a Drawable, or null if none found
504     */
505    private Drawable getDrawableFromResourceValue(String drawableId) {
506        if (drawableId == null || drawableId.isEmpty() || "0".equals(drawableId)) {
507            return null;
508        }
509        try {
510            // First, see if it's just an integer
511            int resourceId = Integer.parseInt(drawableId);
512            // It's an int, look for it in the cache
513            String drawableUri = ContentResolver.SCHEME_ANDROID_RESOURCE
514                    + "://" + mProviderContext.getPackageName() + "/" + resourceId;
515            // Must use URI as cache key, since ints are app-specific
516            Drawable drawable = checkIconCache(drawableUri);
517            if (drawable != null) {
518                return drawable;
519            }
520            // Not cached, find it by resource ID
521            drawable = ContextCompat.getDrawable(mProviderContext, resourceId);
522            // Stick it in the cache, using the URI as key
523            storeInIconCache(drawableUri, drawable);
524            return drawable;
525        } catch (NumberFormatException nfe) {
526            // It's not an integer, use it as a URI
527            Drawable drawable = checkIconCache(drawableId);
528            if (drawable != null) {
529                return drawable;
530            }
531            Uri uri = Uri.parse(drawableId);
532            drawable = getDrawable(uri);
533            storeInIconCache(drawableId, drawable);
534            return drawable;
535        } catch (Resources.NotFoundException nfe) {
536            // It was an integer, but it couldn't be found, bail out
537            Log.w(LOG_TAG, "Icon resource not found: " + drawableId);
538            return null;
539        }
540    }
541
542    /**
543     * Gets a drawable by URI, without using the cache.
544     *
545     * @return A drawable, or {@code null} if the drawable could not be loaded.
546     */
547    private Drawable getDrawable(Uri uri) {
548        try {
549            String scheme = uri.getScheme();
550            if (ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme)) {
551                // Load drawables through Resources, to get the source density information
552                try {
553                    return getDrawableFromResourceUri(uri);
554                } catch (Resources.NotFoundException ex) {
555                    throw new FileNotFoundException("Resource does not exist: " + uri);
556                }
557            } else {
558                // Let the ContentResolver handle content and file URIs.
559                InputStream stream = mProviderContext.getContentResolver().openInputStream(uri);
560                if (stream == null) {
561                    throw new FileNotFoundException("Failed to open " + uri);
562                }
563                try {
564                    return Drawable.createFromStream(stream, null);
565                } finally {
566                    try {
567                        stream.close();
568                    } catch (IOException ex) {
569                        Log.e(LOG_TAG, "Error closing icon stream for " + uri, ex);
570                    }
571                }
572            }
573        } catch (FileNotFoundException fnfe) {
574            Log.w(LOG_TAG, "Icon not found: " + uri + ", " + fnfe.getMessage());
575            return null;
576        }
577    }
578
579
580
581    private Drawable checkIconCache(String resourceUri) {
582        Drawable.ConstantState cached = mOutsideDrawablesCache.get(resourceUri);
583        if (cached == null) {
584            return null;
585        }
586        if (DBG) Log.d(LOG_TAG, "Found icon in cache: " + resourceUri);
587        return cached.newDrawable();
588    }
589
590    private void storeInIconCache(String resourceUri, Drawable drawable) {
591        if (drawable != null) {
592            mOutsideDrawablesCache.put(resourceUri, drawable.getConstantState());
593        }
594    }
595
596    /**
597     * Gets the left-hand side icon that will be used for the current suggestion
598     * if the suggestion contains an icon column but no icon or a broken icon.
599     *
600     * @param cursor A cursor positioned at the current suggestion.
601     * @return A non-null drawable.
602     */
603    private Drawable getDefaultIcon1(Cursor cursor) {
604        // Check the component that gave us the suggestion
605        Drawable drawable = getActivityIconWithCache(mSearchable.getSearchActivity());
606        if (drawable != null) {
607            return drawable;
608        }
609
610        // Fall back to a default icon
611        return mContext.getPackageManager().getDefaultActivityIcon();
612    }
613
614    /**
615     * Gets the activity or application icon for an activity.
616     * Uses the local icon cache for fast repeated lookups.
617     *
618     * @param component Name of an activity.
619     * @return A drawable, or {@code null} if neither the activity nor the application
620     *         has an icon set.
621     */
622    private Drawable getActivityIconWithCache(ComponentName component) {
623        // First check the icon cache
624        String componentIconKey = component.flattenToShortString();
625        // Using containsKey() since we also store null values.
626        if (mOutsideDrawablesCache.containsKey(componentIconKey)) {
627            Drawable.ConstantState cached = mOutsideDrawablesCache.get(componentIconKey);
628            return cached == null ? null : cached.newDrawable(mProviderContext.getResources());
629        }
630        // Then try the activity or application icon
631        Drawable drawable = getActivityIcon(component);
632        // Stick it in the cache so we don't do this lookup again.
633        Drawable.ConstantState toCache = drawable == null ? null : drawable.getConstantState();
634        mOutsideDrawablesCache.put(componentIconKey, toCache);
635        return drawable;
636    }
637
638    /**
639     * Gets the activity or application icon for an activity.
640     *
641     * @param component Name of an activity.
642     * @return A drawable, or {@code null} if neither the activity or the application
643     *         have an icon set.
644     */
645    private Drawable getActivityIcon(ComponentName component) {
646        PackageManager pm = mContext.getPackageManager();
647        final ActivityInfo activityInfo;
648        try {
649            activityInfo = pm.getActivityInfo(component, PackageManager.GET_META_DATA);
650        } catch (NameNotFoundException ex) {
651            Log.w(LOG_TAG, ex.toString());
652            return null;
653        }
654        int iconId = activityInfo.getIconResource();
655        if (iconId == 0) return null;
656        String pkg = component.getPackageName();
657        Drawable drawable = pm.getDrawable(pkg, iconId, activityInfo.applicationInfo);
658        if (drawable == null) {
659            Log.w(LOG_TAG, "Invalid icon resource " + iconId + " for "
660                    + component.flattenToShortString());
661            return null;
662        }
663        return drawable;
664    }
665
666    /**
667     * Gets the value of a string column by name.
668     *
669     * @param cursor Cursor to read the value from.
670     * @param columnName The name of the column to read.
671     * @return The value of the given column, or <code>null</null>
672     *         if the cursor does not contain the given column.
673     */
674    public static String getColumnString(Cursor cursor, String columnName) {
675        int col = cursor.getColumnIndex(columnName);
676        return getStringOrNull(cursor, col);
677    }
678
679    private static String getStringOrNull(Cursor cursor, int col) {
680        if (col == INVALID_INDEX) {
681            return null;
682        }
683        try {
684            return cursor.getString(col);
685        } catch (Exception e) {
686            Log.e(LOG_TAG,
687                    "unexpected error retrieving valid column from cursor, "
688                            + "did the remote process die?", e);
689            return null;
690        }
691    }
692
693    /**
694     * Import of hidden method: ContentResolver.getResourceId(Uri).
695     * Modified to return a drawable, rather than a hidden type.
696     */
697    Drawable getDrawableFromResourceUri(Uri uri) throws FileNotFoundException {
698        String authority = uri.getAuthority();
699        Resources r;
700        if (TextUtils.isEmpty(authority)) {
701            throw new FileNotFoundException("No authority: " + uri);
702        } else {
703            try {
704                r = mContext.getPackageManager().getResourcesForApplication(authority);
705            } catch (NameNotFoundException ex) {
706                throw new FileNotFoundException("No package found for authority: " + uri);
707            }
708        }
709        List<String> path = uri.getPathSegments();
710        if (path == null) {
711            throw new FileNotFoundException("No path: " + uri);
712        }
713        int len = path.size();
714        int id;
715        if (len == 1) {
716            try {
717                id = Integer.parseInt(path.get(0));
718            } catch (NumberFormatException e) {
719                throw new FileNotFoundException("Single path segment is not a resource ID: " + uri);
720            }
721        } else if (len == 2) {
722            id = r.getIdentifier(path.get(1), path.get(0), authority);
723        } else {
724            throw new FileNotFoundException("More than two path segments: " + uri);
725        }
726        if (id == 0) {
727            throw new FileNotFoundException("No resource found for: " + uri);
728        }
729        return r.getDrawable(id);
730    }
731
732    /**
733     * Import of hidden method: SearchManager.getSuggestions(SearchableInfo, String, int).
734     */
735    Cursor getSearchManagerSuggestions(SearchableInfo searchable, String query, int limit) {
736        if (searchable == null) {
737            return null;
738        }
739
740        String authority = searchable.getSuggestAuthority();
741        if (authority == null) {
742            return null;
743        }
744
745        Uri.Builder uriBuilder = new Uri.Builder()
746                .scheme(ContentResolver.SCHEME_CONTENT)
747                .authority(authority)
748                .query("")  // TODO: Remove, workaround for a bug in Uri.writeToParcel()
749                .fragment("");  // TODO: Remove, workaround for a bug in Uri.writeToParcel()
750
751        // if content path provided, insert it now
752        final String contentPath = searchable.getSuggestPath();
753        if (contentPath != null) {
754            uriBuilder.appendEncodedPath(contentPath);
755        }
756
757        // append standard suggestion query path
758        uriBuilder.appendPath(SearchManager.SUGGEST_URI_PATH_QUERY);
759
760        // get the query selection, may be null
761        String selection = searchable.getSuggestSelection();
762        // inject query, either as selection args or inline
763        String[] selArgs = null;
764        if (selection != null) {    // use selection if provided
765            selArgs = new String[] { query };
766        } else {                    // no selection, use REST pattern
767            uriBuilder.appendPath(query);
768        }
769
770        if (limit > 0) {
771            uriBuilder.appendQueryParameter("limit", String.valueOf(limit));
772        }
773
774        Uri uri = uriBuilder.build();
775
776        // finally, make the query
777        return mContext.getContentResolver().query(uri, null, selection, selArgs, null);
778    }
779}