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 17package com.android.photos; 18 19import android.annotation.TargetApi; 20import android.content.Context; 21import android.content.res.Resources; 22import android.graphics.Bitmap; 23import android.graphics.BitmapFactory; 24import android.graphics.BitmapRegionDecoder; 25import android.graphics.Canvas; 26import android.graphics.Paint; 27import android.graphics.Rect; 28import android.net.Uri; 29import android.opengl.GLUtils; 30import android.os.Build; 31import android.util.Log; 32 33import com.android.gallery3d.common.ExifOrientation; 34import com.android.gallery3d.common.Utils; 35import com.android.gallery3d.glrenderer.BasicTexture; 36import com.android.gallery3d.glrenderer.BitmapTexture; 37import com.android.photos.views.TiledImageRenderer; 38import com.android.wallpaperpicker.common.InputStreamProvider; 39 40import java.io.File; 41import java.io.IOException; 42import java.io.InputStream; 43 44interface SimpleBitmapRegionDecoder { 45 int getWidth(); 46 int getHeight(); 47 Bitmap decodeRegion(Rect wantRegion, BitmapFactory.Options options); 48} 49 50class SimpleBitmapRegionDecoderWrapper implements SimpleBitmapRegionDecoder { 51 BitmapRegionDecoder mDecoder; 52 private SimpleBitmapRegionDecoderWrapper(BitmapRegionDecoder decoder) { 53 mDecoder = decoder; 54 } 55 56 public static SimpleBitmapRegionDecoderWrapper newInstance( 57 InputStream is, boolean isShareable) { 58 try { 59 BitmapRegionDecoder d = BitmapRegionDecoder.newInstance(is, isShareable); 60 if (d != null) { 61 return new SimpleBitmapRegionDecoderWrapper(d); 62 } 63 } catch (IOException e) { 64 Log.w("BitmapRegionTileSource", "getting decoder failed", e); 65 return null; 66 } 67 return null; 68 } 69 public int getWidth() { 70 return mDecoder.getWidth(); 71 } 72 public int getHeight() { 73 return mDecoder.getHeight(); 74 } 75 public Bitmap decodeRegion(Rect wantRegion, BitmapFactory.Options options) { 76 return mDecoder.decodeRegion(wantRegion, options); 77 } 78} 79 80class DumbBitmapRegionDecoder implements SimpleBitmapRegionDecoder { 81 Bitmap mBuffer; 82 Canvas mTempCanvas; 83 Paint mTempPaint; 84 private DumbBitmapRegionDecoder(Bitmap b) { 85 mBuffer = b; 86 } 87 public static DumbBitmapRegionDecoder newInstance(InputStream is) { 88 Bitmap b = BitmapFactory.decodeStream(is); 89 if (b != null) { 90 return new DumbBitmapRegionDecoder(b); 91 } 92 return null; 93 } 94 public int getWidth() { 95 return mBuffer.getWidth(); 96 } 97 public int getHeight() { 98 return mBuffer.getHeight(); 99 } 100 public Bitmap decodeRegion(Rect wantRegion, BitmapFactory.Options options) { 101 if (mTempCanvas == null) { 102 mTempCanvas = new Canvas(); 103 mTempPaint = new Paint(); 104 mTempPaint.setFilterBitmap(true); 105 } 106 int sampleSize = Math.max(options.inSampleSize, 1); 107 Bitmap newBitmap = Bitmap.createBitmap( 108 wantRegion.width() / sampleSize, 109 wantRegion.height() / sampleSize, 110 Bitmap.Config.ARGB_8888); 111 mTempCanvas.setBitmap(newBitmap); 112 mTempCanvas.save(); 113 mTempCanvas.scale(1f / sampleSize, 1f / sampleSize); 114 mTempCanvas.drawBitmap(mBuffer, -wantRegion.left, -wantRegion.top, mTempPaint); 115 mTempCanvas.restore(); 116 mTempCanvas.setBitmap(null); 117 return newBitmap; 118 } 119} 120 121/** 122 * A {@link com.android.photos.views.TiledImageRenderer.TileSource} using 123 * {@link BitmapRegionDecoder} to wrap a local file 124 */ 125@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) 126public class BitmapRegionTileSource implements TiledImageRenderer.TileSource { 127 128 private static final String TAG = "BitmapRegionTileSource"; 129 130 private static final int GL_SIZE_LIMIT = 2048; 131 // This must be no larger than half the size of the GL_SIZE_LIMIT 132 // due to decodePreview being allowed to be up to 2x the size of the target 133 private static final int MAX_PREVIEW_SIZE = GL_SIZE_LIMIT / 2; 134 135 public static abstract class BitmapSource { 136 private SimpleBitmapRegionDecoder mDecoder; 137 private Bitmap mPreview; 138 private int mRotation; 139 public enum State { NOT_LOADED, LOADED, ERROR_LOADING }; 140 private State mState = State.NOT_LOADED; 141 142 /** Returns whether loading was successful. */ 143 public boolean loadInBackground(InBitmapProvider bitmapProvider) { 144 mRotation = getExifRotation(); 145 mDecoder = loadBitmapRegionDecoder(); 146 if (mDecoder == null) { 147 mState = State.ERROR_LOADING; 148 return false; 149 } else { 150 int width = mDecoder.getWidth(); 151 int height = mDecoder.getHeight(); 152 153 BitmapFactory.Options opts = new BitmapFactory.Options(); 154 opts.inPreferredConfig = Bitmap.Config.ARGB_8888; 155 opts.inPreferQualityOverSpeed = true; 156 157 float scale = (float) MAX_PREVIEW_SIZE / Math.max(width, height); 158 opts.inSampleSize = Utils.computeSampleSizeLarger(scale); 159 opts.inJustDecodeBounds = false; 160 opts.inMutable = true; 161 162 if (bitmapProvider != null) { 163 int expectedPixles = (width / opts.inSampleSize) * (height / opts.inSampleSize); 164 Bitmap reusableBitmap = bitmapProvider.forPixelCount(expectedPixles); 165 if (reusableBitmap != null) { 166 // Try loading with reusable bitmap 167 opts.inBitmap = reusableBitmap; 168 try { 169 mPreview = loadPreviewBitmap(opts); 170 } catch (IllegalArgumentException e) { 171 Log.d(TAG, "Unable to reuse bitmap", e); 172 opts.inBitmap = null; 173 mPreview = null; 174 } 175 } 176 } 177 if (mPreview == null) { 178 mPreview = loadPreviewBitmap(opts); 179 } 180 if (mPreview == null) { 181 mState = State.ERROR_LOADING; 182 return false; 183 } 184 185 // Verify that the bitmap can be used on GL surface 186 try { 187 GLUtils.getInternalFormat(mPreview); 188 GLUtils.getType(mPreview); 189 mState = State.LOADED; 190 } catch (IllegalArgumentException e) { 191 Log.d(TAG, "Image cannot be rendered on a GL surface", e); 192 mState = State.ERROR_LOADING; 193 } 194 return mState == State.LOADED; 195 } 196 } 197 198 public State getLoadingState() { 199 return mState; 200 } 201 202 public SimpleBitmapRegionDecoder getBitmapRegionDecoder() { 203 return mDecoder; 204 } 205 206 public Bitmap getPreviewBitmap() { 207 return mPreview; 208 } 209 210 public int getRotation() { 211 return mRotation; 212 } 213 214 public abstract int getExifRotation(); 215 public abstract SimpleBitmapRegionDecoder loadBitmapRegionDecoder(); 216 public abstract Bitmap loadPreviewBitmap(BitmapFactory.Options options); 217 218 public interface InBitmapProvider { 219 Bitmap forPixelCount(int count); 220 } 221 } 222 223 public static class InputStreamSource extends BitmapSource { 224 private final InputStreamProvider mStreamProvider; 225 private final Context mContext; 226 227 public InputStreamSource(Context context, Uri uri) { 228 this(InputStreamProvider.fromUri(context, uri), context); 229 } 230 231 public InputStreamSource(Resources res, int resId, Context context) { 232 this(InputStreamProvider.fromResource(res, resId), context); 233 } 234 235 public InputStreamSource(InputStreamProvider streamProvider, Context context) { 236 mStreamProvider = streamProvider; 237 mContext = context; 238 } 239 240 @Override 241 public SimpleBitmapRegionDecoder loadBitmapRegionDecoder() { 242 try { 243 InputStream is = mStreamProvider.newStreamNotNull(); 244 SimpleBitmapRegionDecoder regionDecoder = 245 SimpleBitmapRegionDecoderWrapper.newInstance(is, false); 246 Utils.closeSilently(is); 247 if (regionDecoder == null) { 248 is = mStreamProvider.newStreamNotNull(); 249 regionDecoder = DumbBitmapRegionDecoder.newInstance(is); 250 Utils.closeSilently(is); 251 } 252 return regionDecoder; 253 } catch (IOException e) { 254 Log.e("InputStreamSource", "Failed to load stream", e); 255 return null; 256 } 257 } 258 259 @Override 260 public int getExifRotation() { 261 return mStreamProvider.getRotationFromExif(mContext); 262 } 263 264 @Override 265 public Bitmap loadPreviewBitmap(BitmapFactory.Options options) { 266 try { 267 InputStream is = mStreamProvider.newStreamNotNull(); 268 Bitmap b = BitmapFactory.decodeStream(is, null, options); 269 Utils.closeSilently(is); 270 return b; 271 } catch (IOException | OutOfMemoryError e) { 272 Log.e("InputStreamSource", "Failed to load stream", e); 273 return null; 274 } 275 } 276 } 277 278 public static class FilePathBitmapSource extends InputStreamSource { 279 private String mPath; 280 public FilePathBitmapSource(File file, Context context) { 281 super(context, Uri.fromFile(file)); 282 mPath = file.getAbsolutePath(); 283 } 284 285 @Override 286 public int getExifRotation() { 287 return ExifOrientation.readRotation(mPath); 288 } 289 } 290 291 SimpleBitmapRegionDecoder mDecoder; 292 int mWidth; 293 int mHeight; 294 int mTileSize; 295 private BasicTexture mPreview; 296 private final int mRotation; 297 298 // For use only by getTile 299 private Rect mWantRegion = new Rect(); 300 private BitmapFactory.Options mOptions; 301 302 public BitmapRegionTileSource(Context context, BitmapSource source, byte[] tempStorage) { 303 mTileSize = TiledImageRenderer.suggestedTileSize(context); 304 mRotation = source.getRotation(); 305 mDecoder = source.getBitmapRegionDecoder(); 306 if (mDecoder != null) { 307 mWidth = mDecoder.getWidth(); 308 mHeight = mDecoder.getHeight(); 309 mOptions = new BitmapFactory.Options(); 310 mOptions.inPreferredConfig = Bitmap.Config.ARGB_8888; 311 mOptions.inPreferQualityOverSpeed = true; 312 mOptions.inTempStorage = tempStorage; 313 314 Bitmap preview = source.getPreviewBitmap(); 315 if (preview != null && 316 preview.getWidth() <= GL_SIZE_LIMIT && preview.getHeight() <= GL_SIZE_LIMIT) { 317 mPreview = new BitmapTexture(preview); 318 } else { 319 Log.w(TAG, String.format( 320 "Failed to create preview of apropriate size! " 321 + " in: %dx%d, out: %dx%d", 322 mWidth, mHeight, 323 preview == null ? -1 : preview.getWidth(), 324 preview == null ? -1 : preview.getHeight())); 325 } 326 } 327 } 328 329 public Bitmap getBitmap() { 330 return mPreview instanceof BitmapTexture ? ((BitmapTexture) mPreview).getBitmap() : null; 331 } 332 333 @Override 334 public int getTileSize() { 335 return mTileSize; 336 } 337 338 @Override 339 public int getImageWidth() { 340 return mWidth; 341 } 342 343 @Override 344 public int getImageHeight() { 345 return mHeight; 346 } 347 348 @Override 349 public BasicTexture getPreview() { 350 return mPreview; 351 } 352 353 @Override 354 public int getRotation() { 355 return mRotation; 356 } 357 358 @Override 359 public Bitmap getTile(int level, int x, int y, Bitmap bitmap) { 360 int tileSize = getTileSize(); 361 int t = tileSize << level; 362 mWantRegion.set(x, y, x + t, y + t); 363 364 if (bitmap == null) { 365 bitmap = Bitmap.createBitmap(tileSize, tileSize, Bitmap.Config.ARGB_8888); 366 } 367 368 mOptions.inSampleSize = (1 << level); 369 mOptions.inBitmap = bitmap; 370 371 try { 372 bitmap = mDecoder.decodeRegion(mWantRegion, mOptions); 373 } finally { 374 if (mOptions.inBitmap != bitmap && mOptions.inBitmap != null) { 375 mOptions.inBitmap = null; 376 } 377 } 378 379 if (bitmap == null) { 380 Log.w("BitmapRegionTileSource", "fail in decoding region"); 381 } 382 return bitmap; 383 } 384} 385