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