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