Home | History | Annotate | Download | only in media
      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      */
     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