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