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