content: /*
   * Copyright (C) 2018 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 android.graphics.drawable;
  
  import android.annotation.IntRange;
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.annotation.UnsupportedAppUsage;
  import android.content.res.AssetFileDescriptor;
  import android.content.res.Resources;
  import android.content.res.Resources.Theme;
  import android.content.res.TypedArray;
  import android.graphics.Bitmap;
  import android.graphics.Canvas;
  import android.graphics.ColorFilter;
  import android.graphics.ImageDecoder;
  import android.graphics.PixelFormat;
  import android.graphics.Rect;
  import android.os.Handler;
  import android.os.Looper;
  import android.os.SystemClock;
  import android.util.AttributeSet;
  import android.util.DisplayMetrics;
  import android.util.TypedValue;
  import android.view.View;
  
  import com.android.internal.R;
  
  import dalvik.annotation.optimization.FastNative;
  
  import libcore.util.NativeAllocationRegistry;
  
  import org.xmlpull.v1.XmlPullParser;
  import org.xmlpull.v1.XmlPullParserException;
  
  import java.io.IOException;
  import java.io.InputStream;
  import java.util.ArrayList;
  
  /**
   * {@link Drawable} for drawing animated images (like GIF).
   *
   * <p>The framework handles decoding subsequent frames in another thread and
   * updating when necessary. The drawable will only animate while it is being
   * displayed.</p>
   *
   * <p>Created by {@link ImageDecoder#decodeDrawable}. A user needs to call
   * {@link #start} to start the animation.</p>
   *
   * <p>It can also be defined in XML using the <code>&lt;animated-image></code>
   * element.</p>
   *
   * @attr ref android.R.styleable#AnimatedImageDrawable_src
   * @attr ref android.R.styleable#AnimatedImageDrawable_autoStart
   * @attr ref android.R.styleable#AnimatedImageDrawable_repeatCount
   * @attr ref android.R.styleable#AnimatedImageDrawable_autoMirrored
   */
  public class AnimatedImageDrawable extends Drawable implements Animatable2 {
      private int mIntrinsicWidth;
      private int mIntrinsicHeight;
  
      private boolean mStarting;
  
      private Handler mHandler;
  
      private class State {
          State(long nativePtr, InputStream is, AssetFileDescriptor afd) {
              mNativePtr = nativePtr;
              mInputStream = is;
              mAssetFd = afd;
          }
  
          final long mNativePtr;
  
          // These just keep references so the native code can continue using them.
          private final InputStream mInputStream;
          private final AssetFileDescriptor mAssetFd;
  
          int[] mThemeAttrs = null;
          boolean mAutoMirrored = false;
          int mRepeatCount = REPEAT_UNDEFINED;
      }
  
      private State mState;
  
      private Runnable mRunnable;
  
      private ColorFilter mColorFilter;
  
      /**
       *  Pass this to {@link #setRepeatCount} to repeat infinitely.
       *
       *  <p>{@link Animatable2.AnimationCallback#onAnimationEnd} will never be
       *  called unless there is an error.</p>
       */
      public static final int REPEAT_INFINITE = -1;
  
      /** @removed
       * @deprecated Replaced with REPEAT_INFINITE to match other APIs.
       */
      @java.lang.Deprecated
      public static final int LOOP_INFINITE = REPEAT_INFINITE;
  
      private static final int REPEAT_UNDEFINED = -2;
  
      /**
       *  Specify the number of times to repeat the animation.
       *
       *  <p>By default, the repeat count in the encoded data is respected. If set
       *  to {@link #REPEAT_INFINITE}, the animation will repeat as long as it is
       *  displayed. If the value is {@code 0}, the animation will play once.</p>
       *
       *  <p>This call replaces the current repeat count. If the encoded data
       *  specified a repeat count of {@code 2} (meaning that
       *  {@link #getRepeatCount()} returns {@code 2}, the animation will play
       *  three times. Calling {@code setRepeatCount(1)} will result in playing only
       *  twice and {@link #getRepeatCount()} returning {@code 1}.</p>
       *
       *  <p>If the animation is already playing, the iterations that have already
       *  occurred count towards the new count. If the animation has already
       *  repeated the appropriate number of times (or more), it will finish its
       *  current iteration and then stop.</p>
       */
      public void setRepeatCount(@IntRange(from = REPEAT_INFINITE) int repeatCount) {
          if (repeatCount < REPEAT_INFINITE) {
              throw new IllegalArgumentException("invalid value passed to setRepeatCount"
                      + repeatCount);
          }
          if (mState.mRepeatCount != repeatCount) {
              mState.mRepeatCount = repeatCount;
              if (mState.mNativePtr != 0) {
                  nSetRepeatCount(mState.mNativePtr, repeatCount);
              }
          }
      }
  
      /** @removed
       * @deprecated Replaced with setRepeatCount to match other APIs.
       */
      @java.lang.Deprecated
      public void setLoopCount(int loopCount) {
          setRepeatCount(loopCount);
      }
  
      /**
       *  Retrieve the number of times the animation will repeat.
       *
       *  <p>By default, the repeat count in the encoded data is respected. If the
       *  value is {@link #REPEAT_INFINITE}, the animation will repeat as long as
       *  it is displayed. If the value is {@code 0}, it will play once.</p>
       *
       *  <p>Calling {@link #setRepeatCount} will make future calls to this method
       *  return the value passed to {@link #setRepeatCount}.</p>
       */
      public int getRepeatCount() {
          if (mState.mNativePtr == 0) {
              throw new IllegalStateException("called getRepeatCount on empty AnimatedImageDrawable");
          }
          if (mState.mRepeatCount == REPEAT_UNDEFINED) {
              mState.mRepeatCount = nGetRepeatCount(mState.mNativePtr);
  
          }
          return mState.mRepeatCount;
      }
  
      /** @removed
       * @deprecated Replaced with getRepeatCount to match other APIs.
       */
      @java.lang.Deprecated
      public int getLoopCount(int loopCount) {
          return getRepeatCount();
      }
  
      /**
       * Create an empty AnimatedImageDrawable.
       */
      public AnimatedImageDrawable() {
          mState = new State(0, null, null);
      }
  
      @Override
      public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)
              throws XmlPullParserException, IOException {
          super.inflate(r, parser, attrs, theme);
  
          final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.AnimatedImageDrawable);
          updateStateFromTypedArray(a, mSrcDensityOverride);
      }
  
      private void updateStateFromTypedArray(TypedArray a, int srcDensityOverride)
              throws XmlPullParserException {
          State oldState = mState;
          final Resources r = a.getResources();
          final int srcResId = a.getResourceId(R.styleable.AnimatedImageDrawable_src, 0);
          if (srcResId != 0) {
              // Follow the density handling in BitmapDrawable.
              final TypedValue value = new TypedValue();
              r.getValueForDensity(srcResId, srcDensityOverride, value, true);
              if (srcDensityOverride > 0 && value.density > 0
                      && value.density != TypedValue.DENSITY_NONE) {
                  if (value.density == srcDensityOverride) {
                      value.density = r.getDisplayMetrics().densityDpi;
                  } else {
                      value.density =
                              (value.density * r.getDisplayMetrics().densityDpi) / srcDensityOverride;
                  }
              }
  
              int density = Bitmap.DENSITY_NONE;
              if (value.density == TypedValue.DENSITY_DEFAULT) {
                  density = DisplayMetrics.DENSITY_DEFAULT;
              } else if (value.density != TypedValue.DENSITY_NONE) {
                  density = value.density;
              }
  
              Drawable drawable = null;
              try {
                  InputStream is = r.openRawResource(srcResId, value);
                  ImageDecoder.Source source = ImageDecoder.createSource(r, is, density);
                  drawable = ImageDecoder.decodeDrawable(source, (decoder, info, src) -> {
                      if (!info.isAnimated()) {
                          throw new IllegalArgumentException("image is not animated");
                      }
                  });
              } catch (IOException e) {
                  throw new XmlPullParserException(a.getPositionDescription() +
                          ": <animated-image> requires a valid 'src' attribute", null, e);
              }
  
              if (!(drawable instanceof AnimatedImageDrawable)) {
                  throw new XmlPullParserException(a.getPositionDescription() +
                          ": <animated-image> did not decode animated");
              }
  
              // This may have previously been set without a src if we were waiting for a
              // theme.
              final int repeatCount = mState.mRepeatCount;
              // Transfer the state of other to this one. other will be discarded.
              AnimatedImageDrawable other = (AnimatedImageDrawable) drawable;
              mState = other.mState;
              other.mState = null;
              mIntrinsicWidth =  other.mIntrinsicWidth;
              mIntrinsicHeight = other.mIntrinsicHeight;
              if (repeatCount != REPEAT_UNDEFINED) {
                  this.setRepeatCount(repeatCount);
              }
          }
  
          mState.mThemeAttrs = a.extractThemeAttrs();
          if (mState.mNativePtr == 0 && (mState.mThemeAttrs == null
                  || mState.mThemeAttrs[R.styleable.AnimatedImageDrawable_src] == 0)) {
              throw new XmlPullParserException(a.getPositionDescription() +
                      ": <animated-image> requires a valid 'src' attribute");
          }
  
          mState.mAutoMirrored = a.getBoolean(
                  R.styleable.AnimatedImageDrawable_autoMirrored, oldState.mAutoMirrored);
  
          int repeatCount = a.getInt(
                  R.styleable.AnimatedImageDrawable_repeatCount, REPEAT_UNDEFINED);
          if (repeatCount != REPEAT_UNDEFINED) {
              this.setRepeatCount(repeatCount);
          }
  
          boolean autoStart = a.getBoolean(
                  R.styleable.AnimatedImageDrawable_autoStart, false);
          if (autoStart && mState.mNativePtr != 0) {
              this.start();
          }
      }
  
      /**
       * @hide
       * This should only be called by ImageDecoder.
       *
       * decoder is only non-null if it has a PostProcess
       */
      public AnimatedImageDrawable(long nativeImageDecoder,
              @Nullable ImageDecoder decoder, int width, int height,
              long colorSpaceHandle, boolean extended, int srcDensity, int dstDensity,
              Rect cropRect, InputStream inputStream, AssetFileDescriptor afd)
              throws IOException {
          width = Bitmap.scaleFromDensity(width, srcDensity, dstDensity);
          height = Bitmap.scaleFromDensity(height, srcDensity, dstDensity);
  
          if (cropRect == null) {
              mIntrinsicWidth  = width;
              mIntrinsicHeight = height;
          } else {
              cropRect.set(Bitmap.scaleFromDensity(cropRect.left, srcDensity, dstDensity),
                      Bitmap.scaleFromDensity(cropRect.top, srcDensity, dstDensity),
                      Bitmap.scaleFromDensity(cropRect.right, srcDensity, dstDensity),
                      Bitmap.scaleFromDensity(cropRect.bottom, srcDensity, dstDensity));
              mIntrinsicWidth  = cropRect.width();
              mIntrinsicHeight = cropRect.height();
          }
  
          mState = new State(nCreate(nativeImageDecoder, decoder, width, height, colorSpaceHandle,
                      extended, cropRect), inputStream, afd);
  
          final long nativeSize = nNativeByteSize(mState.mNativePtr);
          NativeAllocationRegistry registry = NativeAllocationRegistry.createMalloced(
                  AnimatedImageDrawable.class.getClassLoader(), nGetNativeFinalizer(), nativeSize);
          registry.registerNativeAllocation(mState, mState.mNativePtr);
      }
  
      @Override
      public int getIntrinsicWidth() {
          return mIntrinsicWidth;
      }
  
      @Override
      public int getIntrinsicHeight() {
          return mIntrinsicHeight;
      }
  
      // nDraw returns -1 if the animation has finished.
      private static final int FINISHED = -1;
  
      @Override
      public void draw(@NonNull Canvas canvas) {
          if (mState.mNativePtr == 0) {
              throw new IllegalStateException("called draw on empty AnimatedImageDrawable");
          }
  
          if (mStarting) {
              mStarting = false;
  
              postOnAnimationStart();
          }
  
          long nextUpdate = nDraw(mState.mNativePtr, canvas.getNativeCanvasWrapper());
          // a value <= 0 indicates that the drawable is stopped or that renderThread
          // will manage the animation
          if (nextUpdate > 0) {
              if (mRunnable == null) {
                  mRunnable = this::invalidateSelf;
              }
              scheduleSelf(mRunnable, nextUpdate + SystemClock.uptimeMillis());
          } else if (nextUpdate == FINISHED) {
              // This means the animation was drawn in software mode and ended.
              postOnAnimationEnd();
          }
      }
  
      @Override
      public void setAlpha(@IntRange(from = 0, to = 255) int alpha) {
          if (alpha < 0 || alpha > 255) {
              throw new IllegalArgumentException("Alpha must be between 0 and"
                     + " 255! provided " + alpha);
          }
  
          if (mState.mNativePtr == 0) {
              throw new IllegalStateException("called setAlpha on empty AnimatedImageDrawable");
          }
  
          nSetAlpha(mState.mNativePtr, alpha);
          invalidateSelf();
      }
  
      @Override
      public int getAlpha() {
          if (mState.mNativePtr == 0) {
              throw new IllegalStateException("called getAlpha on empty AnimatedImageDrawable");
          }
          return nGetAlpha(mState.mNativePtr);
      }
  
      @Override
      public void setColorFilter(@Nullable ColorFilter colorFilter) {
          if (mState.mNativePtr == 0) {
              throw new IllegalStateException("called setColorFilter on empty AnimatedImageDrawable");
          }
  
          if (colorFilter != mColorFilter) {
              mColorFilter = colorFilter;
              long nativeFilter = colorFilter == null ? 0 : colorFilter.getNativeInstance();
              nSetColorFilter(mState.mNativePtr, nativeFilter);
              invalidateSelf();
          }
      }
  
      @Override
      @Nullable
      public ColorFilter getColorFilter() {
          return mColorFilter;
      }
  
      @Override
      public @PixelFormat.Opacity int getOpacity() {
          return PixelFormat.TRANSLUCENT;
      }
  
      @Override
      public void setAutoMirrored(boolean mirrored) {
          if (mState.mAutoMirrored != mirrored) {
              mState.mAutoMirrored = mirrored;
              if (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL && mState.mNativePtr != 0) {
                  nSetMirrored(mState.mNativePtr, mirrored);
                  invalidateSelf();
              }
          }
      }
  
      @Override
      public boolean onLayoutDirectionChanged(int layoutDirection) {
          if (!mState.mAutoMirrored || mState.mNativePtr == 0) {
              return false;
          }
  
          final boolean mirror = layoutDirection == View.LAYOUT_DIRECTION_RTL;
          nSetMirrored(mState.mNativePtr, mirror);
          return true;
      }
  
      @Override
      public final boolean isAutoMirrored() {
          return mState.mAutoMirrored;
      }
  
      // Animatable overrides
      /**
       *  Return whether the animation is currently running.
       *
       *  <p>When this drawable is created, this will return {@code false}. A client
       *  needs to call {@link #start} to start the animation.</p>
       */
      @Override
      public boolean isRunning() {
          if (mState.mNativePtr == 0) {
              throw new IllegalStateException("called isRunning on empty AnimatedImageDrawable");
          }
          return nIsRunning(mState.mNativePtr);
      }
  
      /**
       *  Start the animation.
       *
       *  <p>Does nothing if the animation is already running. If the animation is stopped,
       *  this will reset it.</p>
       *
       *  <p>When the drawable is drawn, starting the animation,
       *  {@link Animatable2.AnimationCallback#onAnimationStart} will be called.</p>
       */
      @Override
      public void start() {
          if (mState.mNativePtr == 0) {
              throw new IllegalStateException("called start on empty AnimatedImageDrawable");
          }
  
          if (nStart(mState.mNativePtr)) {
              mStarting = true;
              invalidateSelf();
          }
      }
  
      /**
       *  Stop the animation.
       *
       *  <p>If the animation is stopped, it will continue to display the frame
       *  it was displaying when stopped.</p>
       */
      @Override
      public void stop() {
          if (mState.mNativePtr == 0) {
              throw new IllegalStateException("called stop on empty AnimatedImageDrawable");
          }
          if (nStop(mState.mNativePtr)) {
              postOnAnimationEnd();
          }
      }
  
      // Animatable2 overrides
      private ArrayList<Animatable2.AnimationCallback> mAnimationCallbacks = null;
  
      @Override
      public void registerAnimationCallback(@NonNull AnimationCallback callback) {
          if (callback == null) {
              return;
          }
  
          if (mAnimationCallbacks == null) {
              mAnimationCallbacks = new ArrayList<Animatable2.AnimationCallback>();
              nSetOnAnimationEndListener(mState.mNativePtr, this);
          }
  
          if (!mAnimationCallbacks.contains(callback)) {
              mAnimationCallbacks.add(callback);
          }
      }
  
      @Override
      public boolean unregisterAnimationCallback(@NonNull AnimationCallback callback) {
          if (callback == null || mAnimationCallbacks == null
                  || !mAnimationCallbacks.remove(callback)) {
              return false;
          }
  
          if (mAnimationCallbacks.isEmpty()) {
              clearAnimationCallbacks();
          }
  
          return true;
      }
  
      @Override
      public void clearAnimationCallbacks() {
          if (mAnimationCallbacks != null) {
              mAnimationCallbacks = null;
              nSetOnAnimationEndListener(mState.mNativePtr, null);
          }
      }
  
      private void postOnAnimationStart() {
          if (mAnimationCallbacks == null) {
              return;
          }
  
          getHandler().post(() -> {
              for (Animatable2.AnimationCallback callback : mAnimationCallbacks) {
                  callback.onAnimationStart(this);
              }
          });
      }
  
      private void postOnAnimationEnd() {
          if (mAnimationCallbacks == null) {
              return;
          }
  
          getHandler().post(() -> {
              for (Animatable2.AnimationCallback callback : mAnimationCallbacks) {
                  callback.onAnimationEnd(this);
              }
          });
      }
  
      private Handler getHandler() {
          if (mHandler == null) {
              mHandler = new Handler(Looper.getMainLooper());
          }
          return mHandler;
      }
  
      /**
       *  Called by JNI.
       *
       *  The JNI code has already posted this to the thread that created the
       *  callback, so no need to post.
       */
      @SuppressWarnings("unused")
      @UnsupportedAppUsage
      private void onAnimationEnd() {
          if (mAnimationCallbacks != null) {
              for (Animatable2.AnimationCallback callback : mAnimationCallbacks) {
                  callback.onAnimationEnd(this);
              }
          }
      }
  
  
      private static native long nCreate(long nativeImageDecoder,
              @Nullable ImageDecoder decoder, int width, int height, long colorSpaceHandle,
              boolean extended, Rect cropRect) throws IOException;
      @FastNative
      private static native long nGetNativeFinalizer();
      private static native long nDraw(long nativePtr, long canvasNativePtr);
      @FastNative
      private static native void nSetAlpha(long nativePtr, int alpha);
      @FastNative
      private static native int nGetAlpha(long nativePtr);
      @FastNative
      private static native void nSetColorFilter(long nativePtr, long nativeFilter);
      @FastNative
      private static native boolean nIsRunning(long nativePtr);
      // Return whether the animation started.
      @FastNative
      private static native boolean nStart(long nativePtr);
      @FastNative
      private static native boolean nStop(long nativePtr);
      @FastNative
      private static native int nGetRepeatCount(long nativePtr);
      @FastNative
      private static native void nSetRepeatCount(long nativePtr, int repeatCount);
      // Pass the drawable down to native so it can call onAnimationEnd.
      private static native void nSetOnAnimationEndListener(long nativePtr,
              @Nullable AnimatedImageDrawable drawable);
      @FastNative
      private static native long nNativeByteSize(long nativePtr);
      @FastNative
      private static native void nSetMirrored(long nativePtr, boolean mirror);
  }
  