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