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.Context;
19import android.graphics.Bitmap;
20import android.graphics.BitmapFactory;
21import android.graphics.Canvas;
22import android.graphics.Paint;
23import android.graphics.RectF;
24
25import com.android.messaging.datamodel.data.MessagePartData;
26import com.android.messaging.datamodel.media.PoolableImageCache.ReusableImageResourcePool;
27import com.android.messaging.util.Assert;
28import com.android.messaging.util.ImageUtils;
29import com.android.messaging.util.exif.ExifInterface;
30
31import java.io.FileNotFoundException;
32import java.io.IOException;
33import java.io.InputStream;
34import 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 */
42public 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     */
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
63    public String getKey() {
64        return mDescriptor.getKey();
65    }
66
67    /**
68     * Returns the image request descriptor attached to this request.
69     */
70    @Override
71    public D getDescriptor() {
72        return mDescriptor;
73    }
74
75    @Override
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     */
84    protected boolean hasBitmapObject() {
85        return false;
86    }
87
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     */
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
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
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
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     */
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
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     */
240    protected ReusableImageResourcePool getBitmapPool() {
241        return MediaCacheManager.get().getOrCreateBitmapPoolForCache(getCacheId());
242    }
243
244    @SuppressWarnings("unchecked")
245    @Override
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
255    public int getCacheId() {
256        return BugleMediaCacheManager.DEFAULT_IMAGE_CACHE;
257    }
258}
259