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
17package com.android.messaging.ui.mediapicker;
18
19import android.content.Context;
20import android.graphics.Bitmap;
21import android.graphics.BitmapFactory;
22import android.graphics.Canvas;
23import android.graphics.Matrix;
24import android.net.Uri;
25
26import com.android.messaging.datamodel.MediaScratchFileProvider;
27import com.android.messaging.util.Assert;
28import com.android.messaging.util.ContentType;
29import com.android.messaging.util.LogUtil;
30import com.android.messaging.util.SafeAsyncTask;
31import com.android.messaging.util.exif.ExifInterface;
32import com.android.messaging.util.exif.ExifTag;
33
34import java.io.IOException;
35import java.io.OutputStream;
36
37public class ImagePersistTask extends SafeAsyncTask<Void, Void, Void> {
38    private static final String JPEG_EXTENSION = "jpg";
39    private static final String TAG = LogUtil.BUGLE_TAG;
40
41    private int mWidth;
42    private int mHeight;
43    private final float mHeightPercent;
44    private final byte[] mBytes;
45    private final Context mContext;
46    private final CameraManager.MediaCallback mCallback;
47    private Uri mOutputUri;
48    private Exception mException;
49
50    public ImagePersistTask(
51            final int width,
52            final int height,
53            final float heightPercent,
54            final byte[] bytes,
55            final Context context,
56            final CameraManager.MediaCallback callback) {
57        Assert.isTrue(heightPercent >= 0 && heightPercent <= 1);
58        Assert.notNull(bytes);
59        Assert.notNull(context);
60        Assert.notNull(callback);
61        mWidth = width;
62        mHeight = height;
63        mHeightPercent = heightPercent;
64        mBytes = bytes;
65        mContext = context;
66        mCallback = callback;
67        // TODO: We probably want to store directly in MMS storage to prevent this
68        // intermediate step
69        mOutputUri = MediaScratchFileProvider.buildMediaScratchSpaceUri(JPEG_EXTENSION);
70    }
71
72    @Override
73    protected Void doInBackgroundTimed(final Void... params) {
74        OutputStream outputStream = null;
75        Bitmap bitmap = null;
76        Bitmap clippedBitmap = null;
77        try {
78            outputStream =
79                    mContext.getContentResolver().openOutputStream(mOutputUri);
80            if (mHeightPercent != 1.0f) {
81                int orientation = android.media.ExifInterface.ORIENTATION_UNDEFINED;
82                final ExifInterface exifInterface = new ExifInterface();
83                try {
84                    exifInterface.readExif(mBytes);
85                    final Integer orientationValue =
86                            exifInterface.getTagIntValue(ExifInterface.TAG_ORIENTATION);
87                    if (orientationValue != null) {
88                        orientation = orientationValue.intValue();
89                    }
90                    // The thumbnail is of the full image, but we're cropping it, so just clear
91                    // the thumbnail
92                    exifInterface.setCompressedThumbnail((byte[]) null);
93                } catch (IOException e) {
94                    // Couldn't get exif tags, not the end of the world
95                }
96                bitmap = BitmapFactory.decodeByteArray(mBytes, 0, mBytes.length);
97                final int clippedWidth;
98                final int clippedHeight;
99                if (ExifInterface.getOrientationParams(orientation).invertDimensions) {
100                    Assert.equals(mWidth, bitmap.getHeight());
101                    Assert.equals(mHeight, bitmap.getWidth());
102                    clippedWidth = (int) (mHeight * mHeightPercent);
103                    clippedHeight = mWidth;
104                } else {
105                    Assert.equals(mWidth, bitmap.getWidth());
106                    Assert.equals(mHeight, bitmap.getHeight());
107                    clippedWidth = mWidth;
108                    clippedHeight = (int) (mHeight * mHeightPercent);
109                }
110                final int offsetTop = (bitmap.getHeight() - clippedHeight) / 2;
111                final int offsetLeft = (bitmap.getWidth() - clippedWidth) / 2;
112                mWidth = clippedWidth;
113                mHeight = clippedHeight;
114                clippedBitmap = Bitmap.createBitmap(clippedWidth, clippedHeight,
115                        Bitmap.Config.ARGB_8888);
116                clippedBitmap.setDensity(bitmap.getDensity());
117                final Canvas clippedBitmapCanvas = new Canvas(clippedBitmap);
118                final Matrix matrix = new Matrix();
119                matrix.postTranslate(-offsetLeft, -offsetTop);
120                clippedBitmapCanvas.drawBitmap(bitmap, matrix, null /* paint */);
121                clippedBitmapCanvas.save();
122                // EXIF data can take a big chunk of the file size and is often cleared by the
123                // carrier, only store orientation since that's critical
124                ExifTag orientationTag = exifInterface.getTag(ExifInterface.TAG_ORIENTATION);
125                exifInterface.clearExif();
126                exifInterface.setTag(orientationTag);
127                exifInterface.writeExif(clippedBitmap, outputStream);
128            } else {
129                outputStream.write(mBytes);
130            }
131        } catch (final IOException e) {
132            mOutputUri = null;
133            mException = e;
134            LogUtil.e(TAG, "Unable to persist image to temp storage " + e);
135        } finally {
136            if (bitmap != null) {
137                bitmap.recycle();
138            }
139
140            if (clippedBitmap != null) {
141                clippedBitmap.recycle();
142            }
143
144            if (outputStream != null) {
145                try {
146                    outputStream.flush();
147                } catch (final IOException e) {
148                    mOutputUri = null;
149                    mException = e;
150                    LogUtil.e(TAG, "error trying to flush and close the outputStream" + e);
151                } finally {
152                    try {
153                        outputStream.close();
154                    } catch (final IOException e) {
155                        // Do nothing.
156                    }
157                }
158            }
159        }
160        return null;
161    }
162
163    @Override
164    protected void onPostExecute(final Void aVoid) {
165        if (mOutputUri != null) {
166            mCallback.onMediaReady(mOutputUri, ContentType.IMAGE_JPEG, mWidth, mHeight);
167        } else {
168            Assert.notNull(mException);
169            mCallback.onMediaFailed(mException);
170        }
171    }
172}
173