• 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, 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