content: /*
   * Copyright (C) 2013 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package com.android.photos;
  
  import android.annotation.TargetApi;
  import android.content.Context;
  import android.content.res.Resources;
  import android.graphics.Bitmap;
  import android.graphics.Bitmap.Config;
  import android.graphics.BitmapFactory;
  import android.graphics.BitmapRegionDecoder;
  import android.graphics.Canvas;
  import android.graphics.Paint;
  import android.graphics.Rect;
  import android.net.Uri;
  import android.os.Build;
  import android.os.Build.VERSION_CODES;
  import android.util.Log;
  
  import com.android.gallery3d.common.BitmapUtils;
  import com.android.gallery3d.common.Utils;
  import com.android.gallery3d.exif.ExifInterface;
  import com.android.gallery3d.glrenderer.BasicTexture;
  import com.android.gallery3d.glrenderer.BitmapTexture;
  import com.android.photos.views.TiledImageRenderer;
  
  import java.io.BufferedInputStream;
  import java.io.FileNotFoundException;
  import java.io.IOException;
  import java.io.InputStream;
  
  interface SimpleBitmapRegionDecoder {
      int getWidth();
      int getHeight();
      Bitmap decodeRegion(Rect wantRegion, BitmapFactory.Options options);
  }
  
  class SimpleBitmapRegionDecoderWrapper implements SimpleBitmapRegionDecoder {
      BitmapRegionDecoder mDecoder;
      private SimpleBitmapRegionDecoderWrapper(BitmapRegionDecoder decoder) {
          mDecoder = decoder;
      }
      public static SimpleBitmapRegionDecoderWrapper newInstance(
              String pathName, boolean isShareable) {
          try {
              BitmapRegionDecoder d = BitmapRegionDecoder.newInstance(pathName, isShareable);
              if (d != null) {
                  return new SimpleBitmapRegionDecoderWrapper(d);
              }
          } catch (IOException e) {
              Log.w("BitmapRegionTileSource", "getting decoder failed for path " + pathName, e);
              return null;
          }
          return null;
      }
      public static SimpleBitmapRegionDecoderWrapper newInstance(
              InputStream is, boolean isShareable) {
          try {
              BitmapRegionDecoder d = BitmapRegionDecoder.newInstance(is, isShareable);
              if (d != null) {
                  return new SimpleBitmapRegionDecoderWrapper(d);
              }
          } catch (IOException e) {
              Log.w("BitmapRegionTileSource", "getting decoder failed", e);
              return null;
          }
          return null;
      }
      public int getWidth() {
          return mDecoder.getWidth();
      }
      public int getHeight() {
          return mDecoder.getHeight();
      }
      public Bitmap decodeRegion(Rect wantRegion, BitmapFactory.Options options) {
          return mDecoder.decodeRegion(wantRegion, options);
      }
  }
  
  class DumbBitmapRegionDecoder implements SimpleBitmapRegionDecoder {
      Bitmap mBuffer;
      Canvas mTempCanvas;
      Paint mTempPaint;
      private DumbBitmapRegionDecoder(Bitmap b) {
          mBuffer = b;
      }
      public static DumbBitmapRegionDecoder newInstance(String pathName) {
          Bitmap b = BitmapFactory.decodeFile(pathName);
          if (b != null) {
              return new DumbBitmapRegionDecoder(b);
          }
          return null;
      }
      public static DumbBitmapRegionDecoder newInstance(InputStream is) {
          Bitmap b = BitmapFactory.decodeStream(is);
          if (b != null) {
              return new DumbBitmapRegionDecoder(b);
          }
          return null;
      }
      public int getWidth() {
          return mBuffer.getWidth();
      }
      public int getHeight() {
          return mBuffer.getHeight();
      }
      public Bitmap decodeRegion(Rect wantRegion, BitmapFactory.Options options) {
          if (mTempCanvas == null) {
              mTempCanvas = new Canvas();
              mTempPaint = new Paint();
              mTempPaint.setFilterBitmap(true);
          }
          int sampleSize = Math.max(options.inSampleSize, 1);
          Bitmap newBitmap = Bitmap.createBitmap(
                  wantRegion.width() / sampleSize,
                  wantRegion.height() / sampleSize,
                  Bitmap.Config.ARGB_8888);
          mTempCanvas.setBitmap(newBitmap);
          mTempCanvas.save();
          mTempCanvas.scale(1f / sampleSize, 1f / sampleSize);
          mTempCanvas.drawBitmap(mBuffer, -wantRegion.left, -wantRegion.top, mTempPaint);
          mTempCanvas.restore();
          mTempCanvas.setBitmap(null);
          return newBitmap;
      }
  }
  
  /**
   * A {@link com.android.photos.views.TiledImageRenderer.TileSource} using
   * {@link BitmapRegionDecoder} to wrap a local file
   */
  @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1)
  public class BitmapRegionTileSource implements TiledImageRenderer.TileSource {
  
      private static final String TAG = "BitmapRegionTileSource";
  
      private static final boolean REUSE_BITMAP =
              Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN;
      private static final int GL_SIZE_LIMIT = 2048;
      // This must be no larger than half the size of the GL_SIZE_LIMIT
      // due to decodePreview being allowed to be up to 2x the size of the target
      public static final int MAX_PREVIEW_SIZE = GL_SIZE_LIMIT / 2;
  
      public static abstract class BitmapSource {
          private SimpleBitmapRegionDecoder mDecoder;
          private Bitmap mPreview;
          private int mPreviewSize;
          private int mRotation;
          public enum State { NOT_LOADED, LOADED, ERROR_LOADING };
          private State mState = State.NOT_LOADED;
          public BitmapSource(int previewSize) {
              mPreviewSize = previewSize;
          }
          public boolean loadInBackground() {
              ExifInterface ei = new ExifInterface();
              if (readExif(ei)) {
                  Integer ori = ei.getTagIntValue(ExifInterface.TAG_ORIENTATION);
                  if (ori != null) {
                      mRotation = ExifInterface.getRotationForOrientationValue(ori.shortValue());
                  }
              }
              mDecoder = loadBitmapRegionDecoder();
              if (mDecoder == null) {
                  mState = State.ERROR_LOADING;
                  return false;
              } else {
                  int width = mDecoder.getWidth();
                  int height = mDecoder.getHeight();
                  if (mPreviewSize != 0) {
                      int previewSize = Math.min(mPreviewSize, MAX_PREVIEW_SIZE);
                      BitmapFactory.Options opts = new BitmapFactory.Options();
                      opts.inPreferredConfig = Bitmap.Config.ARGB_8888;
                      opts.inPreferQualityOverSpeed = true;
  
                      float scale = (float) previewSize / Math.max(width, height);
                      opts.inSampleSize = BitmapUtils.computeSampleSizeLarger(scale);
                      opts.inJustDecodeBounds = false;
                      mPreview = loadPreviewBitmap(opts);
                  }
                  mState = State.LOADED;
                  return true;
              }
          }
  
          public State getLoadingState() {
              return mState;
          }
  
          public SimpleBitmapRegionDecoder getBitmapRegionDecoder() {
              return mDecoder;
          }
  
          public Bitmap getPreviewBitmap() {
              return mPreview;
          }
  
          public int getPreviewSize() {
              return mPreviewSize;
          }
  
          public int getRotation() {
              return mRotation;
          }
  
          public abstract boolean readExif(ExifInterface ei);
          public abstract SimpleBitmapRegionDecoder loadBitmapRegionDecoder();
          public abstract Bitmap loadPreviewBitmap(BitmapFactory.Options options);
      }
  
      public static class FilePathBitmapSource extends BitmapSource {
          private String mPath;
          public FilePathBitmapSource(String path, int previewSize) {
              super(previewSize);
              mPath = path;
          }
          @Override
          public SimpleBitmapRegionDecoder loadBitmapRegionDecoder() {
              SimpleBitmapRegionDecoder d;
              d = SimpleBitmapRegionDecoderWrapper.newInstance(mPath, true);
              if (d == null) {
                  d = DumbBitmapRegionDecoder.newInstance(mPath);
              }
              return d;
          }
          @Override
          public Bitmap loadPreviewBitmap(BitmapFactory.Options options) {
              return BitmapFactory.decodeFile(mPath, options);
          }
          @Override
          public boolean readExif(ExifInterface ei) {
              try {
                  ei.readExif(mPath);
                  return true;
              } catch (NullPointerException e) {
                  Log.w("BitmapRegionTileSource", "reading exif failed", e);
                  return false;
              } catch (IOException e) {
                  Log.w("BitmapRegionTileSource", "getting decoder failed", e);
                  return false;
              }
          }
      }
  
      public static class UriBitmapSource extends BitmapSource {
          private Context mContext;
          private Uri mUri;
          public UriBitmapSource(Context context, Uri uri, int previewSize) {
              super(previewSize);
              mContext = context;
              mUri = uri;
          }
          private InputStream regenerateInputStream() throws FileNotFoundException {
              InputStream is = mContext.getContentResolver().openInputStream(mUri);
              return new BufferedInputStream(is);
          }
          @Override
          public SimpleBitmapRegionDecoder loadBitmapRegionDecoder() {
              try {
                  InputStream is = regenerateInputStream();
                  SimpleBitmapRegionDecoder regionDecoder =
                          SimpleBitmapRegionDecoderWrapper.newInstance(is, false);
                  Utils.closeSilently(is);
                  if (regionDecoder == null) {
                      is = regenerateInputStream();
                      regionDecoder = DumbBitmapRegionDecoder.newInstance(is);
                      Utils.closeSilently(is);
                  }
                  return regionDecoder;
              } catch (FileNotFoundException e) {
                  Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e);
                  return null;
              }
          }
          @Override
          public Bitmap loadPreviewBitmap(BitmapFactory.Options options) {
              try {
                  InputStream is = regenerateInputStream();
                  Bitmap b = BitmapFactory.decodeStream(is, null, options);
                  Utils.closeSilently(is);
                  return b;
              } catch (FileNotFoundException e) {
                  Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e);
                  return null;
              }
          }
          @Override
          public boolean readExif(ExifInterface ei) {
              InputStream is = null;
              try {
                  is = regenerateInputStream();
                  ei.readExif(is);
                  Utils.closeSilently(is);
                  return true;
              } catch (FileNotFoundException e) {
                  Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e);
                  return false;
              } catch (IOException e) {
                  Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e);
                  return false;
              } catch (NullPointerException e) {
                  Log.e("BitmapRegionTileSource", "Failed to read EXIF for URI " + mUri, e);
                  return false;
              } finally {
                  Utils.closeSilently(is);
              }
          }
      }
  
      public static class ResourceBitmapSource extends BitmapSource {
          private Resources mRes;
          private int mResId;
          public ResourceBitmapSource(Resources res, int resId, int previewSize) {
              super(previewSize);
              mRes = res;
              mResId = resId;
          }
          private InputStream regenerateInputStream() {
              InputStream is = mRes.openRawResource(mResId);
              return new BufferedInputStream(is);
          }
          @Override
          public SimpleBitmapRegionDecoder loadBitmapRegionDecoder() {
              InputStream is = regenerateInputStream();
              SimpleBitmapRegionDecoder regionDecoder =
                      SimpleBitmapRegionDecoderWrapper.newInstance(is, false);
              Utils.closeSilently(is);
              if (regionDecoder == null) {
                  is = regenerateInputStream();
                  regionDecoder = DumbBitmapRegionDecoder.newInstance(is);
                  Utils.closeSilently(is);
              }
              return regionDecoder;
          }
          @Override
          public Bitmap loadPreviewBitmap(BitmapFactory.Options options) {
              return BitmapFactory.decodeResource(mRes, mResId, options);
          }
          @Override
          public boolean readExif(ExifInterface ei) {
              try {
                  InputStream is = regenerateInputStream();
                  ei.readExif(is);
                  Utils.closeSilently(is);
                  return true;
              } catch (IOException e) {
                  Log.e("BitmapRegionTileSource", "Error reading resource", e);
                  return false;
              }
          }
      }
  
      SimpleBitmapRegionDecoder mDecoder;
      int mWidth;
      int mHeight;
      int mTileSize;
      private BasicTexture mPreview;
      private final int mRotation;
  
      // For use only by getTile
      private Rect mWantRegion = new Rect();
      private Rect mOverlapRegion = new Rect();
      private BitmapFactory.Options mOptions;
      private Canvas mCanvas;
  
      public BitmapRegionTileSource(Context context, BitmapSource source) {
          mTileSize = TiledImageRenderer.suggestedTileSize(context);
          mRotation = source.getRotation();
          mDecoder = source.getBitmapRegionDecoder();
          if (mDecoder != null) {
              mWidth = mDecoder.getWidth();
              mHeight = mDecoder.getHeight();
              mOptions = new BitmapFactory.Options();
              mOptions.inPreferredConfig = Bitmap.Config.ARGB_8888;
              mOptions.inPreferQualityOverSpeed = true;
              mOptions.inTempStorage = new byte[16 * 1024];
              int previewSize = source.getPreviewSize();
              if (previewSize != 0) {
                  previewSize = Math.min(previewSize, MAX_PREVIEW_SIZE);
                  // Although this is the same size as the Bitmap that is likely already
                  // loaded, the lifecycle is different and interactions are on a different
                  // thread. Thus to simplify, this source will decode its own bitmap.
                  Bitmap preview = decodePreview(source, previewSize);
                  if (preview.getWidth() <= GL_SIZE_LIMIT && preview.getHeight() <= GL_SIZE_LIMIT) {
                      mPreview = new BitmapTexture(preview);
                  } else {
                      Log.w(TAG, String.format(
                              "Failed to create preview of apropriate size! "
                              + " in: %dx%d, out: %dx%d",
                              mWidth, mHeight,
                              preview.getWidth(), preview.getHeight()));
                  }
              }
          }
      }
  
      @Override
      public int getTileSize() {
          return mTileSize;
      }
  
      @Override
      public int getImageWidth() {
          return mWidth;
      }
  
      @Override
      public int getImageHeight() {
          return mHeight;
      }
  
      @Override
      public BasicTexture getPreview() {
          return mPreview;
      }
  
      @Override
      public int getRotation() {
          return mRotation;
      }
  
      @Override
      public Bitmap getTile(int level, int x, int y, Bitmap bitmap) {
          int tileSize = getTileSize();
          if (!REUSE_BITMAP) {
              return getTileWithoutReusingBitmap(level, x, y, tileSize);
          }
  
          int t = tileSize << level;
          mWantRegion.set(x, y, x + t, y + t);
  
          if (bitmap == null) {
              bitmap = Bitmap.createBitmap(tileSize, tileSize, Bitmap.Config.ARGB_8888);
          }
  
          mOptions.inSampleSize = (1 << level);
          mOptions.inBitmap = bitmap;
  
          try {
              bitmap = mDecoder.decodeRegion(mWantRegion, mOptions);
          } finally {
              if (mOptions.inBitmap != bitmap && mOptions.inBitmap != null) {
                  mOptions.inBitmap = null;
              }
          }
  
          if (bitmap == null) {
              Log.w("BitmapRegionTileSource", "fail in decoding region");
          }
          return bitmap;
      }
  
      private Bitmap getTileWithoutReusingBitmap(
              int level, int x, int y, int tileSize) {
  
          int t = tileSize << level;
          mWantRegion.set(x, y, x + t, y + t);
  
          mOverlapRegion.set(0, 0, mWidth, mHeight);
  
          mOptions.inSampleSize = (1 << level);
          Bitmap bitmap = mDecoder.decodeRegion(mOverlapRegion, mOptions);
  
          if (bitmap == null) {
              Log.w(TAG, "fail in decoding region");
          }
  
          if (mWantRegion.equals(mOverlapRegion)) {
              return bitmap;
          }
  
          Bitmap result = Bitmap.createBitmap(tileSize, tileSize, Config.ARGB_8888);
          if (mCanvas == null) {
              mCanvas = new Canvas();
          }
          mCanvas.setBitmap(result);
          mCanvas.drawBitmap(bitmap,
                  (mOverlapRegion.left - mWantRegion.left) >> level,
                  (mOverlapRegion.top - mWantRegion.top) >> level, null);
          mCanvas.setBitmap(null);
          return result;
      }
  
      /**
       * Note that the returned bitmap may have a long edge that's longer
       * than the targetSize, but it will always be less than 2x the targetSize
       */
      private Bitmap decodePreview(BitmapSource source, int targetSize) {
          Bitmap result = source.getPreviewBitmap();
          if (result == null) {
              return null;
          }
  
          // We need to resize down if the decoder does not support inSampleSize
          // or didn't support the specified inSampleSize (some decoders only do powers of 2)
          float scale = (float) targetSize / (float) (Math.max(result.getWidth(), result.getHeight()));
  
          if (scale <= 0.5) {
              result = BitmapUtils.resizeBitmapByScale(result, scale, true);
          }
          return ensureGLCompatibleBitmap(result);
      }
  
      private static Bitmap ensureGLCompatibleBitmap(Bitmap bitmap) {
          if (bitmap == null || bitmap.getConfig() != null) {
              return bitmap;
          }
          Bitmap newBitmap = bitmap.copy(Config.ARGB_8888, false);
          bitmap.recycle();
          return newBitmap;
      }
  }
  