1 /*
2  * Copyright (C) 2018 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 android.graphics.drawable;
18 
19 import android.annotation.IntRange;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.compat.annotation.UnsupportedAppUsage;
23 import android.content.res.AssetFileDescriptor;
24 import android.content.res.Resources;
25 import android.content.res.Resources.Theme;
26 import android.content.res.TypedArray;
27 import android.graphics.Bitmap;
28 import android.graphics.Canvas;
29 import android.graphics.ColorFilter;
30 import android.graphics.ImageDecoder;
31 import android.graphics.PixelFormat;
32 import android.graphics.Rect;
33 import android.os.Handler;
34 import android.os.Looper;
35 import android.os.SystemClock;
36 import android.util.AttributeSet;
37 import android.util.DisplayMetrics;
38 import android.util.TypedValue;
39 import android.view.View;
40 
41 import com.android.internal.R;
42 
43 import dalvik.annotation.optimization.FastNative;
44 
45 import libcore.util.NativeAllocationRegistry;
46 
47 import org.xmlpull.v1.XmlPullParser;
48 import org.xmlpull.v1.XmlPullParserException;
49 
50 import java.io.IOException;
51 import java.io.InputStream;
52 import java.util.ArrayList;
53 
54 /**
55  * {@link Drawable} for drawing animated images (like GIF).
56  *
57  * <p>The framework handles decoding subsequent frames in another thread and
58  * updating when necessary. The drawable will only animate while it is being
59  * displayed.</p>
60  *
61  * <p>Created by {@link ImageDecoder#decodeDrawable}. A user needs to call
62  * {@link #start} to start the animation.</p>
63  *
64  * <p>It can also be defined in XML using the <code>&lt;animated-image></code>
65  * element.</p>
66  *
67  * @attr ref android.R.styleable#AnimatedImageDrawable_src
68  * @attr ref android.R.styleable#AnimatedImageDrawable_autoStart
69  * @attr ref android.R.styleable#AnimatedImageDrawable_repeatCount
70  * @attr ref android.R.styleable#AnimatedImageDrawable_autoMirrored
71  */
72 public class AnimatedImageDrawable extends Drawable implements Animatable2 {
73     private int mIntrinsicWidth;
74     private int mIntrinsicHeight;
75 
76     private boolean mStarting;
77 
78     private Handler mHandler;
79 
80     private class State {
State(long nativePtr, InputStream is, AssetFileDescriptor afd)81         State(long nativePtr, InputStream is, AssetFileDescriptor afd) {
82             mNativePtr = nativePtr;
83             mInputStream = is;
84             mAssetFd = afd;
85         }
86 
87         final long mNativePtr;
88 
89         // These just keep references so the native code can continue using them.
90         private final InputStream mInputStream;
91         private final AssetFileDescriptor mAssetFd;
92 
93         int[] mThemeAttrs = null;
94         boolean mAutoMirrored = false;
95         int mRepeatCount = REPEAT_UNDEFINED;
96     }
97 
98     private State mState;
99 
100     private Runnable mRunnable;
101 
102     private ColorFilter mColorFilter;
103 
104     /**
105      *  Pass this to {@link #setRepeatCount} to repeat infinitely.
106      *
107      *  <p>{@link Animatable2.AnimationCallback#onAnimationEnd} will never be
108      *  called unless there is an error.</p>
109      */
110     public static final int REPEAT_INFINITE = -1;
111 
112     /** @removed
113      * @deprecated Replaced with REPEAT_INFINITE to match other APIs.
114      */
115     @java.lang.Deprecated
116     public static final int LOOP_INFINITE = REPEAT_INFINITE;
117 
118     private static final int REPEAT_UNDEFINED = -2;
119 
120     /**
121      *  Specify the number of times to repeat the animation.
122      *
123      *  <p>By default, the repeat count in the encoded data is respected. If set
124      *  to {@link #REPEAT_INFINITE}, the animation will repeat as long as it is
125      *  displayed. If the value is {@code 0}, the animation will play once.</p>
126      *
127      *  <p>This call replaces the current repeat count. If the encoded data
128      *  specified a repeat count of {@code 2} (meaning that
129      *  {@link #getRepeatCount()} returns {@code 2}, the animation will play
130      *  three times. Calling {@code setRepeatCount(1)} will result in playing only
131      *  twice and {@link #getRepeatCount()} returning {@code 1}.</p>
132      *
133      *  <p>If the animation is already playing, the iterations that have already
134      *  occurred count towards the new count. If the animation has already
135      *  repeated the appropriate number of times (or more), it will finish its
136      *  current iteration and then stop.</p>
137      */
setRepeatCount(@ntRangefrom = REPEAT_INFINITE) int repeatCount)138     public void setRepeatCount(@IntRange(from = REPEAT_INFINITE) int repeatCount) {
139         if (repeatCount < REPEAT_INFINITE) {
140             throw new IllegalArgumentException("invalid value passed to setRepeatCount"
141                     + repeatCount);
142         }
143         if (mState.mRepeatCount != repeatCount) {
144             mState.mRepeatCount = repeatCount;
145             if (mState.mNativePtr != 0) {
146                 nSetRepeatCount(mState.mNativePtr, repeatCount);
147             }
148         }
149     }
150 
151     /** @removed
152      * @deprecated Replaced with setRepeatCount to match other APIs.
153      */
154     @java.lang.Deprecated
setLoopCount(int loopCount)155     public void setLoopCount(int loopCount) {
156         setRepeatCount(loopCount);
157     }
158 
159     /**
160      *  Retrieve the number of times the animation will repeat.
161      *
162      *  <p>By default, the repeat count in the encoded data is respected. If the
163      *  value is {@link #REPEAT_INFINITE}, the animation will repeat as long as
164      *  it is displayed. If the value is {@code 0}, it will play once.</p>
165      *
166      *  <p>Calling {@link #setRepeatCount} will make future calls to this method
167      *  return the value passed to {@link #setRepeatCount}.</p>
168      */
getRepeatCount()169     public int getRepeatCount() {
170         if (mState.mNativePtr == 0) {
171             throw new IllegalStateException("called getRepeatCount on empty AnimatedImageDrawable");
172         }
173         if (mState.mRepeatCount == REPEAT_UNDEFINED) {
174             mState.mRepeatCount = nGetRepeatCount(mState.mNativePtr);
175 
176         }
177         return mState.mRepeatCount;
178     }
179 
180     /** @removed
181      * @deprecated Replaced with getRepeatCount to match other APIs.
182      */
183     @java.lang.Deprecated
getLoopCount(int loopCount)184     public int getLoopCount(int loopCount) {
185         return getRepeatCount();
186     }
187 
188     /**
189      * Create an empty AnimatedImageDrawable.
190      */
AnimatedImageDrawable()191     public AnimatedImageDrawable() {
192         mState = new State(0, null, null);
193     }
194 
195     @Override
inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)196     public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)
197             throws XmlPullParserException, IOException {
198         super.inflate(r, parser, attrs, theme);
199 
200         final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.AnimatedImageDrawable);
201         updateStateFromTypedArray(a, mSrcDensityOverride);
202     }
203 
updateStateFromTypedArray(TypedArray a, int srcDensityOverride)204     private void updateStateFromTypedArray(TypedArray a, int srcDensityOverride)
205             throws XmlPullParserException {
206         State oldState = mState;
207         final Resources r = a.getResources();
208         final int srcResId = a.getResourceId(R.styleable.AnimatedImageDrawable_src, 0);
209         if (srcResId != 0) {
210             // Follow the density handling in BitmapDrawable.
211             final TypedValue value = new TypedValue();
212             r.getValueForDensity(srcResId, srcDensityOverride, value, true);
213             if (srcDensityOverride > 0 && value.density > 0
214                     && value.density != TypedValue.DENSITY_NONE) {
215                 if (value.density == srcDensityOverride) {
216                     value.density = r.getDisplayMetrics().densityDpi;
217                 } else {
218                     value.density =
219                             (value.density * r.getDisplayMetrics().densityDpi) / srcDensityOverride;
220                 }
221             }
222 
223             int density = Bitmap.DENSITY_NONE;
224             if (value.density == TypedValue.DENSITY_DEFAULT) {
225                 density = DisplayMetrics.DENSITY_DEFAULT;
226             } else if (value.density != TypedValue.DENSITY_NONE) {
227                 density = value.density;
228             }
229 
230             Drawable drawable = null;
231             try {
232                 InputStream is = r.openRawResource(srcResId, value);
233                 ImageDecoder.Source source = ImageDecoder.createSource(r, is, density);
234                 drawable = ImageDecoder.decodeDrawable(source, (decoder, info, src) -> {
235                     if (!info.isAnimated()) {
236                         throw new IllegalArgumentException("image is not animated");
237                     }
238                 });
239             } catch (IOException e) {
240                 throw new XmlPullParserException(a.getPositionDescription() +
241                         ": <animated-image> requires a valid 'src' attribute", null, e);
242             }
243 
244             if (!(drawable instanceof AnimatedImageDrawable)) {
245                 throw new XmlPullParserException(a.getPositionDescription() +
246                         ": <animated-image> did not decode animated");
247             }
248 
249             // This may have previously been set without a src if we were waiting for a
250             // theme.
251             final int repeatCount = mState.mRepeatCount;
252             // Transfer the state of other to this one. other will be discarded.
253             AnimatedImageDrawable other = (AnimatedImageDrawable) drawable;
254             mState = other.mState;
255             other.mState = null;
256             mIntrinsicWidth =  other.mIntrinsicWidth;
257             mIntrinsicHeight = other.mIntrinsicHeight;
258             if (repeatCount != REPEAT_UNDEFINED) {
259                 this.setRepeatCount(repeatCount);
260             }
261         }
262 
263         mState.mThemeAttrs = a.extractThemeAttrs();
264         if (mState.mNativePtr == 0 && (mState.mThemeAttrs == null
265                 || mState.mThemeAttrs[R.styleable.AnimatedImageDrawable_src] == 0)) {
266             throw new XmlPullParserException(a.getPositionDescription() +
267                     ": <animated-image> requires a valid 'src' attribute");
268         }
269 
270         mState.mAutoMirrored = a.getBoolean(
271                 R.styleable.AnimatedImageDrawable_autoMirrored, oldState.mAutoMirrored);
272 
273         int repeatCount = a.getInt(
274                 R.styleable.AnimatedImageDrawable_repeatCount, REPEAT_UNDEFINED);
275         if (repeatCount != REPEAT_UNDEFINED) {
276             this.setRepeatCount(repeatCount);
277         }
278 
279         boolean autoStart = a.getBoolean(
280                 R.styleable.AnimatedImageDrawable_autoStart, false);
281         if (autoStart && mState.mNativePtr != 0) {
282             this.start();
283         }
284     }
285 
286     /**
287      * @hide
288      * This should only be called by ImageDecoder.
289      *
290      * decoder is only non-null if it has a PostProcess
291      */
AnimatedImageDrawable(long nativeImageDecoder, @Nullable ImageDecoder decoder, int width, int height, long colorSpaceHandle, boolean extended, int srcDensity, int dstDensity, Rect cropRect, InputStream inputStream, AssetFileDescriptor afd)292     public AnimatedImageDrawable(long nativeImageDecoder,
293             @Nullable ImageDecoder decoder, int width, int height,
294             long colorSpaceHandle, boolean extended, int srcDensity, int dstDensity,
295             Rect cropRect, InputStream inputStream, AssetFileDescriptor afd)
296             throws IOException {
297         width = Bitmap.scaleFromDensity(width, srcDensity, dstDensity);
298         height = Bitmap.scaleFromDensity(height, srcDensity, dstDensity);
299 
300         if (cropRect == null) {
301             mIntrinsicWidth  = width;
302             mIntrinsicHeight = height;
303         } else {
304             cropRect.set(Bitmap.scaleFromDensity(cropRect.left, srcDensity, dstDensity),
305                     Bitmap.scaleFromDensity(cropRect.top, srcDensity, dstDensity),
306                     Bitmap.scaleFromDensity(cropRect.right, srcDensity, dstDensity),
307                     Bitmap.scaleFromDensity(cropRect.bottom, srcDensity, dstDensity));
308             mIntrinsicWidth  = cropRect.width();
309             mIntrinsicHeight = cropRect.height();
310         }
311 
312         mState = new State(nCreate(nativeImageDecoder, decoder, width, height, colorSpaceHandle,
313                     extended, cropRect), inputStream, afd);
314 
315         final long nativeSize = nNativeByteSize(mState.mNativePtr);
316         NativeAllocationRegistry registry = NativeAllocationRegistry.createMalloced(
317                 AnimatedImageDrawable.class.getClassLoader(), nGetNativeFinalizer(), nativeSize);
318         registry.registerNativeAllocation(mState, mState.mNativePtr);
319     }
320 
321     @Override
getIntrinsicWidth()322     public int getIntrinsicWidth() {
323         return mIntrinsicWidth;
324     }
325 
326     @Override
getIntrinsicHeight()327     public int getIntrinsicHeight() {
328         return mIntrinsicHeight;
329     }
330 
331     // nDraw returns -1 if the animation has finished.
332     private static final int FINISHED = -1;
333 
334     @Override
draw(@onNull Canvas canvas)335     public void draw(@NonNull Canvas canvas) {
336         if (mState.mNativePtr == 0) {
337             throw new IllegalStateException("called draw on empty AnimatedImageDrawable");
338         }
339 
340         if (mStarting) {
341             mStarting = false;
342 
343             postOnAnimationStart();
344         }
345 
346         long nextUpdate = nDraw(mState.mNativePtr, canvas.getNativeCanvasWrapper());
347         // a value <= 0 indicates that the drawable is stopped or that renderThread
348         // will manage the animation
349         if (nextUpdate > 0) {
350             if (mRunnable == null) {
351                 mRunnable = this::invalidateSelf;
352             }
353             scheduleSelf(mRunnable, nextUpdate + SystemClock.uptimeMillis());
354         } else if (nextUpdate == FINISHED) {
355             // This means the animation was drawn in software mode and ended.
356             postOnAnimationEnd();
357         }
358     }
359 
360     @Override
setAlpha(@ntRangefrom = 0, to = 255) int alpha)361     public void setAlpha(@IntRange(from = 0, to = 255) int alpha) {
362         if (alpha < 0 || alpha > 255) {
363             throw new IllegalArgumentException("Alpha must be between 0 and"
364                    + " 255! provided " + alpha);
365         }
366 
367         if (mState.mNativePtr == 0) {
368             throw new IllegalStateException("called setAlpha on empty AnimatedImageDrawable");
369         }
370 
371         nSetAlpha(mState.mNativePtr, alpha);
372         invalidateSelf();
373     }
374 
375     @Override
getAlpha()376     public int getAlpha() {
377         if (mState.mNativePtr == 0) {
378             throw new IllegalStateException("called getAlpha on empty AnimatedImageDrawable");
379         }
380         return nGetAlpha(mState.mNativePtr);
381     }
382 
383     @Override
setColorFilter(@ullable ColorFilter colorFilter)384     public void setColorFilter(@Nullable ColorFilter colorFilter) {
385         if (mState.mNativePtr == 0) {
386             throw new IllegalStateException("called setColorFilter on empty AnimatedImageDrawable");
387         }
388 
389         if (colorFilter != mColorFilter) {
390             mColorFilter = colorFilter;
391             long nativeFilter = colorFilter == null ? 0 : colorFilter.getNativeInstance();
392             nSetColorFilter(mState.mNativePtr, nativeFilter);
393             invalidateSelf();
394         }
395     }
396 
397     @Override
398     @Nullable
getColorFilter()399     public ColorFilter getColorFilter() {
400         return mColorFilter;
401     }
402 
403     @Override
getOpacity()404     public @PixelFormat.Opacity int getOpacity() {
405         return PixelFormat.TRANSLUCENT;
406     }
407 
408     @Override
setAutoMirrored(boolean mirrored)409     public void setAutoMirrored(boolean mirrored) {
410         if (mState.mAutoMirrored != mirrored) {
411             mState.mAutoMirrored = mirrored;
412             if (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL && mState.mNativePtr != 0) {
413                 nSetMirrored(mState.mNativePtr, mirrored);
414                 invalidateSelf();
415             }
416         }
417     }
418 
419     @Override
onLayoutDirectionChanged(int layoutDirection)420     public boolean onLayoutDirectionChanged(int layoutDirection) {
421         if (!mState.mAutoMirrored || mState.mNativePtr == 0) {
422             return false;
423         }
424 
425         final boolean mirror = layoutDirection == View.LAYOUT_DIRECTION_RTL;
426         nSetMirrored(mState.mNativePtr, mirror);
427         return true;
428     }
429 
430     @Override
isAutoMirrored()431     public final boolean isAutoMirrored() {
432         return mState.mAutoMirrored;
433     }
434 
435     // Animatable overrides
436     /**
437      *  Return whether the animation is currently running.
438      *
439      *  <p>When this drawable is created, this will return {@code false}. A client
440      *  needs to call {@link #start} to start the animation.</p>
441      */
442     @Override
isRunning()443     public boolean isRunning() {
444         if (mState.mNativePtr == 0) {
445             throw new IllegalStateException("called isRunning on empty AnimatedImageDrawable");
446         }
447         return nIsRunning(mState.mNativePtr);
448     }
449 
450     /**
451      *  Start the animation.
452      *
453      *  <p>Does nothing if the animation is already running. If the animation is stopped,
454      *  this will reset it.</p>
455      *
456      *  <p>When the drawable is drawn, starting the animation,
457      *  {@link Animatable2.AnimationCallback#onAnimationStart} will be called.</p>
458      */
459     @Override
start()460     public void start() {
461         if (mState.mNativePtr == 0) {
462             throw new IllegalStateException("called start on empty AnimatedImageDrawable");
463         }
464 
465         if (nStart(mState.mNativePtr)) {
466             mStarting = true;
467             invalidateSelf();
468         }
469     }
470 
471     /**
472      *  Stop the animation.
473      *
474      *  <p>If the animation is stopped, it will continue to display the frame
475      *  it was displaying when stopped.</p>
476      */
477     @Override
stop()478     public void stop() {
479         if (mState.mNativePtr == 0) {
480             throw new IllegalStateException("called stop on empty AnimatedImageDrawable");
481         }
482         if (nStop(mState.mNativePtr)) {
483             postOnAnimationEnd();
484         }
485     }
486 
487     // Animatable2 overrides
488     private ArrayList<Animatable2.AnimationCallback> mAnimationCallbacks = null;
489 
490     @Override
registerAnimationCallback(@onNull AnimationCallback callback)491     public void registerAnimationCallback(@NonNull AnimationCallback callback) {
492         if (callback == null) {
493             return;
494         }
495 
496         if (mAnimationCallbacks == null) {
497             mAnimationCallbacks = new ArrayList<Animatable2.AnimationCallback>();
498             nSetOnAnimationEndListener(mState.mNativePtr, this);
499         }
500 
501         if (!mAnimationCallbacks.contains(callback)) {
502             mAnimationCallbacks.add(callback);
503         }
504     }
505 
506     @Override
unregisterAnimationCallback(@onNull AnimationCallback callback)507     public boolean unregisterAnimationCallback(@NonNull AnimationCallback callback) {
508         if (callback == null || mAnimationCallbacks == null
509                 || !mAnimationCallbacks.remove(callback)) {
510             return false;
511         }
512 
513         if (mAnimationCallbacks.isEmpty()) {
514             clearAnimationCallbacks();
515         }
516 
517         return true;
518     }
519 
520     @Override
clearAnimationCallbacks()521     public void clearAnimationCallbacks() {
522         if (mAnimationCallbacks != null) {
523             mAnimationCallbacks = null;
524             nSetOnAnimationEndListener(mState.mNativePtr, null);
525         }
526     }
527 
postOnAnimationStart()528     private void postOnAnimationStart() {
529         if (mAnimationCallbacks == null) {
530             return;
531         }
532 
533         getHandler().post(() -> {
534             for (Animatable2.AnimationCallback callback : mAnimationCallbacks) {
535                 callback.onAnimationStart(this);
536             }
537         });
538     }
539 
postOnAnimationEnd()540     private void postOnAnimationEnd() {
541         if (mAnimationCallbacks == null) {
542             return;
543         }
544 
545         getHandler().post(() -> {
546             for (Animatable2.AnimationCallback callback : mAnimationCallbacks) {
547                 callback.onAnimationEnd(this);
548             }
549         });
550     }
551 
getHandler()552     private Handler getHandler() {
553         if (mHandler == null) {
554             mHandler = new Handler(Looper.getMainLooper());
555         }
556         return mHandler;
557     }
558 
559     /**
560      *  Called by JNI.
561      *
562      *  The JNI code has already posted this to the thread that created the
563      *  callback, so no need to post.
564      */
565     @SuppressWarnings("unused")
566     @UnsupportedAppUsage
onAnimationEnd()567     private void onAnimationEnd() {
568         if (mAnimationCallbacks != null) {
569             for (Animatable2.AnimationCallback callback : mAnimationCallbacks) {
570                 callback.onAnimationEnd(this);
571             }
572         }
573     }
574 
575 
nCreate(long nativeImageDecoder, @Nullable ImageDecoder decoder, int width, int height, long colorSpaceHandle, boolean extended, Rect cropRect)576     private static native long nCreate(long nativeImageDecoder,
577             @Nullable ImageDecoder decoder, int width, int height, long colorSpaceHandle,
578             boolean extended, Rect cropRect) throws IOException;
579     @FastNative
nGetNativeFinalizer()580     private static native long nGetNativeFinalizer();
nDraw(long nativePtr, long canvasNativePtr)581     private static native long nDraw(long nativePtr, long canvasNativePtr);
582     @FastNative
nSetAlpha(long nativePtr, int alpha)583     private static native void nSetAlpha(long nativePtr, int alpha);
584     @FastNative
nGetAlpha(long nativePtr)585     private static native int nGetAlpha(long nativePtr);
586     @FastNative
nSetColorFilter(long nativePtr, long nativeFilter)587     private static native void nSetColorFilter(long nativePtr, long nativeFilter);
588     @FastNative
nIsRunning(long nativePtr)589     private static native boolean nIsRunning(long nativePtr);
590     // Return whether the animation started.
591     @FastNative
nStart(long nativePtr)592     private static native boolean nStart(long nativePtr);
593     @FastNative
nStop(long nativePtr)594     private static native boolean nStop(long nativePtr);
595     @FastNative
nGetRepeatCount(long nativePtr)596     private static native int nGetRepeatCount(long nativePtr);
597     @FastNative
nSetRepeatCount(long nativePtr, int repeatCount)598     private static native void nSetRepeatCount(long nativePtr, int repeatCount);
599     // Pass the drawable down to native so it can call onAnimationEnd.
nSetOnAnimationEndListener(long nativePtr, @Nullable AnimatedImageDrawable drawable)600     private static native void nSetOnAnimationEndListener(long nativePtr,
601             @Nullable AnimatedImageDrawable drawable);
602     @FastNative
nNativeByteSize(long nativePtr)603     private static native long nNativeByteSize(long nativePtr);
604     @FastNative
nSetMirrored(long nativePtr, boolean mirror)605     private static native void nSetMirrored(long nativePtr, boolean mirror);
606 }
607