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