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