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