• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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