Home | History | Annotate | Download | only in images
      1 /*
      2  * Copyright (C) 2015 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.util.images;
     18 
     19 import android.content.ContentResolver;
     20 import android.content.Context;
     21 import android.database.sqlite.SQLiteException;
     22 import android.graphics.Bitmap;
     23 import android.graphics.BitmapFactory;
     24 import android.graphics.PorterDuff;
     25 import android.graphics.Rect;
     26 import android.graphics.drawable.Drawable;
     27 import android.net.TrafficStats;
     28 import android.net.Uri;
     29 import android.support.annotation.NonNull;
     30 import android.text.TextUtils;
     31 import android.util.Log;
     32 import com.android.tv.common.util.NetworkTrafficTags;
     33 import java.io.BufferedInputStream;
     34 import java.io.Closeable;
     35 import java.io.IOException;
     36 import java.io.InputStream;
     37 import java.net.HttpURLConnection;
     38 import java.net.URL;
     39 import java.net.URLConnection;
     40 
     41 public final class BitmapUtils {
     42     private static final String TAG = "BitmapUtils";
     43     private static final boolean DEBUG = false;
     44 
     45     // The value of 64K, for MARK_READ_LIMIT, is chosen to be eight times the default buffer size
     46     // of BufferedInputStream (8K) allowing it to double its buffers three times. Also it is a
     47     // fairly reasonable value, not using too much memory and being large enough for most cases.
     48     private static final int MARK_READ_LIMIT = 64 * 1024; // 64K
     49 
     50     private static final int CONNECTION_TIMEOUT_MS_FOR_URLCONNECTION = 3000; // 3 sec
     51     private static final int READ_TIMEOUT_MS_FOR_URLCONNECTION = 10000; // 10 sec
     52 
     53     private BitmapUtils() {
     54         /* cannot be instantiated */
     55     }
     56 
     57     public static Bitmap scaleBitmap(Bitmap bm, int maxWidth, int maxHeight) {
     58         Rect rect = calculateNewSize(bm, maxWidth, maxHeight);
     59         return Bitmap.createScaledBitmap(bm, rect.right, rect.bottom, false);
     60     }
     61 
     62     public static Bitmap getScaledMutableBitmap(Bitmap bm, int maxWidth, int maxHeight) {
     63         Bitmap scaledBitmap = scaleBitmap(bm, maxWidth, maxHeight);
     64         return scaledBitmap.isMutable()
     65                 ? scaledBitmap
     66                 : scaledBitmap.copy(Bitmap.Config.ARGB_8888, true);
     67     }
     68 
     69     private static Rect calculateNewSize(Bitmap bm, int maxWidth, int maxHeight) {
     70         final double ratio = maxHeight / (double) maxWidth;
     71         final double bmRatio = bm.getHeight() / (double) bm.getWidth();
     72         Rect rect = new Rect();
     73         if (ratio > bmRatio) {
     74             rect.right = maxWidth;
     75             rect.bottom = Math.round((float) bm.getHeight() * maxWidth / bm.getWidth());
     76         } else {
     77             rect.right = Math.round((float) bm.getWidth() * maxHeight / bm.getHeight());
     78             rect.bottom = maxHeight;
     79         }
     80         return rect;
     81     }
     82 
     83     public static ScaledBitmapInfo createScaledBitmapInfo(
     84             String id, Bitmap bm, int maxWidth, int maxHeight) {
     85         return new ScaledBitmapInfo(
     86                 id,
     87                 scaleBitmap(bm, maxWidth, maxHeight),
     88                 calculateInSampleSize(bm.getWidth(), bm.getHeight(), maxWidth, maxHeight));
     89     }
     90 
     91     /** Decode large sized bitmap into requested size. */
     92     public static ScaledBitmapInfo decodeSampledBitmapFromUriString(
     93             Context context, String uriString, int reqWidth, int reqHeight) {
     94         if (TextUtils.isEmpty(uriString)) {
     95             return null;
     96         }
     97 
     98         Uri uri = Uri.parse(uriString).normalizeScheme();
     99         boolean isResourceUri = isContentResolverUri(uri);
    100         URLConnection urlConnection = null;
    101         InputStream inputStream = null;
    102         final int oldTag = TrafficStats.getThreadStatsTag();
    103         TrafficStats.setThreadStatsTag(NetworkTrafficTags.LOGO_FETCHER);
    104         try {
    105             if (isResourceUri) {
    106                 inputStream = context.getContentResolver().openInputStream(uri);
    107             } else {
    108                 // If the URLConnection is HttpURLConnection, disconnect() should be called
    109                 // explicitly.
    110                 urlConnection = getUrlConnection(uriString);
    111                 inputStream = urlConnection.getInputStream();
    112             }
    113             inputStream = new BufferedInputStream(inputStream);
    114             inputStream.mark(MARK_READ_LIMIT);
    115 
    116             // Check the bitmap dimensions.
    117             BitmapFactory.Options options = new BitmapFactory.Options();
    118             options.inJustDecodeBounds = true;
    119             BitmapFactory.decodeStream(inputStream, null, options);
    120 
    121             // Rewind the stream in order to restart bitmap decoding.
    122             try {
    123                 inputStream.reset();
    124             } catch (IOException e) {
    125                 if (DEBUG) Log.i(TAG, "Failed to rewind stream: " + uriString, e);
    126 
    127                 // Failed to rewind the stream, try to reopen it.
    128                 close(inputStream, urlConnection);
    129                 if (isResourceUri) {
    130                     inputStream = context.getContentResolver().openInputStream(uri);
    131                 } else {
    132                     urlConnection = getUrlConnection(uriString);
    133                     inputStream = urlConnection.getInputStream();
    134                 }
    135             }
    136 
    137             // Decode the bitmap possibly resizing it.
    138             options.inJustDecodeBounds = false;
    139             options.inPreferredConfig = Bitmap.Config.RGB_565;
    140             options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
    141             Bitmap bitmap = BitmapFactory.decodeStream(inputStream, null, options);
    142             if (bitmap == null) {
    143                 return null;
    144             }
    145             return new ScaledBitmapInfo(uriString, bitmap, options.inSampleSize);
    146         } catch (IOException e) {
    147             if (DEBUG) {
    148                 // It can happens in normal cases like when a channel doesn't have any logo.
    149                 Log.w(TAG, "Failed to open stream: " + uriString, e);
    150             }
    151             return null;
    152         } catch (SQLiteException e) {
    153             Log.e(TAG, "Failed to open stream: " + uriString, e);
    154             return null;
    155         } finally {
    156             close(inputStream, urlConnection);
    157             TrafficStats.setThreadStatsTag(oldTag);
    158         }
    159     }
    160 
    161     private static URLConnection getUrlConnection(String uriString) throws IOException {
    162         URLConnection urlConnection = new URL(uriString).openConnection();
    163         urlConnection.setConnectTimeout(CONNECTION_TIMEOUT_MS_FOR_URLCONNECTION);
    164         urlConnection.setReadTimeout(READ_TIMEOUT_MS_FOR_URLCONNECTION);
    165         return urlConnection;
    166     }
    167 
    168     private static int calculateInSampleSize(
    169             BitmapFactory.Options options, int reqWidth, int reqHeight) {
    170         return calculateInSampleSize(options.outWidth, options.outHeight, reqWidth, reqHeight);
    171     }
    172 
    173     private static int calculateInSampleSize(int width, int height, int reqWidth, int reqHeight) {
    174         // Calculates the largest inSampleSize that, is a power of two and, keeps either width or
    175         // height larger or equal to the requested width and height.
    176         int ratio = Math.max(width / reqWidth, height / reqHeight);
    177         return Math.max(1, Integer.highestOneBit(ratio));
    178     }
    179 
    180     private static boolean isContentResolverUri(Uri uri) {
    181         String scheme = uri.getScheme();
    182         return ContentResolver.SCHEME_CONTENT.equals(scheme)
    183                 || ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme)
    184                 || ContentResolver.SCHEME_FILE.equals(scheme);
    185     }
    186 
    187     private static void close(Closeable closeable, URLConnection urlConnection) {
    188         if (closeable != null) {
    189             try {
    190                 closeable.close();
    191             } catch (IOException e) {
    192                 // Log and continue.
    193                 Log.w(TAG, "Error closing " + closeable, e);
    194             }
    195         }
    196         if (urlConnection instanceof HttpURLConnection) {
    197             ((HttpURLConnection) urlConnection).disconnect();
    198         }
    199     }
    200 
    201     /** A wrapper class which contains the loaded bitmap and the scaling information. */
    202     public static class ScaledBitmapInfo {
    203         /** The id of bitmap, usually this is the URI of the original. */
    204         @NonNull public final String id;
    205 
    206         /** The loaded bitmap object. */
    207         @NonNull public final Bitmap bitmap;
    208 
    209         /**
    210          * The scaling factor to the original bitmap. It should be an positive integer.
    211          *
    212          * @see android.graphics.BitmapFactory.Options#inSampleSize
    213          */
    214         public final int inSampleSize;
    215 
    216         /**
    217          * A constructor.
    218          *
    219          * @param bitmap The loaded bitmap object.
    220          * @param inSampleSize The sampling size. See {@link
    221          *     android.graphics.BitmapFactory.Options#inSampleSize}
    222          */
    223         public ScaledBitmapInfo(@NonNull String id, @NonNull Bitmap bitmap, int inSampleSize) {
    224             this.id = id;
    225             this.bitmap = bitmap;
    226             this.inSampleSize = inSampleSize;
    227         }
    228 
    229         /**
    230          * Checks if the bitmap needs to be reloaded. The scaling is performed by power 2. The
    231          * bitmap can be reloaded only if the required width or height is greater then or equal to
    232          * the existing bitmap. If the full sized bitmap is already loaded, returns {@code false}.
    233          *
    234          * @see android.graphics.BitmapFactory.Options#inSampleSize
    235          */
    236         public boolean needToReload(int reqWidth, int reqHeight) {
    237             if (inSampleSize <= 1) {
    238                 if (DEBUG) Log.d(TAG, "Reload not required " + this + " already full size.");
    239                 return false;
    240             }
    241             Rect size = calculateNewSize(this.bitmap, reqWidth, reqHeight);
    242             boolean reload =
    243                     (size.right >= bitmap.getWidth() * 2 || size.bottom >= bitmap.getHeight() * 2);
    244             if (DEBUG) {
    245                 Log.d(
    246                         TAG,
    247                         "needToReload("
    248                                 + reqWidth
    249                                 + ", "
    250                                 + reqHeight
    251                                 + ")="
    252                                 + reload
    253                                 + " because the new size would be "
    254                                 + size
    255                                 + " for "
    256                                 + this);
    257             }
    258             return reload;
    259         }
    260 
    261         /** Returns {@code true} if a request the size of {@code other} would need a reload. */
    262         public boolean needToReload(ScaledBitmapInfo other) {
    263             return needToReload(other.bitmap.getWidth(), other.bitmap.getHeight());
    264         }
    265 
    266         @Override
    267         public String toString() {
    268             return "ScaledBitmapInfo["
    269                     + id
    270                     + "](in="
    271                     + inSampleSize
    272                     + ", w="
    273                     + bitmap.getWidth()
    274                     + ", h="
    275                     + bitmap.getHeight()
    276                     + ")";
    277         }
    278     }
    279 
    280     /**
    281      * Applies a color filter to the {@code drawable}. The color filter is made with the given
    282      * {@code color} and {@link android.graphics.PorterDuff.Mode#SRC_ATOP}.
    283      *
    284      * @see Drawable#setColorFilter
    285      */
    286     public static void setColorFilterToDrawable(int color, Drawable drawable) {
    287         if (drawable != null) {
    288             drawable.mutate().setColorFilter(color, PorterDuff.Mode.SRC_ATOP);
    289         }
    290     }
    291 }
    292