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 
17 package com.android.quicksearchbox;
18 
19 import com.android.quicksearchbox.util.CachedLater;
20 import com.android.quicksearchbox.util.NamedTask;
21 import com.android.quicksearchbox.util.NamedTaskExecutor;
22 import com.android.quicksearchbox.util.Now;
23 import com.android.quicksearchbox.util.NowOrLater;
24 import com.android.quicksearchbox.util.Util;
25 
26 import android.content.ContentResolver;
27 import android.content.Context;
28 import android.content.pm.PackageManager;
29 import android.content.pm.PackageManager.NameNotFoundException;
30 import android.content.res.Resources;
31 import android.graphics.drawable.Drawable;
32 import android.net.Uri;
33 import android.os.Handler;
34 import android.text.TextUtils;
35 import android.util.Log;
36 
37 import java.io.FileNotFoundException;
38 import java.io.IOException;
39 import java.io.InputStream;
40 import java.util.List;
41 
42 /**
43  * Loads icons from other packages.
44  *
45  * Code partly stolen from {@link ContentResolver} and android.app.SuggestionsAdapter.
46   */
47 public 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      */
PackageIconLoader(Context context, String packageName, Handler uiThread, NamedTaskExecutor iconLoaderExecutor)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 
ensurePackageContext()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 
getIcon(final String drawableId)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 
getIconUri(String drawableId)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      */
getDrawable(Uri uri)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      */
getResourceId(Uri uri)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 
IconLaterTask(Uri iconUri)227         public IconLaterTask(Uri iconUri) {
228             mUri = iconUri;
229         }
230 
231         @Override
create()232         protected void create() {
233             mIconLoaderExecutor.execute(this);
234         }
235 
236         @Override
run()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
getName()246         public String getName() {
247             return mPackageName;
248         }
249 
getIcon()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