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.Matrix; 28 import android.graphics.Paint; 29 import android.graphics.PorterDuff; 30 import android.graphics.Rect; 31 import android.net.Uri; 32 import android.os.Build; 33 import android.os.Build.VERSION_CODES; 34 import android.util.Log; 35 36 import com.android.gallery3d.common.BitmapUtils; 37 import com.android.gallery3d.common.Utils; 38 import com.android.gallery3d.exif.ExifInterface; 39 import com.android.gallery3d.glrenderer.BasicTexture; 40 import com.android.gallery3d.glrenderer.BitmapTexture; 41 import com.android.photos.views.TiledImageRenderer; 42 43 import java.io.BufferedInputStream; 44 import java.io.FileNotFoundException; 45 import java.io.IOException; 46 import java.io.InputStream; 47 48 interface SimpleBitmapRegionDecoder { getWidth()49 int getWidth(); getHeight()50 int getHeight(); decodeRegion(Rect wantRegion, BitmapFactory.Options options)51 Bitmap decodeRegion(Rect wantRegion, BitmapFactory.Options options); 52 } 53 54 class SimpleBitmapRegionDecoderWrapper implements SimpleBitmapRegionDecoder { 55 BitmapRegionDecoder mDecoder; SimpleBitmapRegionDecoderWrapper(BitmapRegionDecoder decoder)56 private SimpleBitmapRegionDecoderWrapper(BitmapRegionDecoder decoder) { 57 mDecoder = decoder; 58 } newInstance( String pathName, boolean isShareable)59 public static SimpleBitmapRegionDecoderWrapper newInstance( 60 String pathName, boolean isShareable) { 61 try { 62 BitmapRegionDecoder d = BitmapRegionDecoder.newInstance(pathName, isShareable); 63 if (d != null) { 64 return new SimpleBitmapRegionDecoderWrapper(d); 65 } 66 } catch (IOException e) { 67 Log.w("BitmapRegionTileSource", "getting decoder failed for path " + pathName, e); 68 return null; 69 } 70 return null; 71 } newInstance( InputStream is, boolean isShareable)72 public static SimpleBitmapRegionDecoderWrapper newInstance( 73 InputStream is, boolean isShareable) { 74 try { 75 BitmapRegionDecoder d = BitmapRegionDecoder.newInstance(is, isShareable); 76 if (d != null) { 77 return new SimpleBitmapRegionDecoderWrapper(d); 78 } 79 } catch (IOException e) { 80 Log.w("BitmapRegionTileSource", "getting decoder failed", e); 81 return null; 82 } 83 return null; 84 } getWidth()85 public int getWidth() { 86 return mDecoder.getWidth(); 87 } getHeight()88 public int getHeight() { 89 return mDecoder.getHeight(); 90 } decodeRegion(Rect wantRegion, BitmapFactory.Options options)91 public Bitmap decodeRegion(Rect wantRegion, BitmapFactory.Options options) { 92 return mDecoder.decodeRegion(wantRegion, options); 93 } 94 } 95 96 class DumbBitmapRegionDecoder implements SimpleBitmapRegionDecoder { 97 Bitmap mBuffer; 98 Canvas mTempCanvas; 99 Paint mTempPaint; DumbBitmapRegionDecoder(Bitmap b)100 private DumbBitmapRegionDecoder(Bitmap b) { 101 mBuffer = b; 102 } newInstance(String pathName)103 public static DumbBitmapRegionDecoder newInstance(String pathName) { 104 Bitmap b = BitmapFactory.decodeFile(pathName); 105 if (b != null) { 106 return new DumbBitmapRegionDecoder(b); 107 } 108 return null; 109 } newInstance(InputStream is)110 public static DumbBitmapRegionDecoder newInstance(InputStream is) { 111 Bitmap b = BitmapFactory.decodeStream(is); 112 if (b != null) { 113 return new DumbBitmapRegionDecoder(b); 114 } 115 return null; 116 } getWidth()117 public int getWidth() { 118 return mBuffer.getWidth(); 119 } getHeight()120 public int getHeight() { 121 return mBuffer.getHeight(); 122 } decodeRegion(Rect wantRegion, BitmapFactory.Options options)123 public Bitmap decodeRegion(Rect wantRegion, BitmapFactory.Options options) { 124 if (mTempCanvas == null) { 125 mTempCanvas = new Canvas(); 126 mTempPaint = new Paint(); 127 mTempPaint.setFilterBitmap(true); 128 } 129 int sampleSize = Math.max(options.inSampleSize, 1); 130 Bitmap newBitmap = Bitmap.createBitmap( 131 wantRegion.width() / sampleSize, 132 wantRegion.height() / sampleSize, 133 Bitmap.Config.ARGB_8888); 134 mTempCanvas.setBitmap(newBitmap); 135 mTempCanvas.save(); 136 mTempCanvas.scale(1f / sampleSize, 1f / sampleSize); 137 mTempCanvas.drawBitmap(mBuffer, -wantRegion.left, -wantRegion.top, mTempPaint); 138 mTempCanvas.restore(); 139 mTempCanvas.setBitmap(null); 140 return newBitmap; 141 } 142 } 143 144 /** 145 * A {@link com.android.photos.views.TiledImageRenderer.TileSource} using 146 * {@link BitmapRegionDecoder} to wrap a local file 147 */ 148 @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) 149 public class BitmapRegionTileSource implements TiledImageRenderer.TileSource { 150 151 private static final String TAG = "BitmapRegionTileSource"; 152 153 private static final boolean REUSE_BITMAP = 154 Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN; 155 private static final int GL_SIZE_LIMIT = 2048; 156 // This must be no larger than half the size of the GL_SIZE_LIMIT 157 // due to decodePreview being allowed to be up to 2x the size of the target 158 public static final int MAX_PREVIEW_SIZE = GL_SIZE_LIMIT / 2; 159 160 public static abstract class BitmapSource { 161 private SimpleBitmapRegionDecoder mDecoder; 162 private Bitmap mPreview; 163 private int mPreviewSize; 164 private int mRotation; 165 public enum State { NOT_LOADED, LOADED, ERROR_LOADING }; 166 private State mState = State.NOT_LOADED; BitmapSource(int previewSize)167 public BitmapSource(int previewSize) { 168 mPreviewSize = previewSize; 169 } loadInBackground()170 public boolean loadInBackground() { 171 ExifInterface ei = new ExifInterface(); 172 if (readExif(ei)) { 173 Integer ori = ei.getTagIntValue(ExifInterface.TAG_ORIENTATION); 174 if (ori != null) { 175 mRotation = ExifInterface.getRotationForOrientationValue(ori.shortValue()); 176 } 177 } 178 mDecoder = loadBitmapRegionDecoder(); 179 if (mDecoder == null) { 180 mState = State.ERROR_LOADING; 181 return false; 182 } else { 183 int width = mDecoder.getWidth(); 184 int height = mDecoder.getHeight(); 185 if (mPreviewSize != 0) { 186 int previewSize = Math.min(mPreviewSize, MAX_PREVIEW_SIZE); 187 BitmapFactory.Options opts = new BitmapFactory.Options(); 188 opts.inPreferredConfig = Bitmap.Config.ARGB_8888; 189 opts.inPreferQualityOverSpeed = true; 190 191 float scale = (float) previewSize / Math.max(width, height); 192 opts.inSampleSize = BitmapUtils.computeSampleSizeLarger(scale); 193 opts.inJustDecodeBounds = false; 194 mPreview = loadPreviewBitmap(opts); 195 } 196 mState = State.LOADED; 197 return true; 198 } 199 } 200 getLoadingState()201 public State getLoadingState() { 202 return mState; 203 } 204 getBitmapRegionDecoder()205 public SimpleBitmapRegionDecoder getBitmapRegionDecoder() { 206 return mDecoder; 207 } 208 getPreviewBitmap()209 public Bitmap getPreviewBitmap() { 210 return mPreview; 211 } 212 getPreviewSize()213 public int getPreviewSize() { 214 return mPreviewSize; 215 } 216 getRotation()217 public int getRotation() { 218 return mRotation; 219 } 220 readExif(ExifInterface ei)221 public abstract boolean readExif(ExifInterface ei); loadBitmapRegionDecoder()222 public abstract SimpleBitmapRegionDecoder loadBitmapRegionDecoder(); loadPreviewBitmap(BitmapFactory.Options options)223 public abstract Bitmap loadPreviewBitmap(BitmapFactory.Options options); 224 } 225 226 public static class FilePathBitmapSource extends BitmapSource { 227 private String mPath; FilePathBitmapSource(String path, int previewSize)228 public FilePathBitmapSource(String path, int previewSize) { 229 super(previewSize); 230 mPath = path; 231 } 232 @Override loadBitmapRegionDecoder()233 public SimpleBitmapRegionDecoder loadBitmapRegionDecoder() { 234 SimpleBitmapRegionDecoder d; 235 d = SimpleBitmapRegionDecoderWrapper.newInstance(mPath, true); 236 if (d == null) { 237 d = DumbBitmapRegionDecoder.newInstance(mPath); 238 } 239 return d; 240 } 241 @Override loadPreviewBitmap(BitmapFactory.Options options)242 public Bitmap loadPreviewBitmap(BitmapFactory.Options options) { 243 return BitmapFactory.decodeFile(mPath, options); 244 } 245 @Override readExif(ExifInterface ei)246 public boolean readExif(ExifInterface ei) { 247 try { 248 ei.readExif(mPath); 249 return true; 250 } catch (IOException e) { 251 Log.w("BitmapRegionTileSource", "getting decoder failed", e); 252 return false; 253 } 254 } 255 } 256 257 public static class UriBitmapSource extends BitmapSource { 258 private Context mContext; 259 private Uri mUri; UriBitmapSource(Context context, Uri uri, int previewSize)260 public UriBitmapSource(Context context, Uri uri, int previewSize) { 261 super(previewSize); 262 mContext = context; 263 mUri = uri; 264 } regenerateInputStream()265 private InputStream regenerateInputStream() throws FileNotFoundException { 266 InputStream is = mContext.getContentResolver().openInputStream(mUri); 267 return new BufferedInputStream(is); 268 } 269 @Override loadBitmapRegionDecoder()270 public SimpleBitmapRegionDecoder loadBitmapRegionDecoder() { 271 try { 272 InputStream is = regenerateInputStream(); 273 SimpleBitmapRegionDecoder regionDecoder = 274 SimpleBitmapRegionDecoderWrapper.newInstance(is, false); 275 Utils.closeSilently(is); 276 if (regionDecoder == null) { 277 is = regenerateInputStream(); 278 regionDecoder = DumbBitmapRegionDecoder.newInstance(is); 279 Utils.closeSilently(is); 280 } 281 return regionDecoder; 282 } catch (FileNotFoundException e) { 283 Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e); 284 return null; 285 } catch (IOException e) { 286 Log.e("BitmapRegionTileSource", "Failure while reading URI " + mUri, e); 287 return null; 288 } 289 } 290 @Override loadPreviewBitmap(BitmapFactory.Options options)291 public Bitmap loadPreviewBitmap(BitmapFactory.Options options) { 292 try { 293 InputStream is = regenerateInputStream(); 294 Bitmap b = BitmapFactory.decodeStream(is, null, options); 295 Utils.closeSilently(is); 296 return b; 297 } catch (FileNotFoundException e) { 298 Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e); 299 return null; 300 } 301 } 302 @Override readExif(ExifInterface ei)303 public boolean readExif(ExifInterface ei) { 304 InputStream is = null; 305 try { 306 is = regenerateInputStream(); 307 ei.readExif(is); 308 Utils.closeSilently(is); 309 return true; 310 } catch (FileNotFoundException e) { 311 Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e); 312 return false; 313 } catch (IOException e) { 314 Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e); 315 return false; 316 } finally { 317 Utils.closeSilently(is); 318 } 319 } 320 } 321 322 public static class ResourceBitmapSource extends BitmapSource { 323 private Resources mRes; 324 private int mResId; ResourceBitmapSource(Resources res, int resId, int previewSize)325 public ResourceBitmapSource(Resources res, int resId, int previewSize) { 326 super(previewSize); 327 mRes = res; 328 mResId = resId; 329 } regenerateInputStream()330 private InputStream regenerateInputStream() { 331 InputStream is = mRes.openRawResource(mResId); 332 return new BufferedInputStream(is); 333 } 334 @Override loadBitmapRegionDecoder()335 public SimpleBitmapRegionDecoder loadBitmapRegionDecoder() { 336 InputStream is = regenerateInputStream(); 337 SimpleBitmapRegionDecoder regionDecoder = 338 SimpleBitmapRegionDecoderWrapper.newInstance(is, false); 339 Utils.closeSilently(is); 340 if (regionDecoder == null) { 341 is = regenerateInputStream(); 342 regionDecoder = DumbBitmapRegionDecoder.newInstance(is); 343 Utils.closeSilently(is); 344 } 345 return regionDecoder; 346 } 347 @Override loadPreviewBitmap(BitmapFactory.Options options)348 public Bitmap loadPreviewBitmap(BitmapFactory.Options options) { 349 return BitmapFactory.decodeResource(mRes, mResId, options); 350 } 351 @Override readExif(ExifInterface ei)352 public boolean readExif(ExifInterface ei) { 353 try { 354 InputStream is = regenerateInputStream(); 355 ei.readExif(is); 356 Utils.closeSilently(is); 357 return true; 358 } catch (IOException e) { 359 Log.e("BitmapRegionTileSource", "Error reading resource", e); 360 return false; 361 } 362 } 363 } 364 365 SimpleBitmapRegionDecoder mDecoder; 366 int mWidth; 367 int mHeight; 368 int mTileSize; 369 private BasicTexture mPreview; 370 private final int mRotation; 371 372 // For use only by getTile 373 private Rect mWantRegion = new Rect(); 374 private Rect mOverlapRegion = new Rect(); 375 private BitmapFactory.Options mOptions; 376 private Canvas mCanvas; 377 BitmapRegionTileSource(Context context, BitmapSource source)378 public BitmapRegionTileSource(Context context, BitmapSource source) { 379 mTileSize = TiledImageRenderer.suggestedTileSize(context); 380 mRotation = source.getRotation(); 381 mDecoder = source.getBitmapRegionDecoder(); 382 if (mDecoder != null) { 383 mWidth = mDecoder.getWidth(); 384 mHeight = mDecoder.getHeight(); 385 mOptions = new BitmapFactory.Options(); 386 mOptions.inPreferredConfig = Bitmap.Config.ARGB_8888; 387 mOptions.inPreferQualityOverSpeed = true; 388 mOptions.inTempStorage = new byte[16 * 1024]; 389 int previewSize = source.getPreviewSize(); 390 if (previewSize != 0) { 391 previewSize = Math.min(previewSize, MAX_PREVIEW_SIZE); 392 // Although this is the same size as the Bitmap that is likely already 393 // loaded, the lifecycle is different and interactions are on a different 394 // thread. Thus to simplify, this source will decode its own bitmap. 395 Bitmap preview = decodePreview(source, previewSize); 396 if (preview.getWidth() <= GL_SIZE_LIMIT && preview.getHeight() <= GL_SIZE_LIMIT) { 397 mPreview = new BitmapTexture(preview); 398 } else { 399 Log.w(TAG, String.format( 400 "Failed to create preview of apropriate size! " 401 + " in: %dx%d, out: %dx%d", 402 mWidth, mHeight, 403 preview.getWidth(), preview.getHeight())); 404 } 405 } 406 } 407 } 408 409 @Override getTileSize()410 public int getTileSize() { 411 return mTileSize; 412 } 413 414 @Override getImageWidth()415 public int getImageWidth() { 416 return mWidth; 417 } 418 419 @Override getImageHeight()420 public int getImageHeight() { 421 return mHeight; 422 } 423 424 @Override getPreview()425 public BasicTexture getPreview() { 426 return mPreview; 427 } 428 429 @Override getRotation()430 public int getRotation() { 431 return mRotation; 432 } 433 434 @Override getTile(int level, int x, int y, Bitmap bitmap)435 public Bitmap getTile(int level, int x, int y, Bitmap bitmap) { 436 int tileSize = getTileSize(); 437 if (!REUSE_BITMAP) { 438 return getTileWithoutReusingBitmap(level, x, y, tileSize); 439 } 440 441 int t = tileSize << level; 442 mWantRegion.set(x, y, x + t, y + t); 443 444 if (bitmap == null) { 445 bitmap = Bitmap.createBitmap(tileSize, tileSize, Bitmap.Config.ARGB_8888); 446 } 447 448 mOptions.inSampleSize = (1 << level); 449 mOptions.inBitmap = bitmap; 450 451 try { 452 bitmap = mDecoder.decodeRegion(mWantRegion, mOptions); 453 } finally { 454 if (mOptions.inBitmap != bitmap && mOptions.inBitmap != null) { 455 mOptions.inBitmap = null; 456 } 457 } 458 459 if (bitmap == null) { 460 Log.w("BitmapRegionTileSource", "fail in decoding region"); 461 } 462 return bitmap; 463 } 464 getTileWithoutReusingBitmap( int level, int x, int y, int tileSize)465 private Bitmap getTileWithoutReusingBitmap( 466 int level, int x, int y, int tileSize) { 467 468 int t = tileSize << level; 469 mWantRegion.set(x, y, x + t, y + t); 470 471 mOverlapRegion.set(0, 0, mWidth, mHeight); 472 473 mOptions.inSampleSize = (1 << level); 474 Bitmap bitmap = mDecoder.decodeRegion(mOverlapRegion, mOptions); 475 476 if (bitmap == null) { 477 Log.w(TAG, "fail in decoding region"); 478 } 479 480 if (mWantRegion.equals(mOverlapRegion)) { 481 return bitmap; 482 } 483 484 Bitmap result = Bitmap.createBitmap(tileSize, tileSize, Config.ARGB_8888); 485 if (mCanvas == null) { 486 mCanvas = new Canvas(); 487 } 488 mCanvas.setBitmap(result); 489 mCanvas.drawBitmap(bitmap, 490 (mOverlapRegion.left - mWantRegion.left) >> level, 491 (mOverlapRegion.top - mWantRegion.top) >> level, null); 492 mCanvas.setBitmap(null); 493 return result; 494 } 495 496 /** 497 * Note that the returned bitmap may have a long edge that's longer 498 * than the targetSize, but it will always be less than 2x the targetSize 499 */ decodePreview(BitmapSource source, int targetSize)500 private Bitmap decodePreview(BitmapSource source, int targetSize) { 501 Bitmap result = source.getPreviewBitmap(); 502 if (result == null) { 503 return null; 504 } 505 506 // We need to resize down if the decoder does not support inSampleSize 507 // or didn't support the specified inSampleSize (some decoders only do powers of 2) 508 float scale = (float) targetSize / (float) (Math.max(result.getWidth(), result.getHeight())); 509 510 if (scale <= 0.5) { 511 result = BitmapUtils.resizeBitmapByScale(result, scale, true); 512 } 513 return ensureGLCompatibleBitmap(result); 514 } 515 ensureGLCompatibleBitmap(Bitmap bitmap)516 private static Bitmap ensureGLCompatibleBitmap(Bitmap bitmap) { 517 if (bitmap == null || bitmap.getConfig() != null) { 518 return bitmap; 519 } 520 Bitmap newBitmap = bitmap.copy(Config.ARGB_8888, false); 521 bitmap.recycle(); 522 return newBitmap; 523 } 524 } 525