/* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.wallpaper.asset; import android.app.Activity; import android.graphics.Bitmap; import android.graphics.Bitmap.Config; import android.graphics.BitmapFactory; import android.graphics.BitmapRegionDecoder; import android.graphics.Matrix; import android.graphics.Point; import android.graphics.Rect; import android.media.ExifInterface; import android.os.Handler; import android.os.Looper; import android.util.Log; import androidx.annotation.Nullable; import java.io.IOException; import java.io.InputStream; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * Represents Asset types for which bytes can be read directly, allowing for flexible bitmap * decoding. */ public abstract class StreamableAsset extends Asset { private static final ExecutorService sExecutorService = Executors.newCachedThreadPool(); private static final String TAG = "StreamableAsset"; private BitmapRegionDecoder mBitmapRegionDecoder; private Point mDimensions; /** * Scales and returns a new Rect from the given Rect by the given scaling factor. */ public static Rect scaleRect(Rect rect, float scale) { return new Rect( Math.round((float) rect.left * scale), Math.round((float) rect.top * scale), Math.round((float) rect.right * scale), Math.round((float) rect.bottom * scale)); } /** * Maps from EXIF orientation tag values to counterclockwise degree rotation values. */ private static int getDegreesRotationForExifOrientation(int exifOrientation) { switch (exifOrientation) { case ExifInterface.ORIENTATION_NORMAL: return 0; case ExifInterface.ORIENTATION_ROTATE_90: return 90; case ExifInterface.ORIENTATION_ROTATE_180: return 180; case ExifInterface.ORIENTATION_ROTATE_270: return 270; default: Log.w(TAG, "Unsupported EXIF orientation " + exifOrientation); return 0; } } @Override public void decodeBitmap(int targetWidth, int targetHeight, boolean useHardwareBitmapIfPossible, BitmapReceiver receiver) { sExecutorService.execute(() -> { int newTargetWidth = targetWidth; int newTargetHeight = targetHeight; int exifOrientation = getExifOrientation(); // Switch target height and width if image is rotated 90 or 270 degrees. if (exifOrientation == ExifInterface.ORIENTATION_ROTATE_90 || exifOrientation == ExifInterface.ORIENTATION_ROTATE_270) { int tempHeight = newTargetHeight; newTargetHeight = newTargetWidth; newTargetWidth = tempHeight; } BitmapFactory.Options options = new BitmapFactory.Options(); Point rawDimensions = calculateRawDimensions(); // Raw dimensions may be null if there was an error opening the underlying input stream. if (rawDimensions == null) { decodeBitmapCompleted(receiver, null); return; } options.inSampleSize = BitmapUtils.calculateInSampleSize( rawDimensions.x, rawDimensions.y, newTargetWidth, newTargetHeight); if (useHardwareBitmapIfPossible) { options.inPreferredConfig = Config.HARDWARE; } InputStream inputStream = openInputStream(); Bitmap bitmap = null; if (inputStream != null) { bitmap = BitmapFactory.decodeStream(inputStream, null, options); closeInputStream( inputStream, "Error closing the input stream used " + "to decode the full bitmap"); // Rotate output bitmap if necessary because of EXIF orientation tag. int matrixRotation = getDegreesRotationForExifOrientation(exifOrientation); if (matrixRotation > 0) { Matrix rotateMatrix = new Matrix(); rotateMatrix.setRotate(matrixRotation); bitmap = Bitmap.createBitmap( bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), rotateMatrix, false); } } decodeBitmapCompleted(receiver, bitmap); }); } @Override public void decodeBitmap(BitmapReceiver receiver) { sExecutorService.execute(() -> { BitmapFactory.Options options = new BitmapFactory.Options(); options.inPreferredConfig = Config.HARDWARE; InputStream inputStream = openInputStream(); Bitmap bitmap = null; if (inputStream != null) { bitmap = BitmapFactory.decodeStream(inputStream, null, options); closeInputStream(inputStream, "Error closing the input stream used to decode the full bitmap"); // Rotate output bitmap if necessary because of EXIF orientation tag. int exifOrientation = getExifOrientation(); int matrixRotation = getDegreesRotationForExifOrientation(exifOrientation); if (matrixRotation > 0) { Matrix rotateMatrix = new Matrix(); rotateMatrix.setRotate(matrixRotation); bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), rotateMatrix, false); } } decodeBitmapCompleted(receiver, bitmap); }); } @Override public void decodeRawDimensions(Activity unused, DimensionsReceiver receiver) { sExecutorService.execute(() -> { Point result = calculateRawDimensions(); new Handler(Looper.getMainLooper()).post(() -> { receiver.onDimensionsDecoded(result); }); }); } @Override public void decodeBitmapRegion(Rect rect, int targetWidth, int targetHeight, boolean shouldAdjustForRtl, BitmapReceiver receiver) { runDecodeBitmapRegionTask(rect, targetWidth, targetHeight, shouldAdjustForRtl, receiver); } @Override public boolean supportsTiling() { return true; } /** * Fetches an input stream of bytes for the wallpaper image asset and provides the stream * asynchronously back to a {@link StreamReceiver}. */ public void fetchInputStream(final StreamReceiver streamReceiver) { sExecutorService.execute(() -> { InputStream result = openInputStream(); new Handler(Looper.getMainLooper()).post(() -> { streamReceiver.onInputStreamOpened(result); }); }); } /** * Returns an InputStream representing the asset. Should only be called off the main UI thread. */ @Nullable protected abstract InputStream openInputStream(); /** * Gets the EXIF orientation value of the asset. This method should only be called off the main UI * thread. */ public int getExifOrientation() { // By default, assume that the EXIF orientation is normal (i.e., bitmap is rotated 0 degrees // from how it should be rendered to a viewer). return ExifInterface.ORIENTATION_NORMAL; } /** * Decodes and downscales a bitmap region off the main UI thread. * * @param rect Rect representing the crop region in terms of the original image's resolution. * @param targetWidth Width of target view in physical pixels. * @param targetHeight Height of target view in physical pixels. * @param isRtl * @param receiver Called with the decoded bitmap region or null if there was an error decoding * the bitmap region. */ public void runDecodeBitmapRegionTask(Rect rect, int targetWidth, int targetHeight, boolean isRtl, BitmapReceiver receiver) { sExecutorService.execute(() -> { int newTargetWidth = targetWidth; int newTargetHeight = targetHeight; Rect cropRect = rect; int exifOrientation = getExifOrientation(); // Switch target height and width if image is rotated 90 or 270 degrees. if (exifOrientation == ExifInterface.ORIENTATION_ROTATE_90 || exifOrientation == ExifInterface.ORIENTATION_ROTATE_270) { int tempHeight = newTargetHeight; newTargetHeight = newTargetWidth; newTargetWidth = tempHeight; } // Rotate crop rect if image is rotated more than 0 degrees. Point dimensions = calculateRawDimensions(); cropRect = CropRectRotator.rotateCropRectForExifOrientation( dimensions, cropRect, exifOrientation); // If we're in RTL mode, center in the rightmost side of the image if (isRtl) { cropRect.set(dimensions.x - cropRect.right, cropRect.top, dimensions.x - cropRect.left, cropRect.bottom); } BitmapFactory.Options options = new BitmapFactory.Options(); options.inSampleSize = BitmapUtils.calculateInSampleSize( cropRect.width(), cropRect.height(), newTargetWidth, newTargetHeight); if (mBitmapRegionDecoder == null) { mBitmapRegionDecoder = openBitmapRegionDecoder(); } // Bitmap region decoder may have failed to open if there was a problem with the // underlying InputStream. if (mBitmapRegionDecoder != null) { try { Bitmap bitmap = mBitmapRegionDecoder.decodeRegion(cropRect, options); // Rotate output bitmap if necessary because of EXIF orientation. int matrixRotation = getDegreesRotationForExifOrientation(exifOrientation); if (matrixRotation > 0) { Matrix rotateMatrix = new Matrix(); rotateMatrix.setRotate(matrixRotation); bitmap = Bitmap.createBitmap( bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), rotateMatrix, false); } decodeBitmapCompleted(receiver, bitmap); return; } catch (OutOfMemoryError e) { Log.e(TAG, "Out of memory and unable to decode bitmap region", e); } catch (IllegalArgumentException e) { Log.e(TAG, "Illegal argument for decoding bitmap region", e); } } decodeBitmapCompleted(receiver, null); }); } /** * Decodes the raw dimensions of the asset without allocating memory for the entire asset. Adjusts * for the EXIF orientation if necessary. * * @return Dimensions as a Point where width is represented by "x" and height by "y". */ @Nullable public Point calculateRawDimensions() { if (mDimensions != null) { return mDimensions; } BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; InputStream inputStream = openInputStream(); // Input stream may be null if there was an error opening it. if (inputStream == null) { return null; } BitmapFactory.decodeStream(inputStream, null, options); closeInputStream(inputStream, "There was an error closing the input stream used to calculate " + "the image's raw dimensions"); int exifOrientation = getExifOrientation(); // Swap height and width if image is rotated 90 or 270 degrees. if (exifOrientation == ExifInterface.ORIENTATION_ROTATE_90 || exifOrientation == ExifInterface.ORIENTATION_ROTATE_270) { mDimensions = new Point(options.outHeight, options.outWidth); } else { mDimensions = new Point(options.outWidth, options.outHeight); } return mDimensions; } /** * Returns a BitmapRegionDecoder for the asset. */ @Nullable private BitmapRegionDecoder openBitmapRegionDecoder() { InputStream inputStream = null; BitmapRegionDecoder brd = null; try { inputStream = openInputStream(); // Input stream may be null if there was an error opening it. if (inputStream == null) { return null; } brd = BitmapRegionDecoder.newInstance(inputStream, true); } catch (IOException e) { Log.w(TAG, "Unable to open BitmapRegionDecoder", e); } finally { closeInputStream(inputStream, "Unable to close input stream used to create " + "BitmapRegionDecoder"); } return brd; } /** * Closes the provided InputStream and if there was an error, logs the provided error message. */ private void closeInputStream(InputStream inputStream, String errorMessage) { try { inputStream.close(); } catch (IOException e) { Log.e(TAG, errorMessage); } } /** * Interface for receiving unmodified input streams of the underlying asset without any * downscaling or other decoding options. */ public interface StreamReceiver { /** * Called with an opened input stream of bytes from the underlying image asset. Clients must * close the input stream after it has been read. Returns null if there was an error opening the * input stream. */ void onInputStreamOpened(@Nullable InputStream inputStream); } }