• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
BitmapUtils()53     private BitmapUtils() {
54         /* cannot be instantiated */
55     }
56 
scaleBitmap(Bitmap bm, int maxWidth, int maxHeight)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 
getScaledMutableBitmap(Bitmap bm, int maxWidth, int maxHeight)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 
calculateNewSize(Bitmap bm, int maxWidth, int maxHeight)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 
createScaledBitmapInfo( String id, Bitmap bm, int maxWidth, int maxHeight)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. */
decodeSampledBitmapFromUriString( Context context, String uriString, int reqWidth, int reqHeight)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 
getUrlConnection(String uriString)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 
calculateInSampleSize( BitmapFactory.Options options, int reqWidth, int reqHeight)168     private static int calculateInSampleSize(
169             BitmapFactory.Options options, int reqWidth, int reqHeight) {
170         return calculateInSampleSize(options.outWidth, options.outHeight, reqWidth, reqHeight);
171     }
172 
calculateInSampleSize(int width, int height, int reqWidth, int reqHeight)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 
isContentResolverUri(Uri uri)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 
close(Closeable closeable, URLConnection urlConnection)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          */
ScaledBitmapInfo(@onNull String id, @NonNull Bitmap bitmap, int inSampleSize)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          */
needToReload(int reqWidth, int reqHeight)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. */
needToReload(ScaledBitmapInfo other)262         public boolean needToReload(ScaledBitmapInfo other) {
263             return needToReload(other.bitmap.getWidth(), other.bitmap.getHeight());
264         }
265 
266         @Override
toString()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      */
setColorFilterToDrawable(int color, Drawable drawable)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