1 /**
2  * Copyright (C) 2015 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.gallery3d.common;
17 
18 import android.app.WallpaperManager;
19 import android.content.Context;
20 import android.content.res.Resources;
21 import android.graphics.Bitmap;
22 import android.graphics.Bitmap.CompressFormat;
23 import android.graphics.BitmapFactory;
24 import android.graphics.BitmapRegionDecoder;
25 import android.graphics.Canvas;
26 import android.graphics.Matrix;
27 import android.graphics.Paint;
28 import android.graphics.Point;
29 import android.graphics.Rect;
30 import android.graphics.RectF;
31 import android.net.Uri;
32 import android.os.AsyncTask;
33 import android.util.Log;
34 import android.widget.Toast;
35 
36 import com.android.launcher3.NycWallpaperUtils;
37 import com.android.launcher3.R;
38 import com.android.launcher3.Utilities;
39 
40 import java.io.BufferedInputStream;
41 import java.io.ByteArrayInputStream;
42 import java.io.ByteArrayOutputStream;
43 import java.io.FileNotFoundException;
44 import java.io.IOException;
45 import java.io.InputStream;
46 
47 public class BitmapCropTask extends AsyncTask<Integer, Void, Boolean> {
48 
49     public interface OnBitmapCroppedHandler {
onBitmapCropped(byte[] imageBytes, Rect cropHint)50         public void onBitmapCropped(byte[] imageBytes, Rect cropHint);
51     }
52 
53     public interface OnEndCropHandler {
run(boolean cropSucceeded)54         public void run(boolean cropSucceeded);
55     }
56 
57     private static final int DEFAULT_COMPRESS_QUALITY = 90;
58     private static final String LOGTAG = "BitmapCropTask";
59 
60     Uri mInUri = null;
61     Context mContext;
62     String mInFilePath;
63     byte[] mInImageBytes;
64     int mInResId = 0;
65     RectF mCropBounds = null;
66     int mOutWidth, mOutHeight;
67     int mRotation;
68     boolean mSetWallpaper;
69     boolean mSaveCroppedBitmap;
70     Bitmap mCroppedBitmap;
71     BitmapCropTask.OnEndCropHandler mOnEndCropHandler;
72     Resources mResources;
73     BitmapCropTask.OnBitmapCroppedHandler mOnBitmapCroppedHandler;
74     boolean mNoCrop;
75 
BitmapCropTask(byte[] imageBytes, RectF cropBounds, int rotation, int outWidth, int outHeight, boolean setWallpaper, boolean saveCroppedBitmap, OnEndCropHandler onEndCropHandler)76     public BitmapCropTask(byte[] imageBytes,
77             RectF cropBounds, int rotation, int outWidth, int outHeight,
78             boolean setWallpaper, boolean saveCroppedBitmap, OnEndCropHandler onEndCropHandler) {
79         mInImageBytes = imageBytes;
80         init(cropBounds, rotation,
81                 outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndCropHandler);
82     }
83 
BitmapCropTask(Context c, Uri inUri, RectF cropBounds, int rotation, int outWidth, int outHeight, boolean setWallpaper, boolean saveCroppedBitmap, OnEndCropHandler onEndCropHandler)84     public BitmapCropTask(Context c, Uri inUri,
85             RectF cropBounds, int rotation, int outWidth, int outHeight,
86             boolean setWallpaper, boolean saveCroppedBitmap, OnEndCropHandler onEndCropHandler) {
87         mContext = c;
88         mInUri = inUri;
89         init(cropBounds, rotation,
90                 outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndCropHandler);
91     }
92 
BitmapCropTask(Context c, Resources res, int inResId, RectF cropBounds, int rotation, int outWidth, int outHeight, boolean setWallpaper, boolean saveCroppedBitmap, OnEndCropHandler onEndCropHandler)93     public BitmapCropTask(Context c, Resources res, int inResId,
94             RectF cropBounds, int rotation, int outWidth, int outHeight,
95             boolean setWallpaper, boolean saveCroppedBitmap, OnEndCropHandler onEndCropHandler) {
96         mContext = c;
97         mInResId = inResId;
98         mResources = res;
99         init(cropBounds, rotation,
100                 outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndCropHandler);
101     }
102 
init(RectF cropBounds, int rotation, int outWidth, int outHeight, boolean setWallpaper, boolean saveCroppedBitmap, OnEndCropHandler onEndCropHandler)103     private void init(RectF cropBounds, int rotation, int outWidth, int outHeight,
104             boolean setWallpaper, boolean saveCroppedBitmap, OnEndCropHandler onEndCropHandler) {
105         mCropBounds = cropBounds;
106         mRotation = rotation;
107         mOutWidth = outWidth;
108         mOutHeight = outHeight;
109         mSetWallpaper = setWallpaper;
110         mSaveCroppedBitmap = saveCroppedBitmap;
111         mOnEndCropHandler = onEndCropHandler;
112     }
113 
setOnBitmapCropped(BitmapCropTask.OnBitmapCroppedHandler handler)114     public void setOnBitmapCropped(BitmapCropTask.OnBitmapCroppedHandler handler) {
115         mOnBitmapCroppedHandler = handler;
116     }
117 
setNoCrop(boolean value)118     public void setNoCrop(boolean value) {
119         mNoCrop = value;
120     }
121 
setOnEndRunnable(OnEndCropHandler onEndCropHandler)122     public void setOnEndRunnable(OnEndCropHandler onEndCropHandler) {
123         mOnEndCropHandler = onEndCropHandler;
124     }
125 
126     // Helper to setup input stream
regenerateInputStream()127     private InputStream regenerateInputStream() {
128         if (mInUri == null && mInResId == 0 && mInFilePath == null && mInImageBytes == null) {
129             Log.w(LOGTAG, "cannot read original file, no input URI, resource ID, or " +
130                     "image byte array given");
131         } else {
132             try {
133                 if (mInUri != null) {
134                     return new BufferedInputStream(
135                             mContext.getContentResolver().openInputStream(mInUri));
136                 } else if (mInFilePath != null) {
137                     return mContext.openFileInput(mInFilePath);
138                 } else if (mInImageBytes != null) {
139                     return new BufferedInputStream(new ByteArrayInputStream(mInImageBytes));
140                 } else {
141                     return new BufferedInputStream(mResources.openRawResource(mInResId));
142                 }
143             } catch (FileNotFoundException e) {
144                 Log.w(LOGTAG, "cannot read file: " + mInUri.toString(), e);
145             }
146         }
147         return null;
148     }
149 
getImageBounds()150     public Point getImageBounds() {
151         InputStream is = regenerateInputStream();
152         if (is != null) {
153             BitmapFactory.Options options = new BitmapFactory.Options();
154             options.inJustDecodeBounds = true;
155             BitmapFactory.decodeStream(is, null, options);
156             Utils.closeSilently(is);
157             if (options.outWidth != 0 && options.outHeight != 0) {
158                 return new Point(options.outWidth, options.outHeight);
159             }
160         }
161         return null;
162     }
163 
setCropBounds(RectF cropBounds)164     public void setCropBounds(RectF cropBounds) {
165         mCropBounds = cropBounds;
166     }
167 
getCroppedBitmap()168     public Bitmap getCroppedBitmap() {
169         return mCroppedBitmap;
170     }
cropBitmap(int whichWallpaper)171     public boolean cropBitmap(int whichWallpaper) {
172         boolean failure = false;
173 
174         if (mSetWallpaper && mNoCrop) {
175             try {
176                 InputStream is = regenerateInputStream();
177                 setWallpaper(is, null, whichWallpaper);
178                 Utils.closeSilently(is);
179             } catch (IOException e) {
180                 Log.w(LOGTAG, "cannot write stream to wallpaper", e);
181                 failure = true;
182             }
183             return !failure;
184         } else if (mSetWallpaper && Utilities.ATLEAST_N
185                 && mRotation == 0 && mOutWidth > 0 && mOutHeight > 0) {
186             Rect hint = new Rect();
187             mCropBounds.roundOut(hint);
188 
189             InputStream is = null;
190             try {
191                 is = regenerateInputStream();
192                 if (is == null) {
193                     Log.w(LOGTAG, "cannot get input stream for uri=" + mInUri.toString());
194                     failure = true;
195                     return false;
196                 }
197                 WallpaperManager.getInstance(mContext).suggestDesiredDimensions(mOutWidth, mOutHeight);
198                 setWallpaper(is, hint, whichWallpaper);
199 
200                 if (mOnBitmapCroppedHandler != null) {
201                     mOnBitmapCroppedHandler.onBitmapCropped(null, hint);
202                 }
203 
204                 failure = false;
205             } catch (IOException e) {
206                 Log.w(LOGTAG, "cannot open region decoder for file: " + mInUri.toString(), e);
207             } finally {
208                 Utils.closeSilently(is);
209             }
210         } else {
211             // Find crop bounds (scaled to original image size)
212             Rect roundedTrueCrop = new Rect();
213             Matrix rotateMatrix = new Matrix();
214             Matrix inverseRotateMatrix = new Matrix();
215 
216             Point bounds = getImageBounds();
217             if (mRotation > 0) {
218                 rotateMatrix.setRotate(mRotation);
219                 inverseRotateMatrix.setRotate(-mRotation);
220 
221                 mCropBounds.roundOut(roundedTrueCrop);
222                 mCropBounds = new RectF(roundedTrueCrop);
223 
224                 if (bounds == null) {
225                     Log.w(LOGTAG, "cannot get bounds for image");
226                     failure = true;
227                     return false;
228                 }
229 
230                 float[] rotatedBounds = new float[] { bounds.x, bounds.y };
231                 rotateMatrix.mapPoints(rotatedBounds);
232                 rotatedBounds[0] = Math.abs(rotatedBounds[0]);
233                 rotatedBounds[1] = Math.abs(rotatedBounds[1]);
234 
235                 mCropBounds.offset(-rotatedBounds[0]/2, -rotatedBounds[1]/2);
236                 inverseRotateMatrix.mapRect(mCropBounds);
237                 mCropBounds.offset(bounds.x/2, bounds.y/2);
238             }
239 
240             mCropBounds.roundOut(roundedTrueCrop);
241 
242             if (roundedTrueCrop.width() <= 0 || roundedTrueCrop.height() <= 0) {
243                 Log.w(LOGTAG, "crop has bad values for full size image");
244                 failure = true;
245                 return false;
246             }
247 
248             // See how much we're reducing the size of the image
249             int scaleDownSampleSize = Math.max(1, Math.min(roundedTrueCrop.width() / mOutWidth,
250                     roundedTrueCrop.height() / mOutHeight));
251             // Attempt to open a region decoder
252             BitmapRegionDecoder decoder = null;
253             InputStream is = null;
254             try {
255                 is = regenerateInputStream();
256                 if (is == null) {
257                     Log.w(LOGTAG, "cannot get input stream for uri=" + mInUri.toString());
258                     failure = true;
259                     return false;
260                 }
261                 decoder = BitmapRegionDecoder.newInstance(is, false);
262                 Utils.closeSilently(is);
263             } catch (IOException e) {
264                 Log.w(LOGTAG, "cannot open region decoder for file: " + mInUri.toString(), e);
265             } finally {
266                Utils.closeSilently(is);
267                is = null;
268             }
269 
270             Bitmap crop = null;
271             if (decoder != null) {
272                 // Do region decoding to get crop bitmap
273                 BitmapFactory.Options options = new BitmapFactory.Options();
274                 if (scaleDownSampleSize > 1) {
275                     options.inSampleSize = scaleDownSampleSize;
276                 }
277                 crop = decoder.decodeRegion(roundedTrueCrop, options);
278                 decoder.recycle();
279             }
280 
281             if (crop == null) {
282                 // BitmapRegionDecoder has failed, try to crop in-memory
283                 is = regenerateInputStream();
284                 Bitmap fullSize = null;
285                 if (is != null) {
286                     BitmapFactory.Options options = new BitmapFactory.Options();
287                     if (scaleDownSampleSize > 1) {
288                         options.inSampleSize = scaleDownSampleSize;
289                     }
290                     fullSize = BitmapFactory.decodeStream(is, null, options);
291                     Utils.closeSilently(is);
292                 }
293                 if (fullSize != null) {
294                     // Find out the true sample size that was used by the decoder
295                     scaleDownSampleSize = bounds.x / fullSize.getWidth();
296                     mCropBounds.left /= scaleDownSampleSize;
297                     mCropBounds.top /= scaleDownSampleSize;
298                     mCropBounds.bottom /= scaleDownSampleSize;
299                     mCropBounds.right /= scaleDownSampleSize;
300                     mCropBounds.roundOut(roundedTrueCrop);
301 
302                     // Adjust values to account for issues related to rounding
303                     if (roundedTrueCrop.width() > fullSize.getWidth()) {
304                         // Adjust the width
305                         roundedTrueCrop.right = roundedTrueCrop.left + fullSize.getWidth();
306                     }
307                     if (roundedTrueCrop.right > fullSize.getWidth()) {
308                         // Adjust the left and right values.
309                         roundedTrueCrop.offset(-(roundedTrueCrop.right - fullSize.getWidth()), 0);
310                     }
311                     if (roundedTrueCrop.height() > fullSize.getHeight()) {
312                         // Adjust the height
313                         roundedTrueCrop.bottom = roundedTrueCrop.top + fullSize.getHeight();
314                     }
315                     if (roundedTrueCrop.bottom > fullSize.getHeight()) {
316                         // Adjust the top and bottom values.
317                         roundedTrueCrop.offset(0, -(roundedTrueCrop.bottom - fullSize.getHeight()));
318                     }
319 
320                     crop = Bitmap.createBitmap(fullSize, roundedTrueCrop.left,
321                             roundedTrueCrop.top, roundedTrueCrop.width(),
322                             roundedTrueCrop.height());
323                 }
324             }
325 
326             if (crop == null) {
327                 Log.w(LOGTAG, "cannot decode file: " + mInUri.toString());
328                 failure = true;
329                 return false;
330             }
331             if (mOutWidth > 0 && mOutHeight > 0 || mRotation > 0) {
332                 float[] dimsAfter = new float[] { crop.getWidth(), crop.getHeight() };
333                 rotateMatrix.mapPoints(dimsAfter);
334                 dimsAfter[0] = Math.abs(dimsAfter[0]);
335                 dimsAfter[1] = Math.abs(dimsAfter[1]);
336 
337                 if (!(mOutWidth > 0 && mOutHeight > 0)) {
338                     mOutWidth = Math.round(dimsAfter[0]);
339                     mOutHeight = Math.round(dimsAfter[1]);
340                 }
341 
342                 RectF cropRect = new RectF(0, 0, dimsAfter[0], dimsAfter[1]);
343                 RectF returnRect = new RectF(0, 0, mOutWidth, mOutHeight);
344 
345                 Matrix m = new Matrix();
346                 if (mRotation == 0) {
347                     m.setRectToRect(cropRect, returnRect, Matrix.ScaleToFit.FILL);
348                 } else {
349                     Matrix m1 = new Matrix();
350                     m1.setTranslate(-crop.getWidth() / 2f, -crop.getHeight() / 2f);
351                     Matrix m2 = new Matrix();
352                     m2.setRotate(mRotation);
353                     Matrix m3 = new Matrix();
354                     m3.setTranslate(dimsAfter[0] / 2f, dimsAfter[1] / 2f);
355                     Matrix m4 = new Matrix();
356                     m4.setRectToRect(cropRect, returnRect, Matrix.ScaleToFit.FILL);
357 
358                     Matrix c1 = new Matrix();
359                     c1.setConcat(m2, m1);
360                     Matrix c2 = new Matrix();
361                     c2.setConcat(m4, m3);
362                     m.setConcat(c2, c1);
363                 }
364 
365                 Bitmap tmp = Bitmap.createBitmap((int) returnRect.width(),
366                         (int) returnRect.height(), Bitmap.Config.ARGB_8888);
367                 if (tmp != null) {
368                     Canvas c = new Canvas(tmp);
369                     Paint p = new Paint();
370                     p.setFilterBitmap(true);
371                     c.drawBitmap(crop, m, p);
372                     crop = tmp;
373                 }
374             }
375 
376             if (mSaveCroppedBitmap) {
377                 mCroppedBitmap = crop;
378             }
379 
380             // Compress to byte array
381             ByteArrayOutputStream tmpOut = new ByteArrayOutputStream(2048);
382             if (crop.compress(CompressFormat.JPEG, DEFAULT_COMPRESS_QUALITY, tmpOut)) {
383                 // If we need to set to the wallpaper, set it
384                 if (mSetWallpaper) {
385                     try {
386                         byte[] outByteArray = tmpOut.toByteArray();
387                         setWallpaper(new ByteArrayInputStream(outByteArray), null, whichWallpaper);
388                         if (mOnBitmapCroppedHandler != null) {
389                             mOnBitmapCroppedHandler.onBitmapCropped(outByteArray,
390                                     new Rect(0, 0, crop.getWidth(), crop.getHeight()));
391                         }
392                     } catch (IOException e) {
393                         Log.w(LOGTAG, "cannot write stream to wallpaper", e);
394                         failure = true;
395                     }
396                 }
397             } else {
398                 Log.w(LOGTAG, "cannot compress bitmap");
399                 failure = true;
400             }
401         }
402         return !failure; // True if any of the operations failed
403     }
404 
405     @Override
doInBackground(Integer... params)406     protected Boolean doInBackground(Integer... params) {
407         return cropBitmap(params.length == 0 ? WallpaperManager.FLAG_SYSTEM : params[0]);
408     }
409 
410     @Override
onPostExecute(Boolean cropSucceeded)411     protected void onPostExecute(Boolean cropSucceeded) {
412         if (!cropSucceeded) {
413             Toast.makeText(mContext, R.string.wallpaper_set_fail, Toast.LENGTH_SHORT).show();
414         }
415         if (mOnEndCropHandler != null) {
416             mOnEndCropHandler.run(cropSucceeded);
417         }
418     }
419 
setWallpaper(InputStream in, Rect crop, int whichWallpaper)420     private void setWallpaper(InputStream in, Rect crop, int whichWallpaper) throws IOException {
421         if (!Utilities.ATLEAST_N) {
422             WallpaperManager.getInstance(mContext.getApplicationContext()).setStream(in);
423         } else {
424             NycWallpaperUtils.setStream(mContext, in, crop, true, whichWallpaper);
425         }
426     }
427 }