1 package androidx.appcompat.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 
19 import android.app.SearchManager;
20 import android.app.SearchableInfo;
21 import android.content.ComponentName;
22 import android.content.ContentResolver;
23 import android.content.Context;
24 import android.content.pm.ActivityInfo;
25 import android.content.pm.PackageManager;
26 import android.content.pm.PackageManager.NameNotFoundException;
27 import android.content.res.ColorStateList;
28 import android.content.res.Resources;
29 import android.database.Cursor;
30 import android.graphics.drawable.Drawable;
31 import android.net.Uri;
32 import android.os.Bundle;
33 import android.text.Spannable;
34 import android.text.SpannableString;
35 import android.text.TextUtils;
36 import android.text.style.TextAppearanceSpan;
37 import android.util.Log;
38 import android.util.TypedValue;
39 import android.view.View;
40 import android.view.View.OnClickListener;
41 import android.view.ViewGroup;
42 import android.widget.ImageView;
43 import android.widget.TextView;
44 
45 import androidx.appcompat.R;
46 import androidx.core.content.ContextCompat;
47 import androidx.cursoradapter.widget.ResourceCursorAdapter;
48 
49 import java.io.FileNotFoundException;
50 import java.io.IOException;
51 import java.io.InputStream;
52 import java.util.List;
53 import java.util.WeakHashMap;
54 
55 /**
56  * Provides the contents for the suggestion drop-down list.in {@link SearchView}.
57  */
58 class 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 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     @SuppressWarnings("deprecation")
SuggestionsAdapter(Context context, SearchView searchView, SearchableInfo searchable, WeakHashMap<String, Drawable.ConstantState> outsideDrawablesCache)90     public SuggestionsAdapter(Context context, SearchView searchView, SearchableInfo searchable,
91             WeakHashMap<String, Drawable.ConstantState> outsideDrawablesCache) {
92         // Auto-requery is discouraged, as it results in Cursor queries being performed on the
93         // application's UI thread.
94         super(context, searchView.getSuggestionRowLayout(), null /* no initial cursor */,
95                 true /* auto-requery */);
96         mSearchView = searchView;
97         mSearchable = searchable;
98         mCommitIconResId = searchView.getSuggestionCommitIconResId();
99 
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      */
setQueryRefinement(int refineWhat)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      */
getQueryRefinement()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
hasStableIds()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
runQueryOnBackgroundThread(CharSequence constraint)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 
close()170     public void close() {
171         if (DBG) Log.d(LOG_TAG, "close()");
172         changeCursor(null);
173         mClosed = true;
174     }
175 
176     @Override
notifyDataSetChanged()177     public void notifyDataSetChanged() {
178         if (DBG) Log.d(LOG_TAG, "notifyDataSetChanged");
179         super.notifyDataSetChanged();
180 
181         updateSpinnerState(getCursor());
182     }
183 
184     @Override
notifyDataSetInvalidated()185     public void notifyDataSetInvalidated() {
186         if (DBG) Log.d(LOG_TAG, "notifyDataSetInvalidated");
187         super.notifyDataSetInvalidated();
188 
189         updateSpinnerState(getCursor());
190     }
191 
updateSpinnerState(Cursor cursor)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
changeCursor(Cursor c)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
newView(Context context, Cursor cursor, ViewGroup parent)241     public View newView(Context context, Cursor cursor, ViewGroup parent) {
242         final View v = super.newView(context, cursor, parent);
243         v.setTag(new ChildViewCache(v));
244 
245         // Set up icon.
246         final ImageView iconRefine = (ImageView) v.findViewById(R.id.edit_query);
247         iconRefine.setImageResource(mCommitIconResId);
248         return v;
249     }
250 
251     /**
252      * Cache of the child views of drop-drown list items, to avoid looking up the children
253      * each time the contents of a list item are changed.
254      */
255     private final static class ChildViewCache {
256         public final TextView mText1;
257         public final TextView mText2;
258         public final ImageView mIcon1;
259         public final ImageView mIcon2;
260         public final ImageView mIconRefine;
261 
ChildViewCache(View v)262         public ChildViewCache(View v) {
263             mText1 = (TextView) v.findViewById(android.R.id.text1);
264             mText2 = (TextView) v.findViewById(android.R.id.text2);
265             mIcon1 = (ImageView) v.findViewById(android.R.id.icon1);
266             mIcon2 = (ImageView) v.findViewById(android.R.id.icon2);
267             mIconRefine = (ImageView) v.findViewById(R.id.edit_query);
268         }
269     }
270 
271     @Override
bindView(View view, Context context, Cursor cursor)272     public void bindView(View view, Context context, Cursor cursor) {
273         ChildViewCache views = (ChildViewCache) view.getTag();
274 
275         int flags = 0;
276         if (mFlagsCol != INVALID_INDEX) {
277             flags = cursor.getInt(mFlagsCol);
278         }
279         if (views.mText1 != null) {
280             String text1 = getStringOrNull(cursor, mText1Col);
281             setViewText(views.mText1, text1);
282         }
283         if (views.mText2 != null) {
284             // First check TEXT_2_URL
285             CharSequence text2 = getStringOrNull(cursor, mText2UrlCol);
286             if (text2 != null) {
287                 text2 = formatUrl(text2);
288             } else {
289                 text2 = getStringOrNull(cursor, mText2Col);
290             }
291 
292             // If no second line of text is indicated, allow the first line of text
293             // to be up to two lines if it wants to be.
294             if (TextUtils.isEmpty(text2)) {
295                 if (views.mText1 != null) {
296                     views.mText1.setSingleLine(false);
297                     views.mText1.setMaxLines(2);
298                 }
299             } else {
300                 if (views.mText1 != null) {
301                     views.mText1.setSingleLine(true);
302                     views.mText1.setMaxLines(1);
303                 }
304             }
305             setViewText(views.mText2, text2);
306         }
307 
308         if (views.mIcon1 != null) {
309             setViewDrawable(views.mIcon1, getIcon1(cursor), View.INVISIBLE);
310         }
311         if (views.mIcon2 != null) {
312             setViewDrawable(views.mIcon2, getIcon2(cursor), View.GONE);
313         }
314         if (mQueryRefinement == REFINE_ALL
315                 || (mQueryRefinement == REFINE_BY_ENTRY
316                         && (flags & SearchManager.FLAG_QUERY_REFINEMENT) != 0)) {
317             views.mIconRefine.setVisibility(View.VISIBLE);
318             views.mIconRefine.setTag(views.mText1.getText());
319             views.mIconRefine.setOnClickListener(this);
320         } else {
321             views.mIconRefine.setVisibility(View.GONE);
322         }
323     }
324 
325     @Override
onClick(View v)326     public void onClick(View v) {
327         Object tag = v.getTag();
328         if (tag instanceof CharSequence) {
329             mSearchView.onQueryRefine((CharSequence) tag);
330         }
331     }
332 
formatUrl(CharSequence url)333     private CharSequence formatUrl(CharSequence url) {
334         if (mUrlColor == null) {
335             // Lazily get the URL color from the current theme.
336             TypedValue colorValue = new TypedValue();
337             mProviderContext.getTheme().resolveAttribute(
338                     R.attr.textColorSearchUrl, colorValue, true);
339             mUrlColor = mProviderContext.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 
setViewText(TextView v, CharSequence text)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 
getIcon1(Cursor cursor)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();
370     }
371 
getIcon2(Cursor cursor)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      */
setViewDrawable(ImageView v, Drawable drawable, int nullVisibility)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
convertToString(Cursor cursor)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
getView(int position, View convertView, ViewGroup parent)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(mProviderContext, getCursor(), 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
getDropDownView(int position, View convertView, ViewGroup parent)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(mProviderContext, getCursor(), 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      */
getDrawableFromResourceValue(String drawableId)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      */
getDrawable(Uri uri)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 
checkIconCache(String resourceUri)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 
storeInIconCache(String resourceUri, Drawable drawable)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      * @return A non-null drawable.
601      */
getDefaultIcon1()602     private Drawable getDefaultIcon1() {
603         // Check the component that gave us the suggestion
604         Drawable drawable = getActivityIconWithCache(mSearchable.getSearchActivity());
605         if (drawable != null) {
606             return drawable;
607         }
608 
609         // Fall back to a default icon
610         return mProviderContext.getPackageManager().getDefaultActivityIcon();
611     }
612 
613     /**
614      * Gets the activity or application icon for an activity.
615      * Uses the local icon cache for fast repeated lookups.
616      *
617      * @param component Name of an activity.
618      * @return A drawable, or {@code null} if neither the activity nor the application
619      *         has an icon set.
620      */
getActivityIconWithCache(ComponentName component)621     private Drawable getActivityIconWithCache(ComponentName component) {
622         // First check the icon cache
623         String componentIconKey = component.flattenToShortString();
624         // Using containsKey() since we also store null values.
625         if (mOutsideDrawablesCache.containsKey(componentIconKey)) {
626             Drawable.ConstantState cached = mOutsideDrawablesCache.get(componentIconKey);
627             return cached == null ? null : cached.newDrawable(mProviderContext.getResources());
628         }
629         // Then try the activity or application icon
630         Drawable drawable = getActivityIcon(component);
631         // Stick it in the cache so we don't do this lookup again.
632         Drawable.ConstantState toCache = drawable == null ? null : drawable.getConstantState();
633         mOutsideDrawablesCache.put(componentIconKey, toCache);
634         return drawable;
635     }
636 
637     /**
638      * Gets the activity or application icon for an activity.
639      *
640      * @param component Name of an activity.
641      * @return A drawable, or {@code null} if neither the activity or the application
642      *         have an icon set.
643      */
getActivityIcon(ComponentName component)644     private Drawable getActivityIcon(ComponentName component) {
645         PackageManager pm = mProviderContext.getPackageManager();
646         final ActivityInfo activityInfo;
647         try {
648             activityInfo = pm.getActivityInfo(component, PackageManager.GET_META_DATA);
649         } catch (NameNotFoundException ex) {
650             Log.w(LOG_TAG, ex.toString());
651             return null;
652         }
653         int iconId = activityInfo.getIconResource();
654         if (iconId == 0) return null;
655         String pkg = component.getPackageName();
656         Drawable drawable = pm.getDrawable(pkg, iconId, activityInfo.applicationInfo);
657         if (drawable == null) {
658             Log.w(LOG_TAG, "Invalid icon resource " + iconId + " for "
659                     + component.flattenToShortString());
660             return null;
661         }
662         return drawable;
663     }
664 
665     /**
666      * Gets the value of a string column by name.
667      *
668      * @param cursor Cursor to read the value from.
669      * @param columnName The name of the column to read.
670      * @return The value of the given column, or <code>null</null>
671      *         if the cursor does not contain the given column.
672      */
getColumnString(Cursor cursor, String columnName)673     public static String getColumnString(Cursor cursor, String columnName) {
674         int col = cursor.getColumnIndex(columnName);
675         return getStringOrNull(cursor, col);
676     }
677 
getStringOrNull(Cursor cursor, int col)678     private static String getStringOrNull(Cursor cursor, int col) {
679         if (col == INVALID_INDEX) {
680             return null;
681         }
682         try {
683             return cursor.getString(col);
684         } catch (Exception e) {
685             Log.e(LOG_TAG,
686                     "unexpected error retrieving valid column from cursor, "
687                             + "did the remote process die?", e);
688             return null;
689         }
690     }
691 
692     /**
693      * Import of hidden method: ContentResolver.getResourceId(Uri).
694      * Modified to return a drawable, rather than a hidden type.
695      */
getDrawableFromResourceUri(Uri uri)696     Drawable getDrawableFromResourceUri(Uri uri) throws FileNotFoundException {
697         String authority = uri.getAuthority();
698         Resources r;
699         if (TextUtils.isEmpty(authority)) {
700             throw new FileNotFoundException("No authority: " + uri);
701         } else {
702             try {
703                 r = mProviderContext.getPackageManager().getResourcesForApplication(authority);
704             } catch (NameNotFoundException ex) {
705                 throw new FileNotFoundException("No package found for authority: " + uri);
706             }
707         }
708         List<String> path = uri.getPathSegments();
709         if (path == null) {
710             throw new FileNotFoundException("No path: " + uri);
711         }
712         int len = path.size();
713         int id;
714         if (len == 1) {
715             try {
716                 id = Integer.parseInt(path.get(0));
717             } catch (NumberFormatException e) {
718                 throw new FileNotFoundException("Single path segment is not a resource ID: " + uri);
719             }
720         } else if (len == 2) {
721             id = r.getIdentifier(path.get(1), path.get(0), authority);
722         } else {
723             throw new FileNotFoundException("More than two path segments: " + uri);
724         }
725         if (id == 0) {
726             throw new FileNotFoundException("No resource found for: " + uri);
727         }
728         return r.getDrawable(id);
729     }
730 
731     /**
732      * Import of hidden method: SearchManager.getSuggestions(SearchableInfo, String, int).
733      */
getSearchManagerSuggestions(SearchableInfo searchable, String query, int limit)734     Cursor getSearchManagerSuggestions(SearchableInfo searchable, String query, int limit) {
735         if (searchable == null) {
736             return null;
737         }
738 
739         String authority = searchable.getSuggestAuthority();
740         if (authority == null) {
741             return null;
742         }
743 
744         Uri.Builder uriBuilder = new Uri.Builder()
745                 .scheme(ContentResolver.SCHEME_CONTENT)
746                 .authority(authority)
747                 .query("")  // TODO: Remove, workaround for a bug in Uri.writeToParcel()
748                 .fragment("");  // TODO: Remove, workaround for a bug in Uri.writeToParcel()
749 
750         // if content path provided, insert it now
751         final String contentPath = searchable.getSuggestPath();
752         if (contentPath != null) {
753             uriBuilder.appendEncodedPath(contentPath);
754         }
755 
756         // append standard suggestion query path
757         uriBuilder.appendPath(SearchManager.SUGGEST_URI_PATH_QUERY);
758 
759         // get the query selection, may be null
760         String selection = searchable.getSuggestSelection();
761         // inject query, either as selection args or inline
762         String[] selArgs = null;
763         if (selection != null) {    // use selection if provided
764             selArgs = new String[] { query };
765         } else {                    // no selection, use REST pattern
766             uriBuilder.appendPath(query);
767         }
768 
769         if (limit > 0) {
770             uriBuilder.appendQueryParameter("limit", String.valueOf(limit));
771         }
772 
773         Uri uri = uriBuilder.build();
774 
775         // finally, make the query
776         return mProviderContext.getContentResolver().query(uri, null, selection, selArgs, null);
777     }
778 }