1 /* 2 * Copyright (C) 2017 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 package com.android.wallpaper.asset; 17 18 import android.app.Activity; 19 import android.graphics.Bitmap; 20 import android.graphics.Bitmap.Config; 21 import android.graphics.BitmapFactory; 22 import android.graphics.BitmapRegionDecoder; 23 import android.graphics.Matrix; 24 import android.graphics.Point; 25 import android.graphics.Rect; 26 import android.media.ExifInterface; 27 import android.os.Handler; 28 import android.os.Looper; 29 import android.util.Log; 30 31 import androidx.annotation.Nullable; 32 33 import java.io.IOException; 34 import java.io.InputStream; 35 import java.util.concurrent.ExecutorService; 36 import java.util.concurrent.Executors; 37 38 /** 39 * Represents Asset types for which bytes can be read directly, allowing for flexible bitmap 40 * decoding. 41 */ 42 public abstract class StreamableAsset extends Asset { 43 private static final ExecutorService sExecutorService = Executors.newCachedThreadPool(); 44 private static final String TAG = "StreamableAsset"; 45 46 private BitmapRegionDecoder mBitmapRegionDecoder; 47 private Point mDimensions; 48 49 /** 50 * Scales and returns a new Rect from the given Rect by the given scaling factor. 51 */ scaleRect(Rect rect, float scale)52 public static Rect scaleRect(Rect rect, float scale) { 53 return new Rect( 54 Math.round((float) rect.left * scale), 55 Math.round((float) rect.top * scale), 56 Math.round((float) rect.right * scale), 57 Math.round((float) rect.bottom * scale)); 58 } 59 60 /** 61 * Maps from EXIF orientation tag values to counterclockwise degree rotation values. 62 */ getDegreesRotationForExifOrientation(int exifOrientation)63 private static int getDegreesRotationForExifOrientation(int exifOrientation) { 64 switch (exifOrientation) { 65 case ExifInterface.ORIENTATION_NORMAL: 66 return 0; 67 case ExifInterface.ORIENTATION_ROTATE_90: 68 return 90; 69 case ExifInterface.ORIENTATION_ROTATE_180: 70 return 180; 71 case ExifInterface.ORIENTATION_ROTATE_270: 72 return 270; 73 default: 74 Log.w(TAG, "Unsupported EXIF orientation " + exifOrientation); 75 return 0; 76 } 77 } 78 79 @Override decodeBitmap(int targetWidth, int targetHeight, boolean useHardwareBitmapIfPossible, BitmapReceiver receiver)80 public void decodeBitmap(int targetWidth, int targetHeight, boolean useHardwareBitmapIfPossible, 81 BitmapReceiver receiver) { 82 sExecutorService.execute(() -> { 83 int newTargetWidth = targetWidth; 84 int newTargetHeight = targetHeight; 85 int exifOrientation = getExifOrientation(); 86 // Switch target height and width if image is rotated 90 or 270 degrees. 87 if (exifOrientation == ExifInterface.ORIENTATION_ROTATE_90 88 || exifOrientation == ExifInterface.ORIENTATION_ROTATE_270) { 89 int tempHeight = newTargetHeight; 90 newTargetHeight = newTargetWidth; 91 newTargetWidth = tempHeight; 92 } 93 94 BitmapFactory.Options options = new BitmapFactory.Options(); 95 96 Point rawDimensions = calculateRawDimensions(); 97 // Raw dimensions may be null if there was an error opening the underlying input stream. 98 if (rawDimensions == null) { 99 decodeBitmapCompleted(receiver, null); 100 return; 101 } 102 options.inSampleSize = BitmapUtils.calculateInSampleSize( 103 rawDimensions.x, rawDimensions.y, newTargetWidth, newTargetHeight); 104 if (useHardwareBitmapIfPossible) { 105 options.inPreferredConfig = Config.HARDWARE; 106 } 107 108 InputStream inputStream = openInputStream(); 109 Bitmap bitmap = null; 110 if (inputStream != null) { 111 bitmap = BitmapFactory.decodeStream(inputStream, null, options); 112 closeInputStream( 113 inputStream, "Error closing the input stream used " 114 + "to decode the full bitmap"); 115 116 // Rotate output bitmap if necessary because of EXIF orientation tag. 117 int matrixRotation = getDegreesRotationForExifOrientation(exifOrientation); 118 if (matrixRotation > 0) { 119 Matrix rotateMatrix = new Matrix(); 120 rotateMatrix.setRotate(matrixRotation); 121 bitmap = Bitmap.createBitmap( 122 bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), 123 rotateMatrix, false); 124 } 125 } 126 decodeBitmapCompleted(receiver, bitmap); 127 }); 128 } 129 130 @Override decodeBitmap(BitmapReceiver receiver)131 public void decodeBitmap(BitmapReceiver receiver) { 132 sExecutorService.execute(() -> { 133 BitmapFactory.Options options = new BitmapFactory.Options(); 134 options.inPreferredConfig = Config.HARDWARE; 135 InputStream inputStream = openInputStream(); 136 Bitmap bitmap = null; 137 if (inputStream != null) { 138 bitmap = BitmapFactory.decodeStream(inputStream, null, options); 139 closeInputStream(inputStream, 140 "Error closing the input stream used to decode the full bitmap"); 141 142 // Rotate output bitmap if necessary because of EXIF orientation tag. 143 int exifOrientation = getExifOrientation(); 144 int matrixRotation = getDegreesRotationForExifOrientation(exifOrientation); 145 if (matrixRotation > 0) { 146 Matrix rotateMatrix = new Matrix(); 147 rotateMatrix.setRotate(matrixRotation); 148 bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), 149 bitmap.getHeight(), rotateMatrix, false); 150 } 151 } 152 decodeBitmapCompleted(receiver, bitmap); 153 }); 154 } 155 156 @Override decodeRawDimensions(Activity unused, DimensionsReceiver receiver)157 public void decodeRawDimensions(Activity unused, DimensionsReceiver receiver) { 158 sExecutorService.execute(() -> { 159 Point result = calculateRawDimensions(); 160 new Handler(Looper.getMainLooper()).post(() -> { 161 receiver.onDimensionsDecoded(result); 162 }); 163 }); 164 } 165 166 @Override decodeBitmapRegion(Rect rect, int targetWidth, int targetHeight, boolean shouldAdjustForRtl, BitmapReceiver receiver)167 public void decodeBitmapRegion(Rect rect, int targetWidth, int targetHeight, 168 boolean shouldAdjustForRtl, BitmapReceiver receiver) { 169 runDecodeBitmapRegionTask(rect, targetWidth, targetHeight, shouldAdjustForRtl, receiver); 170 } 171 172 @Override supportsTiling()173 public boolean supportsTiling() { 174 return true; 175 } 176 177 /** 178 * Fetches an input stream of bytes for the wallpaper image asset and provides the stream 179 * asynchronously back to a {@link StreamReceiver}. 180 */ fetchInputStream(final StreamReceiver streamReceiver)181 public void fetchInputStream(final StreamReceiver streamReceiver) { 182 sExecutorService.execute(() -> { 183 InputStream result = openInputStream(); 184 new Handler(Looper.getMainLooper()).post(() -> { 185 streamReceiver.onInputStreamOpened(result); 186 }); 187 }); 188 } 189 190 /** 191 * Returns an InputStream representing the asset. Should only be called off the main UI thread. 192 */ 193 @Nullable openInputStream()194 protected abstract InputStream openInputStream(); 195 196 /** 197 * Gets the EXIF orientation value of the asset. This method should only be called off the main UI 198 * thread. 199 */ getExifOrientation()200 public int getExifOrientation() { 201 // By default, assume that the EXIF orientation is normal (i.e., bitmap is rotated 0 degrees 202 // from how it should be rendered to a viewer). 203 return ExifInterface.ORIENTATION_NORMAL; 204 } 205 206 /** 207 * Decodes and downscales a bitmap region off the main UI thread. 208 * 209 * @param rect Rect representing the crop region in terms of the original image's resolution. 210 * @param targetWidth Width of target view in physical pixels. 211 * @param targetHeight Height of target view in physical pixels. 212 * @param isRtl 213 * @param receiver Called with the decoded bitmap region or null if there was an error decoding 214 * the bitmap region. 215 */ runDecodeBitmapRegionTask(Rect rect, int targetWidth, int targetHeight, boolean isRtl, BitmapReceiver receiver)216 public void runDecodeBitmapRegionTask(Rect rect, int targetWidth, int targetHeight, 217 boolean isRtl, BitmapReceiver receiver) { 218 sExecutorService.execute(() -> { 219 int newTargetWidth = targetWidth; 220 int newTargetHeight = targetHeight; 221 Rect cropRect = rect; 222 int exifOrientation = getExifOrientation(); 223 // Switch target height and width if image is rotated 90 or 270 degrees. 224 if (exifOrientation == ExifInterface.ORIENTATION_ROTATE_90 225 || exifOrientation == ExifInterface.ORIENTATION_ROTATE_270) { 226 int tempHeight = newTargetHeight; 227 newTargetHeight = newTargetWidth; 228 newTargetWidth = tempHeight; 229 } 230 231 // Rotate crop rect if image is rotated more than 0 degrees. 232 Point dimensions = calculateRawDimensions(); 233 cropRect = CropRectRotator.rotateCropRectForExifOrientation( 234 dimensions, cropRect, exifOrientation); 235 236 // If we're in RTL mode, center in the rightmost side of the image 237 if (isRtl) { 238 cropRect.set(dimensions.x - cropRect.right, cropRect.top, 239 dimensions.x - cropRect.left, cropRect.bottom); 240 } 241 242 BitmapFactory.Options options = new BitmapFactory.Options(); 243 options.inSampleSize = BitmapUtils.calculateInSampleSize( 244 cropRect.width(), cropRect.height(), newTargetWidth, newTargetHeight); 245 246 if (mBitmapRegionDecoder == null) { 247 mBitmapRegionDecoder = openBitmapRegionDecoder(); 248 } 249 250 // Bitmap region decoder may have failed to open if there was a problem with the 251 // underlying InputStream. 252 if (mBitmapRegionDecoder != null) { 253 try { 254 Bitmap bitmap = mBitmapRegionDecoder.decodeRegion(cropRect, options); 255 256 // Rotate output bitmap if necessary because of EXIF orientation. 257 int matrixRotation = getDegreesRotationForExifOrientation(exifOrientation); 258 if (matrixRotation > 0) { 259 Matrix rotateMatrix = new Matrix(); 260 rotateMatrix.setRotate(matrixRotation); 261 bitmap = Bitmap.createBitmap( 262 bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), rotateMatrix, 263 false); 264 } 265 decodeBitmapCompleted(receiver, bitmap); 266 return; 267 } catch (OutOfMemoryError e) { 268 Log.e(TAG, "Out of memory and unable to decode bitmap region", e); 269 } catch (IllegalArgumentException e) { 270 Log.e(TAG, "Illegal argument for decoding bitmap region", e); 271 } 272 } 273 decodeBitmapCompleted(receiver, null); 274 }); 275 } 276 277 /** 278 * Decodes the raw dimensions of the asset without allocating memory for the entire asset. Adjusts 279 * for the EXIF orientation if necessary. 280 * 281 * @return Dimensions as a Point where width is represented by "x" and height by "y". 282 */ 283 @Nullable calculateRawDimensions()284 public Point calculateRawDimensions() { 285 if (mDimensions != null) { 286 return mDimensions; 287 } 288 289 BitmapFactory.Options options = new BitmapFactory.Options(); 290 options.inJustDecodeBounds = true; 291 InputStream inputStream = openInputStream(); 292 // Input stream may be null if there was an error opening it. 293 if (inputStream == null) { 294 return null; 295 } 296 BitmapFactory.decodeStream(inputStream, null, options); 297 closeInputStream(inputStream, "There was an error closing the input stream used to calculate " 298 + "the image's raw dimensions"); 299 300 int exifOrientation = getExifOrientation(); 301 // Swap height and width if image is rotated 90 or 270 degrees. 302 if (exifOrientation == ExifInterface.ORIENTATION_ROTATE_90 303 || exifOrientation == ExifInterface.ORIENTATION_ROTATE_270) { 304 mDimensions = new Point(options.outHeight, options.outWidth); 305 } else { 306 mDimensions = new Point(options.outWidth, options.outHeight); 307 } 308 309 return mDimensions; 310 } 311 312 /** 313 * Returns a BitmapRegionDecoder for the asset. 314 */ 315 @Nullable openBitmapRegionDecoder()316 private BitmapRegionDecoder openBitmapRegionDecoder() { 317 InputStream inputStream = null; 318 BitmapRegionDecoder brd = null; 319 320 try { 321 inputStream = openInputStream(); 322 // Input stream may be null if there was an error opening it. 323 if (inputStream == null) { 324 return null; 325 } 326 brd = BitmapRegionDecoder.newInstance(inputStream, true); 327 } catch (IOException e) { 328 Log.w(TAG, "Unable to open BitmapRegionDecoder", e); 329 } finally { 330 closeInputStream(inputStream, "Unable to close input stream used to create " 331 + "BitmapRegionDecoder"); 332 } 333 334 return brd; 335 } 336 337 /** 338 * Closes the provided InputStream and if there was an error, logs the provided error message. 339 */ closeInputStream(InputStream inputStream, String errorMessage)340 private void closeInputStream(InputStream inputStream, String errorMessage) { 341 try { 342 inputStream.close(); 343 } catch (IOException e) { 344 Log.e(TAG, errorMessage); 345 } 346 } 347 348 /** 349 * Interface for receiving unmodified input streams of the underlying asset without any 350 * downscaling or other decoding options. 351 */ 352 public interface StreamReceiver { 353 354 /** 355 * Called with an opened input stream of bytes from the underlying image asset. Clients must 356 * close the input stream after it has been read. Returns null if there was an error opening the 357 * input stream. 358 */ onInputStreamOpened(@ullable InputStream inputStream)359 void onInputStreamOpened(@Nullable InputStream inputStream); 360 } 361 } 362 363 364 365