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