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, BitmapReceiver receiver)89 public void decodeBitmapRegion(Rect rect, int targetWidth, int targetHeight, 90 BitmapReceiver receiver) { 91 runDecodeBitmapRegionTask(rect, targetWidth, targetHeight, 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 receiver Called with the decoded bitmap region or null if there was an error decoding 140 * the bitmap region. 141 * @return AsyncTask reference so that the decoding task can be canceled before it starts. 142 */ runDecodeBitmapRegionTask(Rect rect, int targetWidth, int targetHeight, BitmapReceiver receiver)143 public AsyncTask runDecodeBitmapRegionTask(Rect rect, int targetWidth, int targetHeight, 144 BitmapReceiver receiver) { 145 DecodeBitmapRegionAsyncTask task = 146 new DecodeBitmapRegionAsyncTask(rect, targetWidth, targetHeight, receiver); 147 task.execute(); 148 return task; 149 } 150 151 /** 152 * Decodes the raw dimensions of the asset without allocating memory for the entire asset. Adjusts 153 * for the EXIF orientation if necessary. 154 * 155 * @return Dimensions as a Point where width is represented by "x" and height by "y". 156 */ 157 @Nullable calculateRawDimensions()158 public Point calculateRawDimensions() { 159 if (mDimensions != null) { 160 return mDimensions; 161 } 162 163 BitmapFactory.Options options = new BitmapFactory.Options(); 164 options.inJustDecodeBounds = true; 165 InputStream inputStream = openInputStream(); 166 // Input stream may be null if there was an error opening it. 167 if (inputStream == null) { 168 return null; 169 } 170 BitmapFactory.decodeStream(inputStream, null, options); 171 closeInputStream(inputStream, "There was an error closing the input stream used to calculate " 172 + "the image's raw dimensions"); 173 174 int exifOrientation = getExifOrientation(); 175 // Swap height and width if image is rotated 90 or 270 degrees. 176 if (exifOrientation == ExifInterface.ORIENTATION_ROTATE_90 177 || exifOrientation == ExifInterface.ORIENTATION_ROTATE_270) { 178 mDimensions = new Point(options.outHeight, options.outWidth); 179 } else { 180 mDimensions = new Point(options.outWidth, options.outHeight); 181 } 182 183 return mDimensions; 184 } 185 186 /** 187 * Returns a BitmapRegionDecoder for the asset. 188 */ 189 @Nullable openBitmapRegionDecoder()190 private BitmapRegionDecoder openBitmapRegionDecoder() { 191 InputStream inputStream = null; 192 BitmapRegionDecoder brd = null; 193 194 try { 195 inputStream = openInputStream(); 196 // Input stream may be null if there was an error opening it. 197 if (inputStream == null) { 198 return null; 199 } 200 brd = BitmapRegionDecoder.newInstance(inputStream, true); 201 } catch (IOException e) { 202 Log.w(TAG, "Unable to open BitmapRegionDecoder", e); 203 } finally { 204 closeInputStream(inputStream, "Unable to close input stream used to create " 205 + "BitmapRegionDecoder"); 206 } 207 208 return brd; 209 } 210 211 /** 212 * Closes the provided InputStream and if there was an error, logs the provided error message. 213 */ closeInputStream(InputStream inputStream, String errorMessage)214 private void closeInputStream(InputStream inputStream, String errorMessage) { 215 try { 216 inputStream.close(); 217 } catch (IOException e) { 218 Log.e(TAG, errorMessage); 219 } 220 } 221 222 /** 223 * Interface for receiving unmodified input streams of the underlying asset without any 224 * downscaling or other decoding options. 225 */ 226 public interface StreamReceiver { 227 228 /** 229 * Called with an opened input stream of bytes from the underlying image asset. Clients must 230 * close the input stream after it has been read. Returns null if there was an error opening the 231 * input stream. 232 */ onInputStreamOpened(@ullable InputStream inputStream)233 void onInputStreamOpened(@Nullable InputStream inputStream); 234 } 235 236 /** 237 * AsyncTask which decodes a Bitmap off the UI thread. Scales the Bitmap for the target width and 238 * height if possible. 239 */ 240 private class DecodeBitmapAsyncTask extends AsyncTask<Void, Void, Bitmap> { 241 242 private BitmapReceiver mReceiver; 243 private int mTargetWidth; 244 private int mTargetHeight; 245 DecodeBitmapAsyncTask(int targetWidth, int targetHeight, BitmapReceiver receiver)246 public DecodeBitmapAsyncTask(int targetWidth, int targetHeight, BitmapReceiver receiver) { 247 mReceiver = receiver; 248 mTargetWidth = targetWidth; 249 mTargetHeight = targetHeight; 250 } 251 252 @Override doInBackground(Void... unused)253 protected Bitmap doInBackground(Void... unused) { 254 int exifOrientation = getExifOrientation(); 255 // Switch target height and width if image is rotated 90 or 270 degrees. 256 if (exifOrientation == ExifInterface.ORIENTATION_ROTATE_90 257 || exifOrientation == ExifInterface.ORIENTATION_ROTATE_270) { 258 int tempHeight = mTargetHeight; 259 mTargetHeight = mTargetWidth; 260 mTargetWidth = tempHeight; 261 } 262 263 InputStream inputStream = openInputStream(); 264 // Input stream may be null if there was an error opening it. 265 if (inputStream == null) { 266 return null; 267 } 268 269 BitmapFactory.Options options = new BitmapFactory.Options(); 270 271 Point rawDimensions = calculateRawDimensions(); 272 // Raw dimensions may be null if there was an error opening the underlying input stream. 273 if (rawDimensions == null) { 274 return null; 275 } 276 options.inSampleSize = BitmapUtils.calculateInSampleSize( 277 rawDimensions.x, rawDimensions.y, mTargetWidth, mTargetHeight); 278 options.inPreferredConfig = Config.HARDWARE; 279 280 Bitmap bitmap = BitmapFactory.decodeStream(inputStream, null, options); 281 closeInputStream( 282 inputStream, "Error closing the input stream used to decode the full bitmap"); 283 284 // Rotate output bitmap if necessary because of EXIF orientation tag. 285 int matrixRotation = getDegreesRotationForExifOrientation(exifOrientation); 286 if (matrixRotation > 0) { 287 Matrix rotateMatrix = new Matrix(); 288 rotateMatrix.setRotate(matrixRotation); 289 bitmap = Bitmap.createBitmap( 290 bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), rotateMatrix, false); 291 } 292 293 return bitmap; 294 } 295 296 @Override onPostExecute(Bitmap bitmap)297 protected void onPostExecute(Bitmap bitmap) { 298 mReceiver.onBitmapDecoded(bitmap); 299 } 300 } 301 302 /** 303 * AsyncTask subclass which decodes a bitmap region from the asset off the main UI thread. 304 */ 305 private class DecodeBitmapRegionAsyncTask extends AsyncTask<Void, Void, Bitmap> { 306 307 private Rect mCropRect; 308 private BitmapReceiver mReceiver; 309 private int mTargetWidth; 310 private int mTargetHeight; 311 DecodeBitmapRegionAsyncTask(Rect rect, int targetWidth, int targetHeight, BitmapReceiver receiver)312 public DecodeBitmapRegionAsyncTask(Rect rect, int targetWidth, int targetHeight, 313 BitmapReceiver receiver) { 314 mCropRect = rect; 315 mReceiver = receiver; 316 mTargetWidth = targetWidth; 317 mTargetHeight = targetHeight; 318 } 319 320 @Override doInBackground(Void... voids)321 protected Bitmap doInBackground(Void... voids) { 322 int exifOrientation = getExifOrientation(); 323 // Switch target height and width if image is rotated 90 or 270 degrees. 324 if (exifOrientation == ExifInterface.ORIENTATION_ROTATE_90 325 || exifOrientation == ExifInterface.ORIENTATION_ROTATE_270) { 326 int tempHeight = mTargetHeight; 327 mTargetHeight = mTargetWidth; 328 mTargetWidth = tempHeight; 329 } 330 331 // Rotate crop rect if image is rotated more than 0 degrees. 332 mCropRect = CropRectRotator.rotateCropRectForExifOrientation( 333 calculateRawDimensions(), mCropRect, exifOrientation); 334 335 BitmapFactory.Options options = new BitmapFactory.Options(); 336 options.inSampleSize = BitmapUtils.calculateInSampleSize( 337 mCropRect.width(), mCropRect.height(), mTargetWidth, mTargetHeight); 338 339 if (mBitmapRegionDecoder == null) { 340 mBitmapRegionDecoder = openBitmapRegionDecoder(); 341 } 342 343 // Bitmap region decoder may have failed to open if there was a problem with the underlying 344 // InputStream. 345 if (mBitmapRegionDecoder != null) { 346 try { 347 Bitmap bitmap = mBitmapRegionDecoder.decodeRegion(mCropRect, options); 348 349 // Rotate output bitmap if necessary because of EXIF orientation. 350 int matrixRotation = getDegreesRotationForExifOrientation(exifOrientation); 351 if (matrixRotation > 0) { 352 Matrix rotateMatrix = new Matrix(); 353 rotateMatrix.setRotate(matrixRotation); 354 bitmap = Bitmap.createBitmap( 355 bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), rotateMatrix, false); 356 } 357 358 return bitmap; 359 360 } catch (OutOfMemoryError e) { 361 Log.e(TAG, "Out of memory and unable to decode bitmap region", e); 362 return null; 363 } 364 } 365 366 return null; 367 } 368 369 @Override onPostExecute(Bitmap bitmap)370 protected void onPostExecute(Bitmap bitmap) { 371 mReceiver.onBitmapDecoded(bitmap); 372 } 373 } 374 375 /** 376 * AsyncTask subclass which decodes the raw dimensions of the asset off the main UI thread. Avoids 377 * allocating memory for the fully decoded image. 378 */ 379 private class DecodeDimensionsAsyncTask extends AsyncTask<Void, Void, Point> { 380 private DimensionsReceiver mReceiver; 381 DecodeDimensionsAsyncTask(DimensionsReceiver receiver)382 public DecodeDimensionsAsyncTask(DimensionsReceiver receiver) { 383 mReceiver = receiver; 384 } 385 386 @Override doInBackground(Void... unused)387 protected Point doInBackground(Void... unused) { 388 return calculateRawDimensions(); 389 } 390 391 @Override onPostExecute(Point dimensions)392 protected void onPostExecute(Point dimensions) { 393 mReceiver.onDimensionsDecoded(dimensions); 394 } 395 } 396 } 397 398 399 400