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 WeakReference<ImageView> mImageView;
67     private int mOriginalWidth;
68     private int mOriginalHeight;
69     private RecycleBitmapPool mRecycledBitmaps;
70 
71     private 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>(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         if (mImageView != null) {
137             final ImageView imageView = mImageView.get();
138             if (imageView != null) {
139                 imageView.setImageDrawable(bitmap);
140             }
141         }
142     }
143 
144     @Override
onCancelled(Drawable result)145     protected void onCancelled(Drawable result) {
146         if (result instanceof RefcountBitmapDrawable) {
147             // Remove the extra refcount created by us,  DrawableDownloader LruCache
148             // still holds one to the bitmap
149             RefcountBitmapDrawable d = (RefcountBitmapDrawable) result;
150             d.getRefcountObject().releaseRef();
151         }
152     }
153 
getBitmapFromResource(ShortcutIconResource iconResource, BitmapWorkerOptions outputOptions)154     private Drawable getBitmapFromResource(ShortcutIconResource iconResource,
155             BitmapWorkerOptions outputOptions) throws IOException {
156         if (DEBUG) {
157             Log.d(TAG, "Loading " + iconResource.toString());
158         }
159         String packageName = iconResource.packageName;
160         String resourceName = iconResource.resourceName;
161         try {
162             Object drawable = loadDrawable(outputOptions.getContext(), iconResource);
163             if (drawable instanceof InputStream) {
164                 // Most of these are bitmaps, so resize properly.
165                 return decodeBitmap((InputStream)drawable, outputOptions);
166             } else if (drawable instanceof Drawable){
167                 Drawable d = (Drawable) drawable;
168                 mOriginalWidth = d.getIntrinsicWidth();
169                 mOriginalHeight = d.getIntrinsicHeight();
170                 return d;
171             } else {
172                 Log.w(TAG, "getBitmapFromResource failed, unrecognized resource: " + drawable);
173                 return null;
174             }
175         } catch (NameNotFoundException e) {
176             Log.w(TAG, "Could not load package: " + iconResource.packageName + "! NameNotFound");
177             return null;
178         } catch (NotFoundException e) {
179             Log.w(TAG, "Could not load resource: " + iconResource.resourceName + "! NotFound");
180             return null;
181         }
182     }
183 
decodeBitmap(InputStream in, BitmapWorkerOptions options)184     private Drawable decodeBitmap(InputStream in, BitmapWorkerOptions options)
185             throws IOException {
186         CachedInputStream bufferedStream = null;
187         BitmapFactory.Options bitmapOptions = null;
188         try {
189             bufferedStream = new CachedInputStream(in);
190             // Let the bufferedStream be able to mark unlimited bytes up to full stream length.
191             // The value that BitmapFactory uses (1024) is too small for detecting bounds
192             bufferedStream.setOverrideMarkLimit(Integer.MAX_VALUE);
193             bitmapOptions = new BitmapFactory.Options();
194             bitmapOptions.inJustDecodeBounds = true;
195             if (options.getBitmapConfig() != null) {
196                 bitmapOptions.inPreferredConfig = options.getBitmapConfig();
197             }
198             bitmapOptions.inTempStorage = ByteArrayPool.get16KBPool().allocateChunk();
199             bufferedStream.mark(Integer.MAX_VALUE);
200             BitmapFactory.decodeStream(bufferedStream, null, bitmapOptions);
201 
202             mOriginalWidth = bitmapOptions.outWidth;
203             mOriginalHeight = bitmapOptions.outHeight;
204             int heightScale = 1;
205             int height = options.getHeight();
206             if (height > 0) {
207                 heightScale = bitmapOptions.outHeight / height;
208             }
209 
210             int widthScale = 1;
211             int width = options.getWidth();
212             if (width > 0) {
213                 widthScale = bitmapOptions.outWidth / width;
214             }
215 
216             int scale = heightScale > widthScale ? heightScale : widthScale;
217             if (scale <= 1) {
218                 scale = 1;
219             } else {
220                 int shift = 0;
221                 do {
222                     scale >>= 1;
223                     shift++;
224                 } while (scale != 0);
225                 scale = 1 << (shift - 1);
226             }
227 
228             if (DEBUG) {
229                 Log.d("BitmapWorkerTask", "Source bitmap: (" + bitmapOptions.outWidth + "x"
230                         + bitmapOptions.outHeight + ").  Max size: (" + options.getWidth() + "x"
231                         + options.getHeight() + ").  Chosen scale: " + scale + " -> " + scale);
232             }
233 
234             // Reset buffer to original position and disable the overrideMarkLimit
235             bufferedStream.reset();
236             bufferedStream.setOverrideMarkLimit(0);
237             Bitmap bitmap = null;
238             try {
239                 bitmapOptions.inJustDecodeBounds = false;
240                 bitmapOptions.inSampleSize = scale;
241                 bitmapOptions.inMutable = true;
242                 bitmapOptions.inBitmap = mRecycledBitmaps.getRecycledBitmap(
243                         mOriginalWidth / scale, mOriginalHeight / scale);
244                 bitmap = BitmapFactory.decodeStream(bufferedStream, null, bitmapOptions);
245             } catch (RuntimeException ex) {
246                 Log.e(TAG, "RuntimeException" + ex + ", trying decodeStream again");
247                 bufferedStream.reset();
248                 bufferedStream.setOverrideMarkLimit(0);
249                 bitmapOptions.inBitmap = null;
250                 bitmap = BitmapFactory.decodeStream(bufferedStream, null, bitmapOptions);
251             }
252             if (bitmap == null) {
253                 Log.d(TAG, "bitmap was null");
254                 return null;
255             }
256             RefcountObject<Bitmap> object = new RefcountObject<Bitmap>(bitmap);
257             object.addRef();
258             object.setRefcountListener(mRefcountListener);
259             RefcountBitmapDrawable d = new RefcountBitmapDrawable(
260                     options.getContext().getResources(), object);
261             return d;
262         } finally {
263             Log.w(TAG, "couldn't load bitmap, releasing resources");
264             if (bitmapOptions != null) {
265                 ByteArrayPool.get16KBPool().releaseChunk(bitmapOptions.inTempStorage);
266             }
267             if (bufferedStream != null) {
268                 bufferedStream.close();
269             }
270         }
271     }
272 
getBitmapFromHttp(BitmapWorkerOptions options)273     private Drawable getBitmapFromHttp(BitmapWorkerOptions options) throws IOException {
274         URL url = new URL(options.getResourceUri().toString());
275         if (DEBUG) {
276             Log.d(TAG, "Loading " + url);
277         }
278         try {
279             // TODO use volley for better disk cache
280             URLConnection connection = url.openConnection();
281             connection.setConnectTimeout(SOCKET_TIMEOUT);
282             connection.setReadTimeout(READ_TIMEOUT);
283             InputStream in = connection.getInputStream();
284             return decodeBitmap(in, options);
285         } catch (SocketTimeoutException e) {
286             Log.e(TAG, "loading " + url + " timed out");
287         }
288         return null;
289     }
290 
getBitmapFromContent(BitmapWorkerOptions options)291     private Drawable getBitmapFromContent(BitmapWorkerOptions options)
292             throws IOException {
293         Uri resourceUri = options.getResourceUri();
294         if (resourceUri != null) {
295             try {
296                 InputStream bitmapStream =
297                         options.getContext().getContentResolver().openInputStream(resourceUri);
298 
299                 if (bitmapStream != null) {
300                     return decodeBitmap(bitmapStream, options);
301                 } else {
302                     Log.w(TAG, "Content provider returned a null InputStream when trying to " +
303                             "open resource.");
304                     return null;
305                 }
306             } catch (FileNotFoundException e) {
307                 Log.e(TAG, "FileNotFoundException during openInputStream for uri: "
308                         + resourceUri.toString());
309                 return null;
310             }
311         } else {
312             Log.w(TAG, "Get null resourceUri from BitmapWorkerOptions.");
313             return null;
314         }
315     }
316 
317     /**
318      * load drawable for non-bitmap resource or InputStream for bitmap resource without
319      * caching Bitmap in Resources.  So that caller can maintain a different caching
320      * storage with less memory used.
321      * @return  either {@link Drawable} for xml and ColorDrawable <br>
322      *          or {@link InputStream} for Bitmap resource
323      */
loadDrawable(Context context, ShortcutIconResource r)324     private static Object loadDrawable(Context context, ShortcutIconResource r)
325             throws NameNotFoundException {
326         Resources resources = context.getPackageManager()
327                 .getResourcesForApplication(r.packageName);
328         if (resources == null) {
329             return null;
330         }
331         final int id = resources.getIdentifier(r.resourceName, null, null);
332         if (id == 0) {
333             Log.e(TAG, "Couldn't get resource " + r.resourceName + " in resources of "
334                     + r.packageName);
335             return null;
336         }
337         TypedValue value = new TypedValue();
338         resources.getValue(id, value, true);
339         if ((value.type == TypedValue.TYPE_STRING && value.string.toString().endsWith(".xml")) || (
340                 value.type >= TypedValue.TYPE_FIRST_COLOR_INT
341                 && value.type <= TypedValue.TYPE_LAST_COLOR_INT)) {
342             return resources.getDrawable(id);
343         }
344         return resources.openRawResource(id, value);
345     }
346 
getDrawable(Context context, ShortcutIconResource iconResource)347     public static Drawable getDrawable(Context context, ShortcutIconResource iconResource)
348             throws NameNotFoundException {
349         Resources resources =
350                 context.getPackageManager().getResourcesForApplication(iconResource.packageName);
351         int id = resources.getIdentifier(iconResource.resourceName, null, null);
352         if (id == 0) {
353             throw new NameNotFoundException();
354         }
355         return resources.getDrawable(id);
356     }
357 
getAccountImage(BitmapWorkerOptions options)358     private Drawable getAccountImage(BitmapWorkerOptions options) {
359         String accountName = UriUtils.getAccountName(options.getResourceUri());
360         Context context = options.getContext();
361 
362         if (accountName != null && context != null) {
363             Account thisAccount = null;
364             for (Account account : AccountManager.get(context).
365                     getAccountsByType(GOOGLE_ACCOUNT_TYPE)) {
366                 if (account.name.equals(accountName)) {
367                     thisAccount = account;
368                     break;
369                 }
370             }
371             if (thisAccount != null) {
372                 String picUriString = AccountImageHelper.getAccountPictureUri(context, thisAccount);
373                 if (picUriString != null) {
374                     BitmapWorkerOptions.Builder optionBuilder =
375                             new BitmapWorkerOptions.Builder(context)
376                             .width(options.getWidth())
377                                     .height(options.getHeight())
378                                     .cacheFlag(options.getCacheFlag())
379                                     .bitmapConfig(options.getBitmapConfig())
380                                     .resource(Uri.parse(picUriString));
381                     return DrawableDownloader.getInstance(context)
382                             .loadBitmapBlocking(optionBuilder.build());
383                 }
384                 return null;
385             }
386         }
387         return null;
388     }
389 }
390