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