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.bitmap;
  
  import android.graphics.Bitmap;
  import android.graphics.BitmapFactory;
  import android.graphics.BitmapRegionDecoder;
  import android.graphics.Rect;
  import android.os.AsyncTask;
  import android.os.ParcelFileDescriptor;
  import android.os.ParcelFileDescriptor.AutoCloseInputStream;
  import android.util.Log;
  
  import com.android.bitmap.RequestKey.FileDescriptorFactory;
  import com.android.bitmap.util.BitmapUtils;
  import com.android.bitmap.util.Exif;
  import com.android.bitmap.util.RectUtils;
  import com.android.bitmap.util.Trace;
  
  import java.io.IOException;
  import java.io.InputStream;
  
  /**
   * Decodes an image from either a file descriptor or input stream on a worker thread. After the
   * decode is complete, even if the task is cancelled, the result is placed in the given cache.
   * A {@link DecodeCallback} client may be notified on decode begin and completion.
   * <p>
   * This class uses {@link BitmapRegionDecoder} when possible to minimize unnecessary decoding
   * and allow bitmap reuse on Jellybean 4.1 and later.
   * <p>
   *  GIFs are supported, but their decode does not reuse bitmaps at all. The resulting
   *  {@link ReusableBitmap} will be marked as not reusable
   *  ({@link ReusableBitmap#isEligibleForPooling()} will return false).
   */
  public class DecodeTask extends AsyncTask<Void, Void, ReusableBitmap> {
  
      private final RequestKey mKey;
      private final DecodeOptions mDecodeOpts;
      private final FileDescriptorFactory mFactory;
      private final DecodeCallback mDecodeCallback;
      private final BitmapCache mCache;
      private final BitmapFactory.Options mOpts = new BitmapFactory.Options();
  
      private ReusableBitmap mInBitmap = null;
  
      private static final boolean CROP_DURING_DECODE = true;
  
      private static final String TAG = DecodeTask.class.getSimpleName();
      public static final boolean DEBUG = false;
  
      /**
       * Callback interface for clients to be notified of decode state changes and completion.
       */
      public interface DecodeCallback {
          /**
           * Notifies that the async task's work is about to begin. Up until this point, the task
           * may have been preempted by the scheduler or queued up by a bottlenecked executor.
           * <p>
           * N.B. this method runs on the UI thread.
           */
          void onDecodeBegin(RequestKey key);
          /**
           * The task is now complete and the ReusableBitmap is available for use. Clients should
           * double check that the request matches what the client is expecting.
           */
          void onDecodeComplete(RequestKey key, ReusableBitmap result);
          /**
           * The task has been canceled, and {@link #onDecodeComplete(RequestKey, ReusableBitmap)}
           * will not be called.
           */
          void onDecodeCancel(RequestKey key);
      }
  
      /**
     * Create new DecodeTask.
     *
     * @param requestKey The request to decode, also the key to use for the cache.
     * @param decodeOpts The decode options.
     * @param factory    The factory to obtain file descriptors to decode from. If this factory is
       *                 null, then we will decode from requestKey.createInputStream().
     * @param callback   The callback to notify of decode state changes.
     * @param cache      The cache and pool.
     */
      public DecodeTask(RequestKey requestKey, DecodeOptions decodeOpts,
              FileDescriptorFactory factory, DecodeCallback callback, BitmapCache cache) {
          mKey = requestKey;
          mDecodeOpts = decodeOpts;
          mFactory = factory;
          mDecodeCallback = callback;
          mCache = cache;
      }
  
      @Override
      protected ReusableBitmap doInBackground(Void... params) {
          // enqueue the 'onDecodeBegin' signal on the main thread
          publishProgress();
  
          return decode();
      }
  
      public ReusableBitmap decode() {
          if (isCancelled()) {
              return null;
          }
  
          ReusableBitmap result = null;
          ParcelFileDescriptor fd = null;
          InputStream in = null;
  
          try {
              if (mFactory != null) {
                  Trace.beginSection("create fd");
                  fd = mFactory.createFileDescriptor();
                  Trace.endSection();
              } else {
                  in = reset(in);
                  if (in == null) {
                      return null;
                  }
                  if (isCancelled()) {
                      return null;
                  }
              }
  
              final boolean isJellyBeanOrAbove = android.os.Build.VERSION.SDK_INT
                      >= android.os.Build.VERSION_CODES.JELLY_BEAN;
              // This blocks during fling when the pool is empty. We block early to avoid jank.
              if (isJellyBeanOrAbove) {
                  Trace.beginSection("poll for reusable bitmap");
                  mInBitmap = mCache.poll();
                  Trace.endSection();
              }
  
              if (isCancelled()) {
                  return null;
              }
  
              Trace.beginSection("get bytesize");
              final long byteSize;
              if (fd != null) {
                  byteSize = fd.getStatSize();
              } else {
                  byteSize = -1;
              }
              Trace.endSection();
  
              Trace.beginSection("get orientation");
              final int orientation;
              if (mKey.hasOrientationExif()) {
                  if (fd != null) {
                      // Creating an input stream from the file descriptor makes it useless
                      // afterwards.
                      Trace.beginSection("create orientation fd and stream");
                      final ParcelFileDescriptor orientationFd = mFactory.createFileDescriptor();
                      in = new AutoCloseInputStream(orientationFd);
                      Trace.endSection();
                  }
                  orientation = Exif.getOrientation(in, byteSize);
                  if (fd != null) {
                      try {
                          // Close the temporary file descriptor.
                          in.close();
                      } catch (IOException ignored) {
                      }
                  }
              } else {
                  orientation = 0;
              }
              final boolean isNotRotatedOr180 = orientation == 0 || orientation == 180;
              Trace.endSection();
  
              if (orientation != 0) {
                  // disable inBitmap-- bitmap reuse doesn't work with different decode regions due
                  // to orientation
                  if (mInBitmap != null) {
                      mCache.offer(mInBitmap);
                      mInBitmap = null;
                      mOpts.inBitmap = null;
                  }
              }
  
              if (isCancelled()) {
                  return null;
              }
  
              if (fd == null) {
                  in = reset(in);
                  if (in == null) {
                      return null;
                  }
                  if (isCancelled()) {
                      return null;
                  }
              }
  
              Trace.beginSection("decodeBounds");
              mOpts.inJustDecodeBounds = true;
              if (fd != null) {
                  BitmapFactory.decodeFileDescriptor(fd.getFileDescriptor(), null, mOpts);
              } else {
                  BitmapFactory.decodeStream(in, null, mOpts);
              }
              Trace.endSection();
  
              if (isCancelled()) {
                  return null;
              }
  
              // We want to calculate the sample size "as if" the orientation has been corrected.
              final int srcW, srcH; // Orientation corrected.
              if (isNotRotatedOr180) {
                  srcW = mOpts.outWidth;
                  srcH = mOpts.outHeight;
              } else {
                  srcW = mOpts.outHeight;
                  srcH = mOpts.outWidth;
              }
  
              // BEGIN MANUAL-INLINE calculateSampleSize()
  
              final float sz = Math
                      .min((float) srcW / mDecodeOpts.destW, (float) srcH / mDecodeOpts.destH);
  
              final int sampleSize;
              switch (mDecodeOpts.sampleSizeStrategy) {
                  case DecodeOptions.STRATEGY_TRUNCATE:
                      sampleSize = (int) sz;
                      break;
                  case DecodeOptions.STRATEGY_ROUND_UP:
                      sampleSize = (int) Math.ceil(sz);
                      break;
                  case DecodeOptions.STRATEGY_ROUND_NEAREST:
                  default:
                      sampleSize = (int) Math.pow(2, (int) (0.5 + (Math.log(sz) / Math.log(2))));
                      break;
              }
              mOpts.inSampleSize = Math.max(1, sampleSize);
  
              // END MANUAL-INLINE calculateSampleSize()
  
              mOpts.inJustDecodeBounds = false;
              mOpts.inMutable = true;
              if (isJellyBeanOrAbove && orientation == 0) {
                  if (mInBitmap == null) {
                      if (DEBUG) {
                          Log.e(TAG, "decode thread wants a bitmap. cache dump:\n"
                                  + mCache.toDebugString());
                      }
                      Trace.beginSection("create reusable bitmap");
                      mInBitmap = new ReusableBitmap(
                              Bitmap.createBitmap(mDecodeOpts.destW, mDecodeOpts.destH,
                                      Bitmap.Config.ARGB_8888));
                      Trace.endSection();
  
                      if (isCancelled()) {
                          return null;
                      }
  
                      if (DEBUG) {
                          Log.e(TAG, "*** allocated new bitmap in decode thread: "
                                  + mInBitmap + " key=" + mKey);
                      }
                  } else {
                      if (DEBUG) {
                          Log.e(TAG, "*** reusing existing bitmap in decode thread: "
                                  + mInBitmap + " key=" + mKey);
                      }
  
                  }
                  mOpts.inBitmap = mInBitmap.bmp;
              }
  
              if (isCancelled()) {
                  return null;
              }
  
              if (fd == null) {
                  in = reset(in);
                  if (in == null) {
                      return null;
                  }
                  if (isCancelled()) {
                      return null;
                  }
              }
  
  
              Bitmap decodeResult = null;
              final Rect srcRect = new Rect(); // Not orientation corrected. True coordinates.
              if (CROP_DURING_DECODE) {
                  try {
                      Trace.beginSection("decodeCropped" + mOpts.inSampleSize);
  
                      // BEGIN MANUAL INLINE decodeCropped()
  
                      final BitmapRegionDecoder brd;
                      if (fd != null) {
                          brd = BitmapRegionDecoder
                                  .newInstance(fd.getFileDescriptor(), true /* shareable */);
                      } else {
                          brd = BitmapRegionDecoder.newInstance(in, true /* shareable */);
                      }
  
                      final Bitmap bitmap;
                      if (isCancelled()) {
                          bitmap = null;
                      } else {
                          // We want to call calculateCroppedSrcRect() on the source rectangle "as
                          // if" the orientation has been corrected.
                          // Center the decode on the top 1/3.
                          BitmapUtils.calculateCroppedSrcRect(srcW, srcH, mDecodeOpts.destW,
                                  mDecodeOpts.destH, mDecodeOpts.destH, mOpts.inSampleSize,
                                  mDecodeOpts.horizontalCenter, mDecodeOpts.verticalCenter,
                                  true /* absoluteFraction */,
                                  1f, srcRect);
                          if (DEBUG) {
                              System.out.println("rect for this decode is: " + srcRect
                                      + " srcW/H=" + srcW + "/" + srcH
                                      + " dstW/H=" + mDecodeOpts.destW + "/" + mDecodeOpts.destH);
                          }
  
                          // calculateCroppedSrcRect() gave us the source rectangle "as if" the
                          // orientation has been corrected. We need to decode the uncorrected
                          // source rectangle. Calculate true coordinates.
                          RectUtils.rotateRectForOrientation(orientation, new Rect(0, 0, srcW, srcH),
                                  srcRect);
  
                          bitmap = brd.decodeRegion(srcRect, mOpts);
                      }
                      brd.recycle();
  
                      // END MANUAL INLINE decodeCropped()
  
                      decodeResult = bitmap;
                  } catch (IOException e) {
                      // fall through to below and try again with the non-cropping decoder
                      if (fd == null) {
                          in = reset(in);
                          if (in == null) {
                              return null;
                          }
                          if (isCancelled()) {
                              return null;
                          }
                      }
  
                      e.printStackTrace();
                  } finally {
                      Trace.endSection();
                  }
  
                  if (isCancelled()) {
                      return null;
                  }
              }
  
              //noinspection PointlessBooleanExpression
              if (!CROP_DURING_DECODE || (decodeResult == null && !isCancelled())) {
                  try {
                      Trace.beginSection("decode" + mOpts.inSampleSize);
                      // disable inBitmap-- bitmap reuse doesn't work well below K
                      if (mInBitmap != null) {
                          mCache.offer(mInBitmap);
                          mInBitmap = null;
                          mOpts.inBitmap = null;
                      }
                      decodeResult = decode(fd, in);
                  } catch (IllegalArgumentException e) {
                      Log.e(TAG, "decode failed: reason='" + e.getMessage() + "' ss="
                              + mOpts.inSampleSize);
  
                      if (mOpts.inSampleSize > 1) {
                          // try again with ss=1
                          mOpts.inSampleSize = 1;
                          decodeResult = decode(fd, in);
                      }
                  } finally {
                      Trace.endSection();
                  }
  
                  if (isCancelled()) {
                      return null;
                  }
              }
  
              if (decodeResult == null) {
                  return null;
              }
  
              if (mInBitmap != null) {
                  result = mInBitmap;
                  // srcRect is non-empty when using the cropping BitmapRegionDecoder codepath
                  if (!srcRect.isEmpty()) {
                      result.setLogicalWidth((srcRect.right - srcRect.left) / mOpts.inSampleSize);
                      result.setLogicalHeight(
                              (srcRect.bottom - srcRect.top) / mOpts.inSampleSize);
                  } else {
                      result.setLogicalWidth(mOpts.outWidth);
                      result.setLogicalHeight(mOpts.outHeight);
                  }
              } else {
                  // no mInBitmap means no pooling
                  result = new ReusableBitmap(decodeResult, false /* reusable */);
                  if (isNotRotatedOr180) {
                      result.setLogicalWidth(decodeResult.getWidth());
                      result.setLogicalHeight(decodeResult.getHeight());
                  } else {
                      result.setLogicalWidth(decodeResult.getHeight());
                      result.setLogicalHeight(decodeResult.getWidth());
                  }
              }
              result.setOrientation(orientation);
          } catch (Exception e) {
              e.printStackTrace();
          } finally {
              if (fd != null) {
                  try {
                      fd.close();
                  } catch (IOException ignored) {
                  }
              }
              if (in != null) {
                  try {
                      in.close();
                  } catch (IOException ignored) {
                  }
              }
  
              // Cancellations can't be guaranteed to be correct, so skip the cache
              if (!isCancelled()) {
                  // Put result in cache, regardless of null. The cache will handle null results.
                  mCache.put(mKey, result);
              }
              if (result != null) {
                  result.acquireReference();
                  if (DEBUG) {
                      Log.d(TAG, "placed result in cache: key=" + mKey + " bmp="
                          + result + " cancelled=" + isCancelled());
                  }
              } else if (mInBitmap != null) {
                  if (DEBUG) {
                      Log.d(TAG, "placing failed/cancelled bitmap in pool: key="
                          + mKey + " bmp=" + mInBitmap);
                  }
                  mCache.offer(mInBitmap);
              }
          }
          return result;
      }
  
      /**
       * Return an input stream that can be read from the beginning using the most efficient way,
       * given an input stream that may or may not support reset(), or given null.
       *
       * The returned input stream may or may not be the same stream.
       */
      private InputStream reset(InputStream in) throws IOException {
          Trace.beginSection("create stream");
          if (in == null) {
              in = mKey.createInputStream();
          } else if (in.markSupported()) {
              in.reset();
          } else {
              try {
                  in.close();
              } catch (IOException ignored) {
              }
              in = mKey.createInputStream();
          }
          Trace.endSection();
          return in;
      }
  
      private Bitmap decode(ParcelFileDescriptor fd, InputStream in) {
          final Bitmap result;
          if (fd != null) {
              result = BitmapFactory.decodeFileDescriptor(fd.getFileDescriptor(), null, mOpts);
          } else {
              result = BitmapFactory.decodeStream(in, null, mOpts);
          }
          return result;
      }
  
      public void cancel() {
          cancel(true);
          mOpts.requestCancelDecode();
      }
  
      @Override
      protected void onProgressUpdate(Void... values) {
          mDecodeCallback.onDecodeBegin(mKey);
      }
  
      @Override
      public void onPostExecute(ReusableBitmap result) {
          mDecodeCallback.onDecodeComplete(mKey, result);
      }
  
      @Override
      protected void onCancelled(ReusableBitmap result) {
          mDecodeCallback.onDecodeCancel(mKey);
          if (result == null) {
              return;
          }
  
          result.releaseReference();
          if (mInBitmap == null) {
              // not reusing bitmaps: can recycle immediately
              result.bmp.recycle();
          }
      }
  
      /**
       * Parameters to pass to the DecodeTask.
       */
      public static class DecodeOptions {
  
          /**
           * Round sample size to the nearest power of 2. Depending on the source and destination
           * dimensions, we will either truncate, in which case we decode from a bigger region and
           * crop down, or we will round up, in which case we decode from a smaller region and scale
           * up.
           */
          public static final int STRATEGY_ROUND_NEAREST = 0;
          /**
           * Always decode from a bigger region and crop down.
           */
          public static final int STRATEGY_TRUNCATE = 1;
  
          /**
           * Always decode from a smaller region and scale up.
           */
          public static final int STRATEGY_ROUND_UP = 2;
  
          /**
           * The destination width to decode to.
           */
          public int destW;
          /**
           * The destination height to decode to.
           */
          public int destH;
          /**
           * If the destination dimensions are smaller than the source image provided by the request
           * key, this will determine where horizontally the destination rect will be cropped from.
           * Value from 0f for left-most crop to 1f for right-most crop.
           */
          public float horizontalCenter;
          /**
           * If the destination dimensions are smaller than the source image provided by the request
           * key, this will determine where vertically the destination rect will be cropped from.
           * Value from 0f for top-most crop to 1f for bottom-most crop.
           */
          public float verticalCenter;
          /**
           * One of the STRATEGY constants.
           */
          public int sampleSizeStrategy;
  
          public DecodeOptions(final int destW, final int destH) {
              this(destW, destH, 0.5f, 0.5f, STRATEGY_ROUND_NEAREST);
          }
  
          /**
           * Create new DecodeOptions with horizontally-centered cropping if applicable.
           * @param destW The destination width to decode to.
           * @param destH The destination height to decode to.
           * @param verticalCenter If the destination dimensions are smaller than the source image
           *                       provided by the request key, this will determine where vertically
           *                       the destination rect will be cropped from.
           * @param sampleSizeStrategy One of the STRATEGY constants.
           */
          public DecodeOptions(final int destW, final int destH,
                  final float verticalCenter, final int sampleSizeStrategy) {
              this(destW, destH, 0.5f, verticalCenter, sampleSizeStrategy);
          }
  
          /**
           * Create new DecodeOptions.
           * @param destW The destination width to decode to.
           * @param destH The destination height to decode to.
           * @param horizontalCenter If the destination dimensions are smaller than the source image
           *                         provided by the request key, this will determine where
           *                         horizontally the destination rect will be cropped from.
           * @param verticalCenter If the destination dimensions are smaller than the source image
           *                       provided by the request key, this will determine where vertically
           *                       the destination rect will be cropped from.
           * @param sampleSizeStrategy One of the STRATEGY constants.
           */
          public DecodeOptions(final int destW, final int destH, final float horizontalCenter,
                  final float verticalCenter, final int sampleSizeStrategy) {
              this.destW = destW;
              this.destH = destH;
              this.horizontalCenter = horizontalCenter;
              this.verticalCenter = verticalCenter;
              this.sampleSizeStrategy = sampleSizeStrategy;
          }
      }
  }
  