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