blob: e8e2c48ffc50965fd2e316f4c9e924a4536ff24f [file] [log] [blame]
Jon Miranda16ea1b12017-12-12 14:52:48 -08001/*
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 */
16package com.android.wallpaper.asset;
17
18import android.app.Activity;
19import android.graphics.Bitmap;
20import android.graphics.BitmapFactory;
21import android.graphics.BitmapRegionDecoder;
22import android.graphics.Matrix;
23import android.graphics.Point;
24import android.graphics.Rect;
25import android.media.ExifInterface;
26import android.os.AsyncTask;
27import android.support.annotation.Nullable;
28import android.util.Log;
29
30import java.io.IOException;
31import java.io.InputStream;
Jon Miranda16ea1b12017-12-12 14:52:48 -080032
33/**
34 * Represents Asset types for which bytes can be read directly, allowing for flexible bitmap
35 * decoding.
36 */
37public abstract class StreamableAsset extends Asset {
38 private static final String TAG = "StreamableAsset";
39
40 private BitmapRegionDecoder mBitmapRegionDecoder;
41 private Point mDimensions;
42
43 /**
44 * Scales and returns a new Rect from the given Rect by the given scaling factor.
45 */
46 public static Rect scaleRect(Rect rect, float scale) {
47 return new Rect(
48 Math.round((float) rect.left * scale),
49 Math.round((float) rect.top * scale),
50 Math.round((float) rect.right * scale),
51 Math.round((float) rect.bottom * scale));
52 }
53
54 /**
55 * Maps from EXIF orientation tag values to counterclockwise degree rotation values.
56 */
57 private static int getDegreesRotationForExifOrientation(int exifOrientation) {
58 switch (exifOrientation) {
59 case ExifInterface.ORIENTATION_NORMAL:
60 return 0;
61 case ExifInterface.ORIENTATION_ROTATE_90:
62 return 90;
63 case ExifInterface.ORIENTATION_ROTATE_180:
64 return 180;
65 case ExifInterface.ORIENTATION_ROTATE_270:
66 return 270;
67 default:
68 Log.w(TAG, "Unsupported EXIF orientation " + exifOrientation);
69 return 0;
70 }
71 }
72
73 @Override
74 public void decodeBitmap(int targetWidth, int targetHeight,
75 BitmapReceiver receiver) {
76 DecodeBitmapAsyncTask task = new DecodeBitmapAsyncTask(targetWidth, targetHeight, receiver);
77 task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
78 }
79
80 @Override
81 public void decodeRawDimensions(Activity unused, DimensionsReceiver receiver) {
82 DecodeDimensionsAsyncTask task = new DecodeDimensionsAsyncTask(receiver);
83 task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
84 }
85
86 @Override
87 public void decodeBitmapRegion(Rect rect, int targetWidth, int targetHeight,
88 BitmapReceiver receiver) {
89 runDecodeBitmapRegionTask(rect, targetWidth, targetHeight, receiver);
90 }
91
92 @Override
93 public boolean supportsTiling() {
94 return true;
95 }
96
97 /**
98 * Fetches an input stream of bytes for the wallpaper image asset and provides the stream
99 * asynchronously back to a {@link StreamReceiver}.
100 */
101 public void fetchInputStream(final StreamReceiver streamReceiver) {
102 new AsyncTask<Void, Void, InputStream>() {
103 @Override
104 protected InputStream doInBackground(Void... params) {
105 return openInputStream();
106 }
107
108 @Override
109 protected void onPostExecute(InputStream inputStream) {
110 streamReceiver.onInputStreamOpened(inputStream);
111 }
112 }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
113 }
114
115 /**
116 * Returns an InputStream representing the asset. Should only be called off the main UI thread.
117 */
118 @Nullable
119 protected abstract InputStream openInputStream();
120
121 /**
122 * Gets the EXIF orientation value of the asset. This method should only be called off the main UI
123 * thread.
124 */
125 protected int getExifOrientation() {
126 // By default, assume that the EXIF orientation is normal (i.e., bitmap is rotated 0 degrees
127 // from how it should be rendered to a viewer).
128 return ExifInterface.ORIENTATION_NORMAL;
129 }
130
131 /**
132 * Decodes and downscales a bitmap region off the main UI thread.
133 *
134 * @param rect Rect representing the crop region in terms of the original image's resolution.
135 * @param targetWidth Width of target view in physical pixels.
136 * @param targetHeight Height of target view in physical pixels.
137 * @param receiver Called with the decoded bitmap region or null if there was an error decoding
138 * the bitmap region.
139 * @return AsyncTask reference so that the decoding task can be canceled before it starts.
140 */
141 public AsyncTask runDecodeBitmapRegionTask(Rect rect, int targetWidth, int targetHeight,
142 BitmapReceiver receiver) {
143 DecodeBitmapRegionAsyncTask task =
144 new DecodeBitmapRegionAsyncTask(rect, targetWidth, targetHeight, receiver);
145 task.execute();
146 return task;
147 }
148
149 /**
150 * Decodes the raw dimensions of the asset without allocating memory for the entire asset. Adjusts
151 * for the EXIF orientation if necessary.
152 *
153 * @return Dimensions as a Point where width is represented by "x" and height by "y".
154 */
155 @Nullable
156 public Point calculateRawDimensions() {
157 if (mDimensions != null) {
158 return mDimensions;
159 }
160
161 BitmapFactory.Options options = new BitmapFactory.Options();
162 options.inJustDecodeBounds = true;
163 InputStream inputStream = openInputStream();
164 // Input stream may be null if there was an error opening it.
165 if (inputStream == null) {
166 return null;
167 }
168 BitmapFactory.decodeStream(inputStream, null, options);
169 closeInputStream(inputStream, "There was an error closing the input stream used to calculate "
170 + "the image's raw dimensions");
171
172 int exifOrientation = getExifOrientation();
173 // Swap height and width if image is rotated 90 or 270 degrees.
174 if (exifOrientation == ExifInterface.ORIENTATION_ROTATE_90
175 || exifOrientation == ExifInterface.ORIENTATION_ROTATE_270) {
176 mDimensions = new Point(options.outHeight, options.outWidth);
177 } else {
178 mDimensions = new Point(options.outWidth, options.outHeight);
179 }
180
181 return mDimensions;
182 }
183
184 /**
185 * Returns a BitmapRegionDecoder for the asset.
186 */
187 @Nullable
188 private BitmapRegionDecoder openBitmapRegionDecoder() {
189 InputStream inputStream = null;
190 BitmapRegionDecoder brd = null;
191
192 try {
193 inputStream = openInputStream();
194 // Input stream may be null if there was an error opening it.
195 if (inputStream == null) {
196 return null;
197 }
198 brd = BitmapRegionDecoder.newInstance(inputStream, true);
199 } catch (IOException e) {
200 Log.w(TAG, "Unable to open BitmapRegionDecoder", e);
201 } finally {
202 closeInputStream(inputStream, "Unable to close input stream used to create "
203 + "BitmapRegionDecoder");
204 }
205
206 return brd;
207 }
208
209 /**
210 * Closes the provided InputStream and if there was an error, logs the provided error message.
211 */
212 private void closeInputStream(InputStream inputStream, String errorMessage) {
213 try {
214 inputStream.close();
215 } catch (IOException e) {
216 Log.e(TAG, errorMessage);
217 }
218 }
219
220 /**
221 * Interface for receiving unmodified input streams of the underlying asset without any
222 * downscaling or other decoding options.
223 */
224 public interface StreamReceiver {
225
226 /**
227 * Called with an opened input stream of bytes from the underlying image asset. Clients must
228 * close the input stream after it has been read. Returns null if there was an error opening the
229 * input stream.
230 */
231 void onInputStreamOpened(@Nullable InputStream inputStream);
232 }
233
234 /**
235 * AsyncTask which decodes a Bitmap off the UI thread. Scales the Bitmap for the target width and
236 * height if possible.
237 */
238 private class DecodeBitmapAsyncTask extends AsyncTask<Void, Void, Bitmap> {
239
240 private BitmapReceiver mReceiver;
241 private int mTargetWidth;
242 private int mTargetHeight;
243
244 public DecodeBitmapAsyncTask(int targetWidth, int targetHeight, BitmapReceiver receiver) {
245 mReceiver = receiver;
246 mTargetWidth = targetWidth;
247 mTargetHeight = targetHeight;
248 }
249
250 @Override
251 protected Bitmap doInBackground(Void... unused) {
252 int exifOrientation = getExifOrientation();
253 // Switch target height and width if image is rotated 90 or 270 degrees.
254 if (exifOrientation == ExifInterface.ORIENTATION_ROTATE_90
255 || exifOrientation == ExifInterface.ORIENTATION_ROTATE_270) {
256 int tempHeight = mTargetHeight;
257 mTargetHeight = mTargetWidth;
258 mTargetWidth = tempHeight;
259 }
260
261 InputStream inputStream = openInputStream();
262 // Input stream may be null if there was an error opening it.
263 if (inputStream == null) {
264 return null;
265 }
266
267 BitmapFactory.Options options = new BitmapFactory.Options();
268
269 Point rawDimensions = calculateRawDimensions();
270 // Raw dimensions may be null if there was an error opening the underlying input stream.
271 if (rawDimensions == null) {
272 return null;
273 }
274 options.inSampleSize = BitmapUtils.calculateInSampleSize(
275 rawDimensions.x, rawDimensions.y, mTargetWidth, mTargetHeight);
276
277 Bitmap bitmap = BitmapFactory.decodeStream(inputStream, null, options);
278 closeInputStream(
279 inputStream, "Error closing the input stream used to decode the full bitmap");
280
281 // Rotate output bitmap if necessary because of EXIF orientation tag.
282 int matrixRotation = getDegreesRotationForExifOrientation(exifOrientation);
283 if (matrixRotation > 0) {
284 Matrix rotateMatrix = new Matrix();
285 rotateMatrix.setRotate(matrixRotation);
286 bitmap = Bitmap.createBitmap(
287 bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), rotateMatrix, false);
288 }
289
290 return bitmap;
291 }
292
293 @Override
294 protected void onPostExecute(Bitmap bitmap) {
295 mReceiver.onBitmapDecoded(bitmap);
296 }
297 }
298
299 /**
300 * AsyncTask subclass which decodes a bitmap region from the asset off the main UI thread.
301 */
302 private class DecodeBitmapRegionAsyncTask extends AsyncTask<Void, Void, Bitmap> {
303
304 private Rect mCropRect;
305 private BitmapReceiver mReceiver;
306 private int mTargetWidth;
307 private int mTargetHeight;
308
309 public DecodeBitmapRegionAsyncTask(Rect rect, int targetWidth, int targetHeight,
310 BitmapReceiver receiver) {
311 mCropRect = rect;
312 mReceiver = receiver;
313 mTargetWidth = targetWidth;
314 mTargetHeight = targetHeight;
315 }
316
317 @Override
318 protected Bitmap doInBackground(Void... voids) {
319 int exifOrientation = getExifOrientation();
320 // Switch target height and width if image is rotated 90 or 270 degrees.
321 if (exifOrientation == ExifInterface.ORIENTATION_ROTATE_90
322 || exifOrientation == ExifInterface.ORIENTATION_ROTATE_270) {
323 int tempHeight = mTargetHeight;
324 mTargetHeight = mTargetWidth;
325 mTargetWidth = tempHeight;
326 }
327
328 // Rotate crop rect if image is rotated more than 0 degrees.
329 mCropRect = CropRectRotator.rotateCropRectForExifOrientation(
330 calculateRawDimensions(), mCropRect, exifOrientation);
331
332 BitmapFactory.Options options = new BitmapFactory.Options();
333 options.inSampleSize = BitmapUtils.calculateInSampleSize(
334 mCropRect.width(), mCropRect.height(), mTargetWidth, mTargetHeight);
335
336 if (mBitmapRegionDecoder == null) {
337 mBitmapRegionDecoder = openBitmapRegionDecoder();
338 }
339
340 // Bitmap region decoder may have failed to open if there was a problem with the underlying
341 // InputStream.
342 if (mBitmapRegionDecoder != null) {
343 try {
344 Bitmap bitmap = mBitmapRegionDecoder.decodeRegion(mCropRect, options);
345
346 // Rotate output bitmap if necessary because of EXIF orientation.
347 int matrixRotation = getDegreesRotationForExifOrientation(exifOrientation);
348 if (matrixRotation > 0) {
349 Matrix rotateMatrix = new Matrix();
350 rotateMatrix.setRotate(matrixRotation);
351 bitmap = Bitmap.createBitmap(
352 bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), rotateMatrix, false);
353 }
354
355 return bitmap;
356
357 } catch (OutOfMemoryError e) {
358 Log.e(TAG, "Out of memory and unable to decode bitmap region", e);
359 return null;
360 }
361 }
362
363 return null;
364 }
365
366 @Override
367 protected void onPostExecute(Bitmap bitmap) {
368 mReceiver.onBitmapDecoded(bitmap);
369 }
370 }
371
372 /**
373 * AsyncTask subclass which decodes the raw dimensions of the asset off the main UI thread. Avoids
374 * allocating memory for the fully decoded image.
375 */
376 private class DecodeDimensionsAsyncTask extends AsyncTask<Void, Void, Point> {
377 private DimensionsReceiver mReceiver;
378
379 public DecodeDimensionsAsyncTask(DimensionsReceiver receiver) {
380 mReceiver = receiver;
381 }
382
383 @Override
384 protected Point doInBackground(Void... unused) {
385 return calculateRawDimensions();
386 }
387
388 @Override
389 protected void onPostExecute(Point dimensions) {
390 mReceiver.onDimensionsDecoded(dimensions);
391 }
392 }
393}
394
395
396