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