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