Home | History | Annotate | Download | only in common
      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 {
     50         public void onBitmapCropped(byte[] imageBytes, Rect cropHint);
     51     }
     52 
     53     public interface OnEndCropHandler {
     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 
     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 
     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 
     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 
    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 
    114     public void setOnBitmapCropped(BitmapCropTask.OnBitmapCroppedHandler handler) {
    115         mOnBitmapCroppedHandler = handler;
    116     }
    117 
    118     public void setNoCrop(boolean value) {
    119         mNoCrop = value;
    120     }
    121 
    122     public void setOnEndRunnable(OnEndCropHandler onEndCropHandler) {
    123         mOnEndCropHandler = onEndCropHandler;
    124     }
    125 
    126     // Helper to setup input stream
    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 
    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 
    164     public void setCropBounds(RectF cropBounds) {
    165         mCropBounds = cropBounds;
    166     }
    167 
    168     public Bitmap getCroppedBitmap() {
    169         return mCroppedBitmap;
    170     }
    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
    406     protected Boolean doInBackground(Integer... params) {
    407         return cropBitmap(params.length == 0 ? WallpaperManager.FLAG_SYSTEM : params[0]);
    408     }
    409 
    410     @Override
    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 
    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 }