1 /* 2 * Copyright (C) 2013 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 17 package com.android.photos; 18 19 import android.annotation.TargetApi; 20 import android.content.Context; 21 import android.content.res.Resources; 22 import android.graphics.Bitmap; 23 import android.graphics.Bitmap.Config; 24 import android.graphics.BitmapFactory; 25 import android.graphics.BitmapRegionDecoder; 26 import android.graphics.Canvas; 27 import android.graphics.Rect; 28 import android.net.Uri; 29 import android.os.Build; 30 import android.os.Build.VERSION_CODES; 31 import android.util.Log; 32 33 import com.android.gallery3d.common.BitmapUtils; 34 import com.android.gallery3d.glrenderer.BasicTexture; 35 import com.android.gallery3d.glrenderer.BitmapTexture; 36 import com.android.photos.views.TiledImageRenderer; 37 38 import java.io.BufferedInputStream; 39 import java.io.IOException; 40 import java.io.InputStream; 41 42 /** 43 * A {@link com.android.photos.views.TiledImageRenderer.TileSource} using 44 * {@link BitmapRegionDecoder} to wrap a local file 45 */ 46 @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) 47 public class BitmapRegionTileSource implements TiledImageRenderer.TileSource { 48 49 private static final String TAG = "BitmapRegionTileSource"; 50 51 private static final boolean REUSE_BITMAP = 52 Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN; 53 private static final int GL_SIZE_LIMIT = 2048; 54 // This must be no larger than half the size of the GL_SIZE_LIMIT 55 // due to decodePreview being allowed to be up to 2x the size of the target 56 private static final int MAX_PREVIEW_SIZE = 1024; 57 58 BitmapRegionDecoder mDecoder; 59 int mWidth; 60 int mHeight; 61 int mTileSize; 62 private BasicTexture mPreview; 63 private final int mRotation; 64 65 // For use only by getTile 66 private Rect mWantRegion = new Rect(); 67 private Rect mOverlapRegion = new Rect(); 68 private BitmapFactory.Options mOptions; 69 private Canvas mCanvas; 70 71 public BitmapRegionTileSource(Context context, String path, int previewSize, int rotation) { 72 this(null, context, path, null, 0, previewSize, rotation); 73 } 74 75 public BitmapRegionTileSource(Context context, Uri uri, int previewSize, int rotation) { 76 this(null, context, null, uri, 0, previewSize, rotation); 77 } 78 79 public BitmapRegionTileSource(Resources res, 80 Context context, int resId, int previewSize, int rotation) { 81 this(res, context, null, null, resId, previewSize, rotation); 82 } 83 84 private BitmapRegionTileSource(Resources res, 85 Context context, String path, Uri uri, int resId, int previewSize, int rotation) { 86 mTileSize = TiledImageRenderer.suggestedTileSize(context); 87 mRotation = rotation; 88 try { 89 if (path != null) { 90 mDecoder = BitmapRegionDecoder.newInstance(path, true); 91 } else if (uri != null) { 92 InputStream is = context.getContentResolver().openInputStream(uri); 93 BufferedInputStream bis = new BufferedInputStream(is); 94 mDecoder = BitmapRegionDecoder.newInstance(bis, true); 95 } else { 96 InputStream is = res.openRawResource(resId); 97 BufferedInputStream bis = new BufferedInputStream(is); 98 mDecoder = BitmapRegionDecoder.newInstance(bis, true); 99 } 100 mWidth = mDecoder.getWidth(); 101 mHeight = mDecoder.getHeight(); 102 } catch (IOException e) { 103 Log.w("BitmapRegionTileSource", "ctor failed", e); 104 } 105 mOptions = new BitmapFactory.Options(); 106 mOptions.inPreferredConfig = Bitmap.Config.ARGB_8888; 107 mOptions.inPreferQualityOverSpeed = true; 108 mOptions.inTempStorage = new byte[16 * 1024]; 109 if (previewSize != 0) { 110 previewSize = Math.min(previewSize, MAX_PREVIEW_SIZE); 111 // Although this is the same size as the Bitmap that is likely already 112 // loaded, the lifecycle is different and interactions are on a different 113 // thread. Thus to simplify, this source will decode its own bitmap. 114 Bitmap preview = decodePreview(res, context, path, uri, resId, previewSize); 115 if (preview.getWidth() <= GL_SIZE_LIMIT && preview.getHeight() <= GL_SIZE_LIMIT) { 116 mPreview = new BitmapTexture(preview); 117 } else { 118 Log.w(TAG, String.format( 119 "Failed to create preview of apropriate size! " 120 + " in: %dx%d, out: %dx%d", 121 mWidth, mHeight, 122 preview.getWidth(), preview.getHeight())); 123 } 124 } 125 } 126 127 @Override 128 public int getTileSize() { 129 return mTileSize; 130 } 131 132 @Override 133 public int getImageWidth() { 134 return mWidth; 135 } 136 137 @Override 138 public int getImageHeight() { 139 return mHeight; 140 } 141 142 @Override 143 public BasicTexture getPreview() { 144 return mPreview; 145 } 146 147 @Override 148 public int getRotation() { 149 return mRotation; 150 } 151 152 @Override 153 public Bitmap getTile(int level, int x, int y, Bitmap bitmap) { 154 int tileSize = getTileSize(); 155 if (!REUSE_BITMAP) { 156 return getTileWithoutReusingBitmap(level, x, y, tileSize); 157 } 158 159 int t = tileSize << level; 160 mWantRegion.set(x, y, x + t, y + t); 161 162 if (bitmap == null) { 163 bitmap = Bitmap.createBitmap(tileSize, tileSize, Bitmap.Config.ARGB_8888); 164 } 165 166 mOptions.inSampleSize = (1 << level); 167 mOptions.inBitmap = bitmap; 168 169 try { 170 bitmap = mDecoder.decodeRegion(mWantRegion, mOptions); 171 } finally { 172 if (mOptions.inBitmap != bitmap && mOptions.inBitmap != null) { 173 mOptions.inBitmap = null; 174 } 175 } 176 177 if (bitmap == null) { 178 Log.w("BitmapRegionTileSource", "fail in decoding region"); 179 } 180 return bitmap; 181 } 182 183 private Bitmap getTileWithoutReusingBitmap( 184 int level, int x, int y, int tileSize) { 185 186 int t = tileSize << level; 187 mWantRegion.set(x, y, x + t, y + t); 188 189 mOverlapRegion.set(0, 0, mWidth, mHeight); 190 191 mOptions.inSampleSize = (1 << level); 192 Bitmap bitmap = mDecoder.decodeRegion(mOverlapRegion, mOptions); 193 194 if (bitmap == null) { 195 Log.w(TAG, "fail in decoding region"); 196 } 197 198 if (mWantRegion.equals(mOverlapRegion)) { 199 return bitmap; 200 } 201 202 Bitmap result = Bitmap.createBitmap(tileSize, tileSize, Config.ARGB_8888); 203 if (mCanvas == null) { 204 mCanvas = new Canvas(); 205 } 206 mCanvas.setBitmap(result); 207 mCanvas.drawBitmap(bitmap, 208 (mOverlapRegion.left - mWantRegion.left) >> level, 209 (mOverlapRegion.top - mWantRegion.top) >> level, null); 210 mCanvas.setBitmap(null); 211 return result; 212 } 213 214 /** 215 * Note that the returned bitmap may have a long edge that's longer 216 * than the targetSize, but it will always be less than 2x the targetSize 217 */ 218 private Bitmap decodePreview( 219 Resources res, Context context, String file, Uri uri, int resId, int targetSize) { 220 float scale = (float) targetSize / Math.max(mWidth, mHeight); 221 mOptions.inSampleSize = BitmapUtils.computeSampleSizeLarger(scale); 222 mOptions.inJustDecodeBounds = false; 223 224 Bitmap result = null; 225 if (file != null) { 226 result = BitmapFactory.decodeFile(file, mOptions); 227 } else if (uri != null) { 228 try { 229 InputStream is = context.getContentResolver().openInputStream(uri); 230 BufferedInputStream bis = new BufferedInputStream(is); 231 result = BitmapFactory.decodeStream(bis, null, mOptions); 232 } catch (IOException e) { 233 Log.w("BitmapRegionTileSource", "getting preview failed", e); 234 } 235 } else { 236 result = BitmapFactory.decodeResource(res, resId, mOptions); 237 } 238 if (result == null) { 239 return null; 240 } 241 242 // We need to resize down if the decoder does not support inSampleSize 243 // or didn't support the specified inSampleSize (some decoders only do powers of 2) 244 scale = (float) targetSize / (float) (Math.max(result.getWidth(), result.getHeight())); 245 246 if (scale <= 0.5) { 247 result = BitmapUtils.resizeBitmapByScale(result, scale, true); 248 } 249 return ensureGLCompatibleBitmap(result); 250 } 251 252 private static Bitmap ensureGLCompatibleBitmap(Bitmap bitmap) { 253 if (bitmap == null || bitmap.getConfig() != null) { 254 return bitmap; 255 } 256 Bitmap newBitmap = bitmap.copy(Config.ARGB_8888, false); 257 bitmap.recycle(); 258 return newBitmap; 259 } 260 } 261