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