1 /*
2  * Copyright (C) 2014 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.tv.settings.widget;
18 
19 import android.accounts.Account;
20 import android.accounts.AccountManager;
21 import android.content.Context;
22 import android.content.Intent.ShortcutIconResource;
23 import android.content.pm.PackageManager.NameNotFoundException;
24 import android.content.res.Resources;
25 import android.content.res.Resources.NotFoundException;
26 import android.graphics.Bitmap;
27 import android.graphics.BitmapFactory;
28 import android.graphics.drawable.Drawable;
29 import android.net.Uri;
30 import android.os.AsyncTask;
31 import android.util.Log;
32 import android.util.TypedValue;
33 import android.widget.ImageView;
34 
35 import com.android.tv.settings.util.AccountImageHelper;
36 import com.android.tv.settings.util.ByteArrayPool;
37 import com.android.tv.settings.util.CachedInputStream;
38 import com.android.tv.settings.util.UriUtils;
39 
40 import java.io.FileNotFoundException;
41 import java.io.IOException;
42 import java.io.InputStream;
43 import java.lang.ref.WeakReference;
44 import java.net.SocketTimeoutException;
45 import java.net.URL;
46 import java.net.URLConnection;
47 
48 /**
49  * AsyncTask which loads a bitmap.
50  * <p>
51  * The source of this can be another package (via a resource), a URI (content provider), or
52  * a file path.
53  *
54  * @see BitmapWorkerOptions
55  */
56 class DrawableLoader extends AsyncTask<BitmapWorkerOptions, Void, Drawable> {
57 
58     private static final String TAG = "DrawableLoader";
59     private static final String GOOGLE_ACCOUNT_TYPE = "com.google";
60 
61     private static final boolean DEBUG = false;
62 
63     private static final int SOCKET_TIMEOUT = 10000;
64     private static final int READ_TIMEOUT = 10000;
65 
66     private final WeakReference<ImageView> mImageView;
67     private int mOriginalWidth;
68     private int mOriginalHeight;
69     private final RecycleBitmapPool mRecycledBitmaps;
70 
71     private final RefcountObject.RefcountListener mRefcountListener =
72             new RefcountObject.RefcountListener() {
73         @Override
74         public void onRefcountZero(RefcountObject object) {
75             mRecycledBitmaps.addRecycledBitmap((Bitmap) object.getObject());
76         }
77     };
78 
79 
DrawableLoader(ImageView imageView, RecycleBitmapPool recycledBitmapPool)80     DrawableLoader(ImageView imageView, RecycleBitmapPool recycledBitmapPool) {
81         mImageView = new WeakReference<>(imageView);
82         mRecycledBitmaps = recycledBitmapPool;
83     }
84 
getOriginalWidth()85     public int getOriginalWidth() {
86         return mOriginalWidth;
87     }
88 
getOriginalHeight()89     public int getOriginalHeight() {
90         return mOriginalHeight;
91     }
92 
93     @Override
doInBackground(BitmapWorkerOptions... params)94     protected Drawable doInBackground(BitmapWorkerOptions... params) {
95 
96         return retrieveDrawable(params[0]);
97     }
98 
retrieveDrawable(BitmapWorkerOptions workerOptions)99     protected Drawable retrieveDrawable(BitmapWorkerOptions workerOptions) {
100         try {
101             if (workerOptions.getIconResource() != null) {
102                 return getBitmapFromResource(workerOptions.getIconResource(), workerOptions);
103             } else if (workerOptions.getResourceUri() != null) {
104                 if (UriUtils.isAndroidResourceUri(workerOptions.getResourceUri())
105                         || UriUtils.isShortcutIconResourceUri(workerOptions.getResourceUri())) {
106                     // Make an icon resource from this.
107                     return getBitmapFromResource(
108                             UriUtils.getIconResource(workerOptions.getResourceUri()),
109                             workerOptions);
110                 } else if (UriUtils.isWebUri(workerOptions.getResourceUri())) {
111                     return getBitmapFromHttp(workerOptions);
112                 } else if (UriUtils.isContentUri(workerOptions.getResourceUri())) {
113                     return getBitmapFromContent(workerOptions);
114                 } else if (UriUtils.isAccountImageUri(workerOptions.getResourceUri())) {
115                     return getAccountImage(workerOptions);
116                 } else {
117                     Log.e(TAG, "Error loading bitmap - unknown resource URI! "
118                             + workerOptions.getResourceUri());
119                 }
120             } else {
121                 Log.e(TAG, "Error loading bitmap - no source!");
122             }
123         } catch (IOException e) {
124             Log.e(TAG, "Error loading url " + workerOptions.getResourceUri(), e);
125             return null;
126         } catch (RuntimeException e) {
127             Log.e(TAG, "Critical Error loading url " + workerOptions.getResourceUri(), e);
128             return null;
129         }
130 
131         return null;
132     }
133 
134     @Override
onPostExecute(Drawable bitmap)135     protected void onPostExecute(Drawable bitmap) {
136         final ImageView imageView = mImageView.get();
137         if (imageView != null) {
138             imageView.setImageDrawable(bitmap);
139         }
140     }
141 
142     @Override
onCancelled(Drawable result)143     protected void onCancelled(Drawable result) {
144         if (result instanceof RefcountBitmapDrawable) {
145             // Remove the extra refcount created by us,  DrawableDownloader LruCache
146             // still holds one to the bitmap
147             RefcountBitmapDrawable d = (RefcountBitmapDrawable) result;
148             d.getRefcountObject().releaseRef();
149         }
150     }
151 
getBitmapFromResource(ShortcutIconResource iconResource, BitmapWorkerOptions outputOptions)152     private Drawable getBitmapFromResource(ShortcutIconResource iconResource,
153             BitmapWorkerOptions outputOptions) throws IOException {
154         if (DEBUG) {
155             Log.d(TAG, "Loading " + iconResource.toString());
156         }
157         try {
158             Object drawable = loadDrawable(outputOptions.getContext(), iconResource);
159             if (drawable instanceof InputStream) {
160                 // Most of these are bitmaps, so resize properly.
161                 return decodeBitmap((InputStream)drawable, outputOptions);
162             } else if (drawable instanceof Drawable){
163                 Drawable d = (Drawable) drawable;
164                 mOriginalWidth = d.getIntrinsicWidth();
165                 mOriginalHeight = d.getIntrinsicHeight();
166                 return d;
167             } else {
168                 Log.w(TAG, "getBitmapFromResource failed, unrecognized resource: " + drawable);
169                 return null;
170             }
171         } catch (NameNotFoundException e) {
172             Log.w(TAG, "Could not load package: " + iconResource.packageName + "! NameNotFound");
173             return null;
174         } catch (NotFoundException e) {
175             Log.w(TAG, "Could not load resource: " + iconResource.resourceName + "! NotFound");
176             return null;
177         }
178     }
179 
decodeBitmap(InputStream in, BitmapWorkerOptions options)180     private Drawable decodeBitmap(InputStream in, BitmapWorkerOptions options)
181             throws IOException {
182         CachedInputStream bufferedStream = null;
183         BitmapFactory.Options bitmapOptions = null;
184         try {
185             bufferedStream = new CachedInputStream(in);
186             // Let the bufferedStream be able to mark unlimited bytes up to full stream length.
187             // The value that BitmapFactory uses (1024) is too small for detecting bounds
188             bufferedStream.setOverrideMarkLimit(Integer.MAX_VALUE);
189             bitmapOptions = new BitmapFactory.Options();
190             bitmapOptions.inJustDecodeBounds = true;
191             if (options.getBitmapConfig() != null) {
192                 bitmapOptions.inPreferredConfig = options.getBitmapConfig();
193             }
194             bitmapOptions.inTempStorage = ByteArrayPool.get16KBPool().allocateChunk();
195             bufferedStream.mark(Integer.MAX_VALUE);
196             BitmapFactory.decodeStream(bufferedStream, null, bitmapOptions);
197 
198             mOriginalWidth = bitmapOptions.outWidth;
199             mOriginalHeight = bitmapOptions.outHeight;
200             int heightScale = 1;
201             int height = options.getHeight();
202             if (height > 0) {
203                 heightScale = bitmapOptions.outHeight / height;
204             }
205 
206             int widthScale = 1;
207             int width = options.getWidth();
208             if (width > 0) {
209                 widthScale = bitmapOptions.outWidth / width;
210             }
211 
212             int scale = heightScale > widthScale ? heightScale : widthScale;
213             if (scale <= 1) {
214                 scale = 1;
215             } else {
216                 int shift = 0;
217                 do {
218                     scale >>= 1;
219                     shift++;
220                 } while (scale != 0);
221                 scale = 1 << (shift - 1);
222             }
223 
224             if (DEBUG) {
225                 Log.d("BitmapWorkerTask", "Source bitmap: (" + bitmapOptions.outWidth + "x"
226                         + bitmapOptions.outHeight + ").  Max size: (" + options.getWidth() + "x"
227                         + options.getHeight() + ").  Chosen scale: " + scale + " -> " + scale);
228             }
229 
230             // Reset buffer to original position and disable the overrideMarkLimit
231             bufferedStream.reset();
232             bufferedStream.setOverrideMarkLimit(0);
233             Bitmap bitmap;
234             try {
235                 bitmapOptions.inJustDecodeBounds = false;
236                 bitmapOptions.inSampleSize = scale;
237                 bitmapOptions.inMutable = true;
238                 bitmapOptions.inBitmap = mRecycledBitmaps.getRecycledBitmap(
239                         mOriginalWidth / scale, mOriginalHeight / scale);
240                 bitmap = BitmapFactory.decodeStream(bufferedStream, null, bitmapOptions);
241             } catch (RuntimeException ex) {
242                 Log.e(TAG, "RuntimeException" + ex + ", trying decodeStream again");
243                 bufferedStream.reset();
244                 bufferedStream.setOverrideMarkLimit(0);
245                 bitmapOptions.inBitmap = null;
246                 bitmap = BitmapFactory.decodeStream(bufferedStream, null, bitmapOptions);
247             }
248             if (bitmap == null) {
249                 Log.d(TAG, "bitmap was null");
250                 return null;
251             }
252             RefcountObject<Bitmap> object = new RefcountObject<>(bitmap);
253             object.addRef();
254             object.setRefcountListener(mRefcountListener);
255             RefcountBitmapDrawable d = new RefcountBitmapDrawable(
256                     options.getContext().getResources(), object);
257             return d;
258         } finally {
259             Log.w(TAG, "couldn't load bitmap, releasing resources");
260             if (bitmapOptions != null) {
261                 ByteArrayPool.get16KBPool().releaseChunk(bitmapOptions.inTempStorage);
262             }
263             if (bufferedStream != null) {
264                 bufferedStream.close();
265             }
266         }
267     }
268 
getBitmapFromHttp(BitmapWorkerOptions options)269     private Drawable getBitmapFromHttp(BitmapWorkerOptions options) throws IOException {
270         URL url = new URL(options.getResourceUri().toString());
271         if (DEBUG) {
272             Log.d(TAG, "Loading " + url);
273         }
274         try {
275             // TODO use volley for better disk cache
276             URLConnection connection = url.openConnection();
277             connection.setConnectTimeout(SOCKET_TIMEOUT);
278             connection.setReadTimeout(READ_TIMEOUT);
279             InputStream in = connection.getInputStream();
280             return decodeBitmap(in, options);
281         } catch (SocketTimeoutException e) {
282             Log.e(TAG, "loading " + url + " timed out");
283         }
284         return null;
285     }
286 
getBitmapFromContent(BitmapWorkerOptions options)287     private Drawable getBitmapFromContent(BitmapWorkerOptions options)
288             throws IOException {
289         Uri resourceUri = options.getResourceUri();
290         if (resourceUri != null) {
291             try {
292                 InputStream bitmapStream =
293                         options.getContext().getContentResolver().openInputStream(resourceUri);
294 
295                 if (bitmapStream != null) {
296                     return decodeBitmap(bitmapStream, options);
297                 } else {
298                     Log.w(TAG, "Content provider returned a null InputStream when trying to " +
299                             "open resource.");
300                     return null;
301                 }
302             } catch (FileNotFoundException e) {
303                 Log.e(TAG, "FileNotFoundException during openInputStream for uri: "
304                         + resourceUri.toString());
305                 return null;
306             }
307         } else {
308             Log.w(TAG, "Get null resourceUri from BitmapWorkerOptions.");
309             return null;
310         }
311     }
312 
313     /**
314      * load drawable for non-bitmap resource or InputStream for bitmap resource without
315      * caching Bitmap in Resources.  So that caller can maintain a different caching
316      * storage with less memory used.
317      * @return  either {@link Drawable} for xml and ColorDrawable <br>
318      *          or {@link InputStream} for Bitmap resource
319      */
loadDrawable(Context context, ShortcutIconResource r)320     private static Object loadDrawable(Context context, ShortcutIconResource r)
321             throws NameNotFoundException {
322         Resources resources = context.getPackageManager().getResourcesForApplication(r.packageName);
323         if (resources == null) {
324             return null;
325         }
326         final int id = resources.getIdentifier(r.resourceName, null, null);
327         if (id == 0) {
328             Log.e(TAG, "Couldn't get resource " + r.resourceName + " in resources of "
329                     + r.packageName);
330             return null;
331         }
332         TypedValue value = new TypedValue();
333         resources.getValue(id, value, true);
334         if ((value.type == TypedValue.TYPE_STRING && value.string.toString().endsWith(".xml")) || (
335                 value.type >= TypedValue.TYPE_FIRST_COLOR_INT
336                 && value.type <= TypedValue.TYPE_LAST_COLOR_INT)) {
337             return resources.getDrawable(id);
338         }
339         return resources.openRawResource(id, value);
340     }
341 
getDrawable(Context context, ShortcutIconResource iconResource)342     public static Drawable getDrawable(Context context, ShortcutIconResource iconResource)
343             throws NameNotFoundException {
344         Resources resources =
345                 context.getPackageManager().getResourcesForApplication(iconResource.packageName);
346         int id = resources.getIdentifier(iconResource.resourceName, null, null);
347         if (id == 0) {
348             throw new NameNotFoundException();
349         }
350         return resources.getDrawable(id);
351     }
352 
getAccountImage(BitmapWorkerOptions options)353     private Drawable getAccountImage(BitmapWorkerOptions options) {
354         String accountName = UriUtils.getAccountName(options.getResourceUri());
355         Context context = options.getContext();
356 
357         if (accountName != null && context != null) {
358             Account thisAccount = null;
359             for (Account account : AccountManager.get(context).
360                     getAccountsByType(GOOGLE_ACCOUNT_TYPE)) {
361                 if (account.name.equals(accountName)) {
362                     thisAccount = account;
363                     break;
364                 }
365             }
366             if (thisAccount != null) {
367                 String picUriString = AccountImageHelper.getAccountPictureUri(context, thisAccount);
368                 if (picUriString != null) {
369                     BitmapWorkerOptions.Builder optionBuilder =
370                             new BitmapWorkerOptions.Builder(context)
371                             .width(options.getWidth())
372                                     .height(options.getHeight())
373                                     .cacheFlag(options.getCacheFlag())
374                                     .bitmapConfig(options.getBitmapConfig())
375                                     .resource(Uri.parse(picUriString));
376                     return DrawableDownloader.getInstance(context)
377                             .loadBitmapBlocking(optionBuilder.build());
378                 }
379                 return null;
380             }
381         }
382         return null;
383     }
384 }
385