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