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