Home | History | Annotate | Download | only in widget
      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 
     80     DrawableLoader(ImageView imageView, RecycleBitmapPool recycledBitmapPool) {
     81         mImageView = new WeakReference<>(imageView);
     82         mRecycledBitmaps = recycledBitmapPool;
     83     }
     84 
     85     public int getOriginalWidth() {
     86         return mOriginalWidth;
     87     }
     88 
     89     public int getOriginalHeight() {
     90         return mOriginalHeight;
     91     }
     92 
     93     @Override
     94     protected Drawable doInBackground(BitmapWorkerOptions... params) {
     95 
     96         return retrieveDrawable(params[0]);
     97     }
     98 
     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
    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
    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 
    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 
    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 
    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 
    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      */
    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 
    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 
    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