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.messaging.datamodel.media;
17 
18 import android.content.res.Resources;
19 import android.graphics.Bitmap;
20 import android.graphics.drawable.Drawable;
21 
22 import com.android.messaging.ui.OrientedBitmapDrawable;
23 import com.android.messaging.util.Assert;
24 import com.android.messaging.util.Assert.DoesNotRunOnMainThread;
25 import com.android.messaging.util.ImageUtils;
26 import com.android.messaging.util.LogUtil;
27 import com.android.messaging.util.OsUtil;
28 
29 import 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  */
36 public 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 
DecodedImageResource(final String key, final Bitmap bitmap, int orientation)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
getBitmap()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
reuseBitmap()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
supportsBitmapReuse()82     public boolean supportsBitmapReuse() {
83         return true;
84     }
85 
86     @Override
getBytes()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
getOrientation()103     public int getOrientation() {
104         return mOrientation;
105     }
106 
107     @Override
getMediaSize()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
close()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
getDrawable(Resources resources)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
isCacheable()147     boolean isCacheable() {
148         return mCacheable;
149     }
150 
setCacheable(final boolean cacheable)151     public void setCacheable(final boolean cacheable) {
152         mCacheable = cacheable;
153     }
154 
155     @SuppressWarnings("unchecked")
156     @Override
getMediaEncodingRequest( final MediaRequest<? extends RefCountedMediaResource> originalRequest)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 
EncodeImageRequest(MediaRequest<ImageResource> originalImageRequest)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
getKey()180         public String getKey() {
181             return DecodedImageResource.this.getKey();
182         }
183 
184         @Override
185         @DoesNotRunOnMainThread
loadMediaBlocking(List<MediaRequest<ImageResource>> chainedRequests)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
getMediaCache()235         public MediaCache<ImageResource> getMediaCache() {
236             return mOriginalImageRequest.getMediaCache();
237         }
238 
239         @Override
getCacheId()240         public int getCacheId() {
241             return mOriginalImageRequest.getCacheId();
242         }
243 
244         @Override
getRequestType()245         public int getRequestType() {
246             return REQUEST_ENCODE_MEDIA;
247         }
248 
249         @Override
getDescriptor()250         public MediaRequestDescriptor<ImageResource> getDescriptor() {
251             return mOriginalImageRequest.getDescriptor();
252         }
253     }
254 }
255