/* * 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 implements MediaRequest { 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> chainedTask) throws IOException { Assert.isNotMainThread(); final ImageResource loadedResource = loadMediaInternal(chainedTask); return postProcessOnBitmapResourceLoaded(loadedResource); } protected ImageResource loadMediaInternal(List> 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 getMediaCache() { return (MediaCache) 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; } }