| /* |
| * Copyright (C) 2015 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.android.messaging.datamodel.media; |
| |
| import android.content.Context; |
| import android.graphics.Bitmap; |
| import android.graphics.BitmapFactory; |
| import android.graphics.Canvas; |
| import android.graphics.Paint; |
| import android.graphics.RectF; |
| |
| import com.android.messaging.datamodel.data.MessagePartData; |
| import com.android.messaging.datamodel.media.PoolableImageCache.ReusableImageResourcePool; |
| import com.android.messaging.util.Assert; |
| import com.android.messaging.util.ImageUtils; |
| import com.android.messaging.util.exif.ExifInterface; |
| |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.util.List; |
| |
| /** |
| * Base class that serves an image request for resolving, retrieving and decoding bitmap resources. |
| * |
| * Subclasses may choose to load images from different medium, such as from the file system or |
| * from the local content resolver, by overriding the abstract getInputStreamForResource() method. |
| */ |
| public abstract class ImageRequest<D extends ImageRequestDescriptor> |
| implements MediaRequest<ImageResource> { |
| public static final int UNSPECIFIED_SIZE = MessagePartData.UNSPECIFIED_SIZE; |
| |
| protected final Context mContext; |
| protected final D mDescriptor; |
| protected int mOrientation; |
| |
| /** |
| * Creates a new image request with the given descriptor. |
| */ |
| public ImageRequest(final Context context, final D descriptor) { |
| mContext = context; |
| mDescriptor = descriptor; |
| } |
| |
| /** |
| * Gets a key that uniquely identify the underlying image resource to be loaded (e.g. Uri or |
| * file path). |
| */ |
| @Override |
| public String getKey() { |
| return mDescriptor.getKey(); |
| } |
| |
| /** |
| * Returns the image request descriptor attached to this request. |
| */ |
| @Override |
| public D getDescriptor() { |
| return mDescriptor; |
| } |
| |
| @Override |
| public int getRequestType() { |
| return MediaRequest.REQUEST_LOAD_MEDIA; |
| } |
| |
| /** |
| * Allows sub classes to specify that they want us to call getBitmapForResource rather than |
| * getInputStreamForResource |
| */ |
| protected boolean hasBitmapObject() { |
| return false; |
| } |
| |
| protected Bitmap getBitmapForResource() throws IOException { |
| return null; |
| } |
| |
| /** |
| * Retrieves an input stream from which image resource could be loaded. |
| * @throws FileNotFoundException |
| */ |
| protected abstract InputStream getInputStreamForResource() throws FileNotFoundException; |
| |
| /** |
| * Loads the image resource. This method is final; to override the media loading behavior |
| * the subclass should override {@link #loadMediaInternal(List)} |
| */ |
| @Override |
| public final ImageResource loadMediaBlocking(List<MediaRequest<ImageResource>> chainedTask) |
| throws IOException { |
| Assert.isNotMainThread(); |
| final ImageResource loadedResource = loadMediaInternal(chainedTask); |
| return postProcessOnBitmapResourceLoaded(loadedResource); |
| } |
| |
| protected ImageResource loadMediaInternal(List<MediaRequest<ImageResource>> chainedTask) |
| throws IOException { |
| if (!mDescriptor.isStatic() && isGif()) { |
| final GifImageResource gifImageResource = |
| GifImageResource.createGifImageResource(getKey(), getInputStreamForResource()); |
| if (gifImageResource == null) { |
| throw new RuntimeException("Error decoding gif"); |
| } |
| return gifImageResource; |
| } else { |
| final Bitmap loadedBitmap = loadBitmapInternal(); |
| if (loadedBitmap == null) { |
| throw new RuntimeException("failed decoding bitmap"); |
| } |
| return new DecodedImageResource(getKey(), loadedBitmap, mOrientation); |
| } |
| } |
| |
| protected boolean isGif() throws FileNotFoundException { |
| return ImageUtils.isGif(getInputStreamForResource()); |
| } |
| |
| /** |
| * The internal routine for loading the image. The caller may optionally provide the width |
| * and height of the source image if known so that we don't need to manually decode those. |
| */ |
| protected Bitmap loadBitmapInternal() throws IOException { |
| |
| final boolean unknownSize = mDescriptor.sourceWidth == UNSPECIFIED_SIZE || |
| mDescriptor.sourceHeight == UNSPECIFIED_SIZE; |
| |
| // If the ImageRequest has a Bitmap object rather than a stream, there's little to do here |
| if (hasBitmapObject()) { |
| final Bitmap bitmap = getBitmapForResource(); |
| if (bitmap != null && unknownSize) { |
| mDescriptor.updateSourceDimensions(bitmap.getWidth(), bitmap.getHeight()); |
| } |
| return bitmap; |
| } |
| |
| mOrientation = ImageUtils.getOrientation(getInputStreamForResource()); |
| |
| final BitmapFactory.Options options = PoolableImageCache.getBitmapOptionsForPool( |
| false /* scaled */, 0 /* inputDensity */, 0 /* targetDensity */); |
| // First, check dimensions of the bitmap if not already known. |
| if (unknownSize) { |
| final InputStream inputStream = getInputStreamForResource(); |
| if (inputStream != null) { |
| try { |
| options.inJustDecodeBounds = true; |
| BitmapFactory.decodeStream(inputStream, null, options); |
| // This is called when dimensions of image were unknown to allow db update |
| if (ExifInterface.getOrientationParams(mOrientation).invertDimensions) { |
| mDescriptor.updateSourceDimensions(options.outHeight, options.outWidth); |
| } else { |
| mDescriptor.updateSourceDimensions(options.outWidth, options.outHeight); |
| } |
| } finally { |
| inputStream.close(); |
| } |
| } else { |
| throw new FileNotFoundException(); |
| } |
| } else { |
| options.outWidth = mDescriptor.sourceWidth; |
| options.outHeight = mDescriptor.sourceHeight; |
| } |
| |
| // Calculate inSampleSize |
| options.inSampleSize = ImageUtils.get().calculateInSampleSize(options, |
| mDescriptor.desiredWidth, mDescriptor.desiredHeight); |
| Assert.isTrue(options.inSampleSize > 0); |
| |
| // Reopen the input stream and actually decode the bitmap. The initial |
| // BitmapFactory.decodeStream() reads the header portion of the bitmap stream and leave |
| // the input stream at the last read position. Since this input stream doesn't support |
| // mark() and reset(), the only viable way to reload the input stream is to re-open it. |
| // Alternatively, we could decode the bitmap into a byte array first and act on the byte |
| // array, but that also means the entire bitmap (for example a 10MB image from the gallery) |
| // without downsampling will have to be loaded into memory up front, which we don't want |
| // as it gives a much bigger possibility of OOM when handling big images. Therefore, the |
| // solution here is to close and reopen the bitmap input stream. |
| // For inline images the size is cached in DB and this hit is only taken once per image |
| final InputStream inputStream = getInputStreamForResource(); |
| if (inputStream != null) { |
| try { |
| options.inJustDecodeBounds = false; |
| |
| // Actually decode the bitmap, optionally using the bitmap pool. |
| final ReusableImageResourcePool bitmapPool = getBitmapPool(); |
| if (bitmapPool == null) { |
| return BitmapFactory.decodeStream(inputStream, null, options); |
| } else { |
| final int sampledWidth = (options.outWidth + options.inSampleSize - 1) / |
| options.inSampleSize; |
| final int sampledHeight = (options.outHeight + options.inSampleSize - 1) / |
| options.inSampleSize; |
| return bitmapPool.decodeSampledBitmapFromInputStream( |
| inputStream, options, sampledWidth, sampledHeight); |
| } |
| } finally { |
| inputStream.close(); |
| } |
| } else { |
| throw new FileNotFoundException(); |
| } |
| } |
| |
| private ImageResource postProcessOnBitmapResourceLoaded(final ImageResource loadedResource) { |
| if (mDescriptor.cropToCircle && loadedResource instanceof DecodedImageResource) { |
| final int width = mDescriptor.desiredWidth; |
| final int height = mDescriptor.desiredHeight; |
| final Bitmap sourceBitmap = loadedResource.getBitmap(); |
| final Bitmap targetBitmap = getBitmapPool().createOrReuseBitmap(width, height); |
| final RectF dest = new RectF(0, 0, width, height); |
| final RectF source = new RectF(0, 0, sourceBitmap.getWidth(), sourceBitmap.getHeight()); |
| final int backgroundColor = mDescriptor.circleBackgroundColor; |
| final int strokeColor = mDescriptor.circleStrokeColor; |
| ImageUtils.drawBitmapWithCircleOnCanvas(sourceBitmap, new Canvas(targetBitmap), source, |
| dest, null, backgroundColor == 0 ? false : true /* fillBackground */, |
| backgroundColor, strokeColor); |
| return new DecodedImageResource(getKey(), targetBitmap, |
| loadedResource.getOrientation()); |
| } |
| return loadedResource; |
| } |
| |
| /** |
| * Returns the bitmap pool for this image request. |
| */ |
| protected ReusableImageResourcePool getBitmapPool() { |
| return MediaCacheManager.get().getOrCreateBitmapPoolForCache(getCacheId()); |
| } |
| |
| @SuppressWarnings("unchecked") |
| @Override |
| public MediaCache<ImageResource> getMediaCache() { |
| return (MediaCache<ImageResource>) MediaCacheManager.get().getOrCreateMediaCacheById( |
| getCacheId()); |
| } |
| |
| /** |
| * Returns the cache id. Subclasses may override this to use a different cache. |
| */ |
| @Override |
| public int getCacheId() { |
| return BugleMediaCacheManager.DEFAULT_IMAGE_CACHE; |
| } |
| } |