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 53 private BitmapUtils() { 54 /* cannot be instantiated */ 55 } 56 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 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 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 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. */ 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 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 168 private static int calculateInSampleSize( 169 BitmapFactory.Options options, int reqWidth, int reqHeight) { 170 return calculateInSampleSize(options.outWidth, options.outHeight, reqWidth, reqHeight); 171 } 172 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 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 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 */ 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 */ 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. */ 262 public boolean needToReload(ScaledBitmapInfo other) { 263 return needToReload(other.bitmap.getWidth(), other.bitmap.getHeight()); 264 } 265 266 @Override 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 */ 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