PackageIconLoader.java revision e29d52aa72c96c3147fa91d83aeb8dafc6d1f578
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 com.android.quicksearchbox;
18
19import com.android.quicksearchbox.util.CachedLater;
20import com.android.quicksearchbox.util.Now;
21import com.android.quicksearchbox.util.NowOrLater;
22import com.android.quicksearchbox.util.Util;
23
24import android.content.ContentResolver;
25import android.content.Context;
26import android.content.pm.PackageManager;
27import android.content.pm.PackageManager.NameNotFoundException;
28import android.content.res.Resources;
29import android.graphics.drawable.Drawable;
30import android.net.Uri;
31import android.os.Handler;
32import android.os.HandlerThread;
33import android.os.Process;
34import android.text.TextUtils;
35import android.util.Log;
36
37import java.io.FileNotFoundException;
38import java.io.IOException;
39import java.io.InputStream;
40import java.util.List;
41
42/**
43 * Loads icons from other packages.
44 *
45 * Code partly stolen from {@link ContentResolver} and android.app.SuggestionsAdapter.
46  */
47public class PackageIconLoader implements IconLoader {
48
49    private static final boolean DBG = false;
50    private static final String TAG = "QSB.PackageIconLoader";
51
52    private static final String RESOURCE_URI_SCHEME = "android.resource";
53
54    private final Context mContext;
55
56    private final String mPackageName;
57
58    private Context mPackageContext;
59
60    private final Handler mUiThread;
61
62    private final HandlerThread mIconLoaderThread;
63    private final Handler mIconLoaderHandler;
64
65    /**
66     * Creates a new icon loader.
67     *
68     * @param context The QSB application context.
69     * @param packageName The name of the package from which the icons will be loaded.
70     *        Resource IDs without an explicit package will be resolved against the package
71     *        of this context.
72     */
73    public PackageIconLoader(Context context, String packageName, Handler uiThread) {
74        mContext = context;
75        mPackageName = packageName;
76        mUiThread = uiThread;
77        mIconLoaderThread = new HandlerThread("Icon loader " + packageName,
78                Process.THREAD_PRIORITY_BACKGROUND);
79        mIconLoaderThread.start();
80        mIconLoaderHandler = new Handler(mIconLoaderThread.getLooper());
81    }
82
83    private boolean ensurePackageContext() {
84        if (mPackageContext == null) {
85            try {
86                mPackageContext = mContext.createPackageContext(mPackageName,
87                        Context.CONTEXT_RESTRICTED);
88            } catch (PackageManager.NameNotFoundException ex) {
89                // This should only happen if the app has just be uninstalled
90                Log.e(TAG, "Application not found " + mPackageName);
91                return false;
92            }
93        }
94        return true;
95    }
96
97    public NowOrLater<Drawable> getIcon(final String drawableId) {
98        if (DBG) Log.d(TAG, "getIcon(" + drawableId + ")");
99        if (TextUtils.isEmpty(drawableId) || "0".equals(drawableId)) {
100            return new Now<Drawable>(null);
101        }
102        if (!ensurePackageContext()) {
103            return new Now<Drawable>(null);
104        }
105        NowOrLater<Drawable> drawable;
106        try {
107            // First, see if it's just an integer
108            int resourceId = Integer.parseInt(drawableId);
109            // If so, find it by resource ID
110            Drawable icon = mPackageContext.getResources().getDrawable(resourceId);
111            drawable = new Now<Drawable>(icon);
112        } catch (NumberFormatException nfe) {
113            // It's not an integer, use it as a URI
114            Uri uri = Uri.parse(drawableId);
115            if (RESOURCE_URI_SCHEME.equals(uri.getScheme())) {
116                // load all resources synchronously, to reduce UI flickering
117                drawable = new Now<Drawable>(getDrawable(uri));
118            } else {
119                drawable = new IconLaterTask(uri);
120            }
121        } catch (Resources.NotFoundException nfe) {
122            // It was an integer, but it couldn't be found, bail out
123            Log.w(TAG, "Icon resource not found: " + drawableId);
124            drawable = new Now<Drawable>(null);
125        }
126        return drawable;
127    }
128
129    public Uri getIconUri(String drawableId) {
130        if (TextUtils.isEmpty(drawableId) || "0".equals(drawableId)) {
131            return null;
132        }
133        if (!ensurePackageContext()) return null;
134        try {
135            int resourceId = Integer.parseInt(drawableId);
136            return Util.getResourceUri(mPackageContext, resourceId);
137        } catch (NumberFormatException nfe) {
138            return Uri.parse(drawableId);
139        }
140    }
141
142    /**
143     * Gets a drawable by URI.
144     *
145     * @return A drawable, or {@code null} if the drawable could not be loaded.
146     */
147    private Drawable getDrawable(Uri uri) {
148        try {
149            String scheme = uri.getScheme();
150            if (ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme)) {
151                // Load drawables through Resources, to get the source density information
152                OpenResourceIdResult r = getResourceId(uri);
153                try {
154                    return r.r.getDrawable(r.id);
155                } catch (Resources.NotFoundException ex) {
156                    throw new FileNotFoundException("Resource does not exist: " + uri);
157                }
158            } else {
159                // Let the ContentResolver handle content and file URIs.
160                InputStream stream = mPackageContext.getContentResolver().openInputStream(uri);
161                if (stream == null) {
162                    throw new FileNotFoundException("Failed to open " + uri);
163                }
164                try {
165                    return Drawable.createFromStream(stream, null);
166                } finally {
167                    try {
168                        stream.close();
169                    } catch (IOException ex) {
170                        Log.e(TAG, "Error closing icon stream for " + uri, ex);
171                    }
172                }
173            }
174        } catch (FileNotFoundException fnfe) {
175            Log.w(TAG, "Icon not found: " + uri + ", " + fnfe.getMessage());
176            return null;
177        }
178    }
179
180    /**
181     * A resource identified by the {@link Resources} that contains it, and a resource id.
182     */
183    private class OpenResourceIdResult {
184        public Resources r;
185        public int id;
186    }
187
188    /**
189     * Resolves an android.resource URI to a {@link Resources} and a resource id.
190     */
191    private OpenResourceIdResult getResourceId(Uri uri) throws FileNotFoundException {
192        String authority = uri.getAuthority();
193        Resources r;
194        if (TextUtils.isEmpty(authority)) {
195            throw new FileNotFoundException("No authority: " + uri);
196        } else {
197            try {
198                r = mPackageContext.getPackageManager().getResourcesForApplication(authority);
199            } catch (NameNotFoundException ex) {
200                throw new FileNotFoundException("Failed to get resources: " + ex);
201            }
202        }
203        List<String> path = uri.getPathSegments();
204        if (path == null) {
205            throw new FileNotFoundException("No path: " + uri);
206        }
207        int len = path.size();
208        int id;
209        if (len == 1) {
210            try {
211                id = Integer.parseInt(path.get(0));
212            } catch (NumberFormatException e) {
213                throw new FileNotFoundException("Single path segment is not a resource ID: " + uri);
214            }
215        } else if (len == 2) {
216            id = r.getIdentifier(path.get(1), path.get(0), authority);
217        } else {
218            throw new FileNotFoundException("More than two path segments: " + uri);
219        }
220        if (id == 0) {
221            throw new FileNotFoundException("No resource found for: " + uri);
222        }
223        OpenResourceIdResult res = new OpenResourceIdResult();
224        res.r = r;
225        res.id = id;
226        return res;
227    }
228
229    private class IconLaterTask extends CachedLater<Drawable> implements Runnable {
230        private final Uri mUri;
231
232        public IconLaterTask(Uri iconUri) {
233            mUri = iconUri;
234        }
235
236        @Override
237        protected void create() {
238            mIconLoaderHandler.post(this);
239        }
240
241        @Override
242        public void run() {
243            final Drawable icon = getIcon();
244            mUiThread.post(new Runnable(){
245                public void run() {
246                    store(icon);
247                }});
248        }
249
250        private Drawable getIcon() {
251            try {
252                return getDrawable(mUri);
253            } catch (Throwable t) {
254                // we're making a call into another package, which could throw any exception.
255                // Make sure it doesn't crash QSB
256                Log.e(TAG, "Failed to load icon " + mUri, t);
257                return null;
258            }
259        }
260    }
261}
262