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.Context;
19 import android.graphics.Bitmap;
20 import android.graphics.BitmapFactory;
21 import android.graphics.Canvas;
22 import android.graphics.Paint;
23 import android.graphics.RectF;
24 
25 import com.android.messaging.datamodel.data.MessagePartData;
26 import com.android.messaging.datamodel.media.PoolableImageCache.ReusableImageResourcePool;
27 import com.android.messaging.util.Assert;
28 import com.android.messaging.util.ImageUtils;
29 import com.android.messaging.util.exif.ExifInterface;
30 
31 import java.io.FileNotFoundException;
32 import java.io.IOException;
33 import java.io.InputStream;
34 import java.util.List;
35 
36 /**
37  * Base class that serves an image request for resolving, retrieving and decoding bitmap resources.
38  *
39  * Subclasses may choose to load images from different medium, such as from the file system or
40  * from the local content resolver, by overriding the abstract getInputStreamForResource() method.
41  */
42 public abstract class ImageRequest<D extends ImageRequestDescriptor>
43         implements MediaRequest<ImageResource> {
44     public static final int UNSPECIFIED_SIZE = MessagePartData.UNSPECIFIED_SIZE;
45 
46     protected final Context mContext;
47     protected final D mDescriptor;
48     protected int mOrientation;
49 
50     /**
51      * Creates a new image request with the given descriptor.
52      */
ImageRequest(final Context context, final D descriptor)53     public ImageRequest(final Context context, final D descriptor) {
54         mContext = context;
55         mDescriptor = descriptor;
56     }
57 
58     /**
59      * Gets a key that uniquely identify the underlying image resource to be loaded (e.g. Uri or
60      * file path).
61      */
62     @Override
getKey()63     public String getKey() {
64         return mDescriptor.getKey();
65     }
66 
67     /**
68      * Returns the image request descriptor attached to this request.
69      */
70     @Override
getDescriptor()71     public D getDescriptor() {
72         return mDescriptor;
73     }
74 
75     @Override
getRequestType()76     public int getRequestType() {
77         return MediaRequest.REQUEST_LOAD_MEDIA;
78     }
79 
80     /**
81      * Allows sub classes to specify that they want us to call getBitmapForResource rather than
82      * getInputStreamForResource
83      */
hasBitmapObject()84     protected boolean hasBitmapObject() {
85         return false;
86     }
87 
getBitmapForResource()88     protected Bitmap getBitmapForResource() throws IOException {
89         return null;
90     }
91 
92     /**
93      * Retrieves an input stream from which image resource could be loaded.
94      * @throws FileNotFoundException
95      */
getInputStreamForResource()96     protected abstract InputStream getInputStreamForResource() throws FileNotFoundException;
97 
98     /**
99      * Loads the image resource. This method is final; to override the media loading behavior
100      * the subclass should override {@link #loadMediaInternal(List)}
101      */
102     @Override
loadMediaBlocking(List<MediaRequest<ImageResource>> chainedTask)103     public final ImageResource loadMediaBlocking(List<MediaRequest<ImageResource>> chainedTask)
104             throws IOException {
105         Assert.isNotMainThread();
106         final ImageResource loadedResource = loadMediaInternal(chainedTask);
107         return postProcessOnBitmapResourceLoaded(loadedResource);
108     }
109 
loadMediaInternal(List<MediaRequest<ImageResource>> chainedTask)110     protected ImageResource loadMediaInternal(List<MediaRequest<ImageResource>> chainedTask)
111             throws IOException {
112         if (!mDescriptor.isStatic() && isGif()) {
113             final GifImageResource gifImageResource =
114                     GifImageResource.createGifImageResource(getKey(), getInputStreamForResource());
115             if (gifImageResource == null) {
116                 throw new RuntimeException("Error decoding gif");
117             }
118             return gifImageResource;
119         } else {
120             final Bitmap loadedBitmap = loadBitmapInternal();
121             if (loadedBitmap == null) {
122                 throw new RuntimeException("failed decoding bitmap");
123             }
124             return new DecodedImageResource(getKey(), loadedBitmap, mOrientation);
125         }
126     }
127 
isGif()128     protected boolean isGif() throws FileNotFoundException {
129         return ImageUtils.isGif(getInputStreamForResource());
130     }
131 
132     /**
133      * The internal routine for loading the image. The caller may optionally provide the width
134      * and height of the source image if known so that we don't need to manually decode those.
135      */
loadBitmapInternal()136     protected Bitmap loadBitmapInternal() throws IOException {
137 
138         final boolean unknownSize = mDescriptor.sourceWidth == UNSPECIFIED_SIZE ||
139                 mDescriptor.sourceHeight == UNSPECIFIED_SIZE;
140 
141         // If the ImageRequest has a Bitmap object rather than a stream, there's little to do here
142         if (hasBitmapObject()) {
143             final Bitmap bitmap = getBitmapForResource();
144             if (bitmap != null && unknownSize) {
145                 mDescriptor.updateSourceDimensions(bitmap.getWidth(), bitmap.getHeight());
146             }
147             return bitmap;
148         }
149 
150         mOrientation = ImageUtils.getOrientation(getInputStreamForResource());
151 
152         final BitmapFactory.Options options = PoolableImageCache.getBitmapOptionsForPool(
153                 false /* scaled */, 0 /* inputDensity */, 0 /* targetDensity */);
154         // First, check dimensions of the bitmap if not already known.
155         if (unknownSize) {
156             final InputStream inputStream = getInputStreamForResource();
157             if (inputStream != null) {
158                 try {
159                     options.inJustDecodeBounds = true;
160                     BitmapFactory.decodeStream(inputStream, null, options);
161                     // This is called when dimensions of image were unknown to allow db update
162                     if (ExifInterface.getOrientationParams(mOrientation).invertDimensions) {
163                         mDescriptor.updateSourceDimensions(options.outHeight, options.outWidth);
164                     } else {
165                         mDescriptor.updateSourceDimensions(options.outWidth, options.outHeight);
166                     }
167                 } finally {
168                     inputStream.close();
169                 }
170             } else {
171                 throw new FileNotFoundException();
172             }
173         } else {
174             options.outWidth = mDescriptor.sourceWidth;
175             options.outHeight = mDescriptor.sourceHeight;
176         }
177 
178         // Calculate inSampleSize
179         options.inSampleSize = ImageUtils.get().calculateInSampleSize(options,
180                 mDescriptor.desiredWidth, mDescriptor.desiredHeight);
181         Assert.isTrue(options.inSampleSize > 0);
182 
183         // Reopen the input stream and actually decode the bitmap. The initial
184         // BitmapFactory.decodeStream() reads the header portion of the bitmap stream and leave
185         // the input stream at the last read position. Since this input stream doesn't support
186         // mark() and reset(), the only viable way to reload the input stream is to re-open it.
187         // Alternatively, we could decode the bitmap into a byte array first and act on the byte
188         // array, but that also means the entire bitmap (for example a 10MB image from the gallery)
189         // without downsampling will have to be loaded into memory up front, which we don't want
190         // as it gives a much bigger possibility of OOM when handling big images. Therefore, the
191         // solution here is to close and reopen the bitmap input stream.
192         // For inline images the size is cached in DB and this hit is only taken once per image
193         final InputStream inputStream = getInputStreamForResource();
194         if (inputStream != null) {
195             try {
196                 options.inJustDecodeBounds = false;
197 
198                 // Actually decode the bitmap, optionally using the bitmap pool.
199                 final ReusableImageResourcePool bitmapPool = getBitmapPool();
200                 if (bitmapPool == null) {
201                     return BitmapFactory.decodeStream(inputStream, null, options);
202                 } else {
203                     final int sampledWidth = (options.outWidth + options.inSampleSize - 1) /
204                             options.inSampleSize;
205                     final int sampledHeight = (options.outHeight + options.inSampleSize - 1) /
206                             options.inSampleSize;
207                     return bitmapPool.decodeSampledBitmapFromInputStream(
208                             inputStream, options, sampledWidth, sampledHeight);
209                 }
210             } finally {
211                 inputStream.close();
212             }
213         } else {
214             throw new FileNotFoundException();
215         }
216     }
217 
postProcessOnBitmapResourceLoaded(final ImageResource loadedResource)218     private ImageResource postProcessOnBitmapResourceLoaded(final ImageResource loadedResource) {
219         if (mDescriptor.cropToCircle && loadedResource instanceof DecodedImageResource) {
220             final int width = mDescriptor.desiredWidth;
221             final int height = mDescriptor.desiredHeight;
222             final Bitmap sourceBitmap = loadedResource.getBitmap();
223             final Bitmap targetBitmap = getBitmapPool().createOrReuseBitmap(width, height);
224             final RectF dest = new RectF(0, 0, width, height);
225             final RectF source = new RectF(0, 0, sourceBitmap.getWidth(), sourceBitmap.getHeight());
226             final int backgroundColor = mDescriptor.circleBackgroundColor;
227             final int strokeColor = mDescriptor.circleStrokeColor;
228             ImageUtils.drawBitmapWithCircleOnCanvas(sourceBitmap, new Canvas(targetBitmap), source,
229                     dest, null, backgroundColor == 0 ? false : true /* fillBackground */,
230                             backgroundColor, strokeColor);
231             return new DecodedImageResource(getKey(), targetBitmap,
232                     loadedResource.getOrientation());
233         }
234         return loadedResource;
235     }
236 
237     /**
238      * Returns the bitmap pool for this image request.
239      */
getBitmapPool()240     protected ReusableImageResourcePool getBitmapPool() {
241         return MediaCacheManager.get().getOrCreateBitmapPoolForCache(getCacheId());
242     }
243 
244     @SuppressWarnings("unchecked")
245     @Override
getMediaCache()246     public MediaCache<ImageResource> getMediaCache() {
247         return (MediaCache<ImageResource>) MediaCacheManager.get().getOrCreateMediaCacheById(
248                 getCacheId());
249     }
250 
251     /**
252      * Returns the cache id. Subclasses may override this to use a different cache.
253      */
254     @Override
getCacheId()255     public int getCacheId() {
256         return BugleMediaCacheManager.DEFAULT_IMAGE_CACHE;
257     }
258 }
259