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 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 
     80     DrawableLoader(ImageView imageView, RecycleBitmapPool recycledBitmapPool) {
     81         mImageView = new WeakReference<ImageView>(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         if (mImageView != null) {
    137             final ImageView imageView = mImageView.get();
    138             if (imageView != null) {
    139                 imageView.setImageDrawable(bitmap);
    140             }
    141         }
    142     }
    143 
    144     @Override
    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 
    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 
    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 
    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 
    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      */
    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 
    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 
    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