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 */
16package com.android.messaging.datamodel.media;
17
18import android.content.res.Resources;
19import android.graphics.Bitmap;
20import android.graphics.drawable.Drawable;
21
22import com.android.messaging.ui.OrientedBitmapDrawable;
23import com.android.messaging.util.Assert;
24import com.android.messaging.util.Assert.DoesNotRunOnMainThread;
25import com.android.messaging.util.ImageUtils;
26import com.android.messaging.util.LogUtil;
27import com.android.messaging.util.OsUtil;
28
29import java.util.List;
30
31
32/**
33 * Container class for holding a bitmap resource used by the MediaResourceManager. This resource
34 * can both be cached (albeit not very storage-efficiently) and directly used by the UI.
35 */
36public class DecodedImageResource extends ImageResource {
37    private static final int BITMAP_QUALITY = 100;
38    private static final int COMPRESS_QUALITY = 50;
39
40    private Bitmap mBitmap;
41    private final int mOrientation;
42    private boolean mCacheable = true;
43
44    public DecodedImageResource(final String key, final Bitmap bitmap, int orientation) {
45        super(key, orientation);
46        mBitmap = bitmap;
47        mOrientation = orientation;
48    }
49
50    /**
51     * Gets the contained bitmap.
52     */
53    @Override
54    public Bitmap getBitmap() {
55        acquireLock();
56        try {
57            return mBitmap;
58        } finally {
59            releaseLock();
60        }
61    }
62
63    /**
64     * Attempt to reuse the bitmap in the image resource and repurpose it for something else.
65     * After this, the image resource will relinquish ownership on the bitmap resource so that
66     * it doesn't try to recycle it when getting closed.
67     */
68    @Override
69    public Bitmap reuseBitmap() {
70        acquireLock();
71        try {
72            assertSingularRefCount();
73            final Bitmap retBitmap = mBitmap;
74            mBitmap = null;
75            return retBitmap;
76        } finally {
77            releaseLock();
78        }
79    }
80
81    @Override
82    public boolean supportsBitmapReuse() {
83        return true;
84    }
85
86    @Override
87    public byte[] getBytes() {
88        acquireLock();
89        try {
90            return ImageUtils.bitmapToBytes(mBitmap, BITMAP_QUALITY);
91        } catch (final Exception e) {
92            LogUtil.e(LogUtil.BUGLE_TAG, "Error trying to get the bitmap bytes " + e);
93        } finally {
94            releaseLock();
95        }
96        return null;
97    }
98
99    /**
100     * Gets the orientation of the image as one of the ExifInterface.ORIENTATION_* constants
101     */
102    @Override
103    public int getOrientation() {
104        return mOrientation;
105    }
106
107    @Override
108    public int getMediaSize() {
109        acquireLock();
110        try {
111            Assert.notNull(mBitmap);
112            if (OsUtil.isAtLeastKLP()) {
113                return mBitmap.getAllocationByteCount();
114            } else {
115                return mBitmap.getRowBytes() * mBitmap.getHeight();
116            }
117        } finally {
118            releaseLock();
119        }
120    }
121
122    @Override
123    protected void close() {
124        acquireLock();
125        try {
126            if (mBitmap != null) {
127                mBitmap.recycle();
128                mBitmap = null;
129            }
130        } finally {
131            releaseLock();
132        }
133    }
134
135    @Override
136    public Drawable getDrawable(Resources resources) {
137        acquireLock();
138        try {
139            Assert.notNull(mBitmap);
140            return OrientedBitmapDrawable.create(getOrientation(), resources, mBitmap);
141        } finally {
142            releaseLock();
143        }
144    }
145
146    @Override
147    boolean isCacheable() {
148        return mCacheable;
149    }
150
151    public void setCacheable(final boolean cacheable) {
152        mCacheable = cacheable;
153    }
154
155    @SuppressWarnings("unchecked")
156    @Override
157    MediaRequest<? extends RefCountedMediaResource> getMediaEncodingRequest(
158            final MediaRequest<? extends RefCountedMediaResource> originalRequest) {
159        Assert.isFalse(isEncoded());
160        if (getBitmap().hasAlpha()) {
161            // We can't compress images with alpha, as JPEG encoding doesn't support this.
162            return null;
163        }
164        return new EncodeImageRequest((MediaRequest<ImageResource>) originalRequest);
165    }
166
167    /**
168     * A MediaRequest that encodes the contained image resource.
169     */
170    private class EncodeImageRequest implements MediaRequest<ImageResource> {
171        private final MediaRequest<ImageResource> mOriginalImageRequest;
172
173        public EncodeImageRequest(MediaRequest<ImageResource> originalImageRequest) {
174            mOriginalImageRequest = originalImageRequest;
175            // Hold a ref onto the encoded resource before the request finishes.
176            DecodedImageResource.this.addRef();
177        }
178
179        @Override
180        public String getKey() {
181            return DecodedImageResource.this.getKey();
182        }
183
184        @Override
185        @DoesNotRunOnMainThread
186        public ImageResource loadMediaBlocking(List<MediaRequest<ImageResource>> chainedRequests)
187                throws Exception {
188            Assert.isNotMainThread();
189            acquireLock();
190            Bitmap scaledBitmap = null;
191            try {
192                Bitmap bitmap = getBitmap();
193                Assert.isFalse(bitmap.hasAlpha());
194                final int bitmapWidth = bitmap.getWidth();
195                final int bitmapHeight = bitmap.getHeight();
196                // The original bitmap was loaded using sub-sampling which was fast in terms of
197                // loading speed, but not optimized for caching, encoding and rendering (since
198                // bitmap resizing to fit the UI image views happens on the UI thread and should
199                // be avoided if possible). Therefore, try to resize the bitmap to the exact desired
200                // size before compressing it.
201                if (bitmapWidth > 0 && bitmapHeight > 0 &&
202                        mOriginalImageRequest instanceof ImageRequest<?>) {
203                    final ImageRequestDescriptor descriptor =
204                            ((ImageRequest<?>) mOriginalImageRequest).getDescriptor();
205                    final float targetScale = Math.max(
206                            (float) descriptor.desiredWidth / bitmapWidth,
207                            (float) descriptor.desiredHeight / bitmapHeight);
208                    final int targetWidth = (int) (bitmapWidth * targetScale);
209                    final int targetHeight = (int) (bitmapHeight * targetScale);
210                    // Only try to scale down the image to the desired size.
211                    if (targetScale < 1.0f && targetWidth > 0 && targetHeight > 0 &&
212                            targetWidth != bitmapWidth && targetHeight != bitmapHeight) {
213                        scaledBitmap = bitmap =
214                                Bitmap.createScaledBitmap(bitmap, targetWidth, targetHeight, false);
215                    }
216                }
217                byte[] encodedBytes = ImageUtils.bitmapToBytes(bitmap, COMPRESS_QUALITY);
218                return new EncodedImageResource(getKey(), encodedBytes, getOrientation());
219            } catch (Exception ex) {
220                // Something went wrong during bitmap compression, fall back to just using the
221                // original bitmap.
222                LogUtil.e(LogUtil.BUGLE_IMAGE_TAG, "Error compressing bitmap", ex);
223                return DecodedImageResource.this;
224            } finally {
225                if (scaledBitmap != null && scaledBitmap != getBitmap()) {
226                    scaledBitmap.recycle();
227                    scaledBitmap = null;
228                }
229                releaseLock();
230                release();
231            }
232        }
233
234        @Override
235        public MediaCache<ImageResource> getMediaCache() {
236            return mOriginalImageRequest.getMediaCache();
237        }
238
239        @Override
240        public int getCacheId() {
241            return mOriginalImageRequest.getCacheId();
242        }
243
244        @Override
245        public int getRequestType() {
246            return REQUEST_ENCODE_MEDIA;
247        }
248
249        @Override
250        public MediaRequestDescriptor<ImageResource> getDescriptor() {
251            return mOriginalImageRequest.getDescriptor();
252        }
253    }
254}
255