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 
BitmapUtils()51     private BitmapUtils() { /* cannot be instantiated */ }
52 
scaleBitmap(Bitmap bm, int maxWidth, int maxHeight)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 
calculateNewSize(Bitmap bm, int maxWidth, int maxHeight)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 
createScaledBitmapInfo(String id, Bitmap bm, int maxWidth, int maxHeight)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      */
decodeSampledBitmapFromUriString(Context context, String uriString, int reqWidth, int reqHeight)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 
calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight)133     private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth,
134             int reqHeight) {
135         return calculateInSampleSize(options.outWidth, options.outHeight, reqWidth, reqHeight);
136     }
137 
calculateInSampleSize(int width, int height, int reqWidth, int reqHeight)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 
getInputStream(Context context, String uriString)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 
isContentResolverUri(Uri uri)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 
close(Closeable closeable)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          */
ScaledBitmapInfo(@onNull String id, @NonNull Bitmap bitmap, int inSampleSize)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          */
needToReload(int reqWidth, int reqHeight)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          */
needToReload(ScaledBitmapInfo other)239         public boolean needToReload(ScaledBitmapInfo other){
240             return needToReload(other.bitmap.getWidth(), other.bitmap.getHeight());
241         }
242 
243         @Override
toString()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      */
setColorFilterToDrawable(int color, Drawable drawable)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