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.cts;
18 
19 import static org.junit.Assert.assertEquals;
20 import static org.junit.Assert.assertFalse;
21 import static org.junit.Assert.assertNotEquals;
22 import static org.junit.Assert.assertNotNull;
23 import static org.junit.Assert.assertTrue;
24 import static org.junit.Assert.fail;
25 
26 import android.Manifest;
27 import android.app.Activity;
28 import android.content.ContentResolver;
29 import android.content.res.Resources;
30 import android.graphics.Bitmap;
31 import android.graphics.Canvas;
32 import android.graphics.Color;
33 import android.graphics.ColorFilter;
34 import android.graphics.ImageDecoder;
35 import android.graphics.LightingColorFilter;
36 import android.graphics.Paint;
37 import android.graphics.PixelFormat;
38 import android.graphics.Rect;
39 import android.graphics.cts.R;
40 import android.graphics.cts.Utils;
41 import android.graphics.drawable.AnimatedImageDrawable;
42 import android.graphics.drawable.Drawable;
43 import android.net.Uri;
44 import android.view.View;
45 import android.widget.ImageView;
46 
47 import androidx.test.InstrumentationRegistry;
48 import androidx.test.filters.FlakyTest;
49 import androidx.test.rule.ActivityTestRule;
50 
51 import com.android.compatibility.common.util.AdoptShellPermissionsRule;
52 import com.android.compatibility.common.util.BitmapUtils;
53 import com.android.compatibility.common.util.WidgetTestUtils;
54 import com.android.compatibility.common.util.WindowUtil;
55 
56 import org.junit.Rule;
57 import org.junit.Test;
58 import org.junit.runner.RunWith;
59 import org.xmlpull.v1.XmlPullParser;
60 import org.xmlpull.v1.XmlPullParserException;
61 
62 import java.io.ByteArrayOutputStream;
63 import java.io.IOException;
64 import java.io.InputStream;
65 import java.nio.ByteBuffer;
66 import java.util.function.BiFunction;
67 
68 import junitparams.JUnitParamsRunner;
69 import junitparams.Parameters;
70 
71 @RunWith(JUnitParamsRunner.class)
72 public class AnimatedImageDrawableTest {
73     private ImageView mImageView;
74 
75     private static final int RES_ID = R.drawable.animated;
76     private static final int WIDTH = 278;
77     private static final int HEIGHT = 183;
78     private static final int NUM_FRAMES = 4;
79     private static final int FRAME_DURATION = 250; // in milliseconds
80     private static final int DURATION = NUM_FRAMES * FRAME_DURATION;
81 
82     @Rule(order = 0)
83     public AdoptShellPermissionsRule mAdoptShellPermissionsRule = new AdoptShellPermissionsRule(
84             androidx.test.platform.app.InstrumentationRegistry
85                     .getInstrumentation().getUiAutomation(),
86             Manifest.permission.START_ACTIVITIES_FROM_SDK_SANDBOX);
87 
88     @Rule(order = 1)
89     public ActivityTestRule<AnimatedImageActivity> mActivityRule =
90             new ActivityTestRule<AnimatedImageActivity>(AnimatedImageActivity.class);
91     private Activity mActivity;
92 
getResources()93     private Resources getResources() {
94         return InstrumentationRegistry.getTargetContext().getResources();
95     }
96 
getContentResolver()97     private ContentResolver getContentResolver() {
98         return InstrumentationRegistry.getTargetContext().getContentResolver();
99     }
100 
setupActivity()101     private void setupActivity() {
102         mActivity = mActivityRule.getActivity();
103         WindowUtil.waitForFocus(mActivity);
104         mImageView = mActivity.findViewById(R.id.animated_image);
105     }
106 
107     @Test
testEmptyConstructor()108     public void testEmptyConstructor() {
109         new AnimatedImageDrawable();
110     }
111 
112     @Test
testMutate()113     public void testMutate() {
114         Resources res = getResources();
115         AnimatedImageDrawable aid1 = (AnimatedImageDrawable) res.getDrawable(R.drawable.animated);
116         AnimatedImageDrawable aid2 = (AnimatedImageDrawable) res.getDrawable(R.drawable.animated);
117 
118         final int originalAlpha = aid1.getAlpha();
119         assertEquals(255, originalAlpha);
120         assertEquals(255, aid2.getAlpha());
121 
122         try {
123             aid1.mutate();
124             aid1.setAlpha(100);
125             assertEquals(originalAlpha, aid2.getAlpha());
126         } finally {
127             res.getDrawable(R.drawable.animated).setAlpha(originalAlpha);
128         }
129     }
130 
createFromImageDecoder(int resId)131     private AnimatedImageDrawable createFromImageDecoder(int resId) {
132         Uri uri = Utils.getAsResourceUri(resId);
133         try {
134             ImageDecoder.Source source = ImageDecoder.createSource(getContentResolver(), uri);
135             Drawable drawable = ImageDecoder.decodeDrawable(source);
136             assertTrue(drawable instanceof AnimatedImageDrawable);
137             return (AnimatedImageDrawable) drawable;
138         } catch (IOException e) {
139             fail("failed to create image from " + uri);
140             return null;
141         }
142     }
143 
decodeBitmap(int resId)144     private Bitmap decodeBitmap(int resId) {
145         Uri uri = Utils.getAsResourceUri(resId);
146         try {
147             ImageDecoder.Source source = ImageDecoder.createSource(getContentResolver(), uri);
148             return ImageDecoder.decodeBitmap(source, (decoder, info, src) -> {
149                 decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
150             });
151         } catch (IOException e) {
152             fail("Failed to create Bitmap from " + uri);
153             return null;
154         }
155     }
156 
157     @Test
testDecodeAnimatedImageDrawable()158     public void testDecodeAnimatedImageDrawable() {
159         Drawable drawable = createFromImageDecoder(RES_ID);
160         assertEquals(WIDTH,  drawable.getIntrinsicWidth());
161         assertEquals(HEIGHT, drawable.getIntrinsicHeight());
162     }
163 
164     private static class Callback extends Animatable2Callback {
165         private final Drawable mDrawable;
166 
Callback(Drawable d)167         public Callback(Drawable d) {
168             mDrawable = d;
169         }
170 
171         @Override
onAnimationStart(Drawable drawable)172         public void onAnimationStart(Drawable drawable) {
173             assertNotNull(drawable);
174             assertEquals(mDrawable, drawable);
175             super.onAnimationStart(drawable);
176         }
177 
178         @Override
onAnimationEnd(Drawable drawable)179         public void onAnimationEnd(Drawable drawable) {
180             assertNotNull(drawable);
181             assertEquals(mDrawable, drawable);
182             super.onAnimationEnd(drawable);
183         }
184     };
185 
186     @Test(expected=IllegalStateException.class)
testRegisterWithoutLooper()187     public void testRegisterWithoutLooper() {
188         AnimatedImageDrawable drawable = createFromImageDecoder(R.drawable.animated);
189 
190         // registerAnimationCallback must be run on a thread with a Looper,
191         // which the test thread does not have.
192         Callback cb = new Callback(drawable);
193         drawable.registerAnimationCallback(cb);
194     }
195 
196     @Test
testRegisterCallback()197     public void testRegisterCallback() throws Throwable {
198         setupActivity();
199         AnimatedImageDrawable drawable = createFromImageDecoder(R.drawable.animated);
200 
201         mActivityRule.runOnUiThread(() -> {
202             // Register a callback.
203             Callback cb = new Callback(drawable);
204             drawable.registerAnimationCallback(cb);
205             assertTrue(drawable.unregisterAnimationCallback(cb));
206 
207             // Now that it has been removed, it cannot be removed again.
208             assertFalse(drawable.unregisterAnimationCallback(cb));
209         });
210     }
211 
212     @Test
testClearCallbacks()213     public void testClearCallbacks() throws Throwable {
214         setupActivity();
215         AnimatedImageDrawable drawable = createFromImageDecoder(R.drawable.animated);
216 
217         Callback[] callbacks = new Callback[] {
218             new Callback(drawable),
219             new Callback(drawable),
220             new Callback(drawable),
221             new Callback(drawable),
222             new Callback(drawable),
223             new Callback(drawable),
224             new Callback(drawable),
225             new Callback(drawable),
226         };
227 
228         mActivityRule.runOnUiThread(() -> {
229             for (Callback cb : callbacks) {
230                 drawable.registerAnimationCallback(cb);
231             }
232         });
233 
234         drawable.clearAnimationCallbacks();
235 
236         for (Callback cb : callbacks) {
237             // It has already been removed.
238             assertFalse(drawable.unregisterAnimationCallback(cb));
239         }
240     }
241 
242     @Test
testUnregisterCallback()243     public void testUnregisterCallback() throws Throwable {
244         setupActivity();
245         AnimatedImageDrawable drawable = createFromImageDecoder(R.drawable.animated);
246 
247         Callback cb = new Callback(drawable);
248         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mImageView, () -> {
249             mImageView.setImageDrawable(drawable);
250 
251             drawable.registerAnimationCallback(cb);
252             assertTrue(drawable.unregisterAnimationCallback(cb));
253             drawable.setRepeatCount(0);
254             drawable.start();
255         });
256 
257         cb.waitForStart();
258         cb.assertStarted(false);
259 
260         cb.waitForEnd(DURATION * 2);
261         cb.assertEnded(false);
262     }
263 
264     @Test
265     @FlakyTest (bugId = 120280954)
testLifeCycle()266     public void testLifeCycle() throws Throwable {
267         setupActivity();
268         AnimatedImageDrawable drawable = createFromImageDecoder(RES_ID);
269 
270         // Only run the animation one time.
271         drawable.setRepeatCount(0);
272 
273         Callback cb = new Callback(drawable);
274         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mImageView, () -> {
275             mImageView.setImageDrawable(drawable);
276 
277             drawable.registerAnimationCallback(cb);
278         });
279 
280         assertFalse(drawable.isRunning());
281         cb.assertStarted(false);
282         cb.assertEnded(false);
283 
284         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mImageView, () -> {
285             drawable.start();
286             assertTrue(drawable.isRunning());
287         });
288         cb.waitForStart();
289         cb.assertStarted(true);
290 
291         // FIXME: Now that it seems the reason for the flakiness has been solved (b/129400990),
292         // reduce this extra duration workaround.
293         // Extra time, to wait for the message to post.
294         cb.waitForEnd(DURATION * 20);
295         cb.assertEnded(true);
296         assertFalse(drawable.isRunning());
297     }
298 
299     @Test
testLifeCycleSoftware()300     public void testLifeCycleSoftware() throws Throwable {
301         setupActivity();
302         AnimatedImageDrawable drawable = createFromImageDecoder(RES_ID);
303 
304         Bitmap bm = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(),
305                 Bitmap.Config.ARGB_8888);
306         Canvas canvas = new Canvas(bm);
307 
308         Callback cb = new Callback(drawable);
309         mActivityRule.runOnUiThread(() -> {
310             drawable.registerAnimationCallback(cb);
311             drawable.draw(canvas);
312         });
313 
314         assertFalse(drawable.isRunning());
315         cb.assertStarted(false);
316         cb.assertEnded(false);
317 
318         mActivityRule.runOnUiThread(() -> {
319             drawable.start();
320             assertTrue(drawable.isRunning());
321             drawable.draw(canvas);
322         });
323         cb.waitForStart();
324         cb.assertStarted(true);
325 
326         // Only run the animation one time.
327         drawable.setRepeatCount(0);
328 
329         // The drawable will prevent skipping frames, so we actually have to
330         // draw each frame. (Start with 1, since we already drew frame 0.)
331         for (int i = 1; i < NUM_FRAMES; i++) {
332             cb.waitForEnd(FRAME_DURATION);
333             cb.assertEnded(false);
334             mActivityRule.runOnUiThread(() -> {
335                 assertTrue(drawable.isRunning());
336                 drawable.draw(canvas);
337             });
338         }
339 
340         cb.waitForEnd(FRAME_DURATION);
341         assertFalse(drawable.isRunning());
342         cb.assertEnded(true);
343     }
344 
345     @Test
346     @FlakyTest (bugId = 72737527)
testAddCallbackAfterStart()347     public void testAddCallbackAfterStart() throws Throwable {
348         setupActivity();
349         AnimatedImageDrawable drawable = createFromImageDecoder(RES_ID);
350         Callback cb = new Callback(drawable);
351         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mImageView, () -> {
352             mImageView.setImageDrawable(drawable);
353 
354             drawable.setRepeatCount(0);
355             drawable.start();
356             drawable.registerAnimationCallback(cb);
357         });
358 
359         // FIXME: Now that it seems the reason for the flakiness has been solved (b/129400990),
360         // reduce this extra duration workaround.
361         // Add extra duration to wait for the message posted by the end of the
362         // animation. This should help fix flakiness.
363         cb.waitForEnd(DURATION * 10);
364         cb.assertEnded(true);
365     }
366 
367     @Test
testStop()368     public void testStop() throws Throwable {
369         setupActivity();
370         AnimatedImageDrawable drawable = createFromImageDecoder(RES_ID);
371         Callback cb = new Callback(drawable);
372         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mImageView, () -> {
373             mImageView.setImageDrawable(drawable);
374 
375             drawable.registerAnimationCallback(cb);
376 
377             drawable.start();
378             assertTrue(drawable.isRunning());
379         });
380 
381         cb.waitForStart();
382         cb.assertStarted(true);
383 
384         mActivityRule.runOnUiThread(() -> {
385             drawable.stop();
386             assertFalse(drawable.isRunning());
387         });
388 
389         // This duration may be overkill, but we need to wait for the message
390         // to post. Increasing it should help with flakiness on bots.
391         cb.waitForEnd(DURATION * 3);
392         cb.assertEnded(true);
393     }
394 
395     @Test
396     @FlakyTest (bugId = 72737527)
397     @Parameters({ "3", "5", "7", "16" })
testRepeatCounts(int repeatCount)398     public void testRepeatCounts(int repeatCount) throws Throwable {
399         setupActivity();
400         AnimatedImageDrawable drawable = createFromImageDecoder(RES_ID);
401         assertEquals(AnimatedImageDrawable.REPEAT_INFINITE, drawable.getRepeatCount());
402 
403         Callback cb = new Callback(drawable);
404         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mImageView, () -> {
405             mImageView.setImageDrawable(drawable);
406 
407             drawable.registerAnimationCallback(cb);
408             drawable.setRepeatCount(repeatCount);
409             assertEquals(repeatCount, drawable.getRepeatCount());
410             drawable.start();
411         });
412 
413         cb.waitForStart();
414         cb.assertStarted(true);
415 
416         // The animation runs repeatCount + 1 total times.
417         cb.waitForEnd(DURATION * repeatCount);
418         cb.assertEnded(false);
419 
420         // FIXME: Now that it seems the reason for the flakiness has been solved (b/129400990),
421         // reduce this extra duration workaround.
422         cb.waitForEnd(DURATION * 20);
423         cb.assertEnded(true);
424 
425         drawable.setRepeatCount(AnimatedImageDrawable.REPEAT_INFINITE);
426         assertEquals(AnimatedImageDrawable.REPEAT_INFINITE, drawable.getRepeatCount());
427     }
428 
429     @Test
testRepeatCountInfinite()430     public void testRepeatCountInfinite() throws Throwable {
431         setupActivity();
432         AnimatedImageDrawable drawable = createFromImageDecoder(RES_ID);
433         Callback cb = new Callback(drawable);
434         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mImageView, () -> {
435             mImageView.setImageDrawable(drawable);
436 
437             drawable.registerAnimationCallback(cb);
438             drawable.setRepeatCount(AnimatedImageDrawable.REPEAT_INFINITE);
439             drawable.start();
440         });
441 
442         // There is no way to truly test infinite, but let it run for a long
443         // time and verify that it's still running.
444         cb.waitForEnd(DURATION * 30);
445         cb.assertEnded(false);
446         assertTrue(drawable.isRunning());
447     }
448 
parametersForTestEncodedRepeats()449     public static Object[] parametersForTestEncodedRepeats() {
450         return new Object[] {
451             new Object[] { R.drawable.animated, AnimatedImageDrawable.REPEAT_INFINITE },
452             new Object[] { R.drawable.animated_one_loop, 1 },
453             new Object[] { R.drawable.webp_animated, AnimatedImageDrawable.REPEAT_INFINITE },
454             new Object[] { R.drawable.webp_animated_large, AnimatedImageDrawable.REPEAT_INFINITE },
455             new Object[] { R.drawable.webp_animated_icc_xmp, 31999 },
456             new Object[] { R.drawable.count_down_color_test, 0 },
457         };
458     }
459 
460     @Test
461     @Parameters(method = "parametersForTestEncodedRepeats")
testEncodedRepeats(int resId, int expectedRepeatCount)462     public void testEncodedRepeats(int resId, int expectedRepeatCount) {
463         AnimatedImageDrawable drawable = createFromImageDecoder(resId);
464         assertEquals(expectedRepeatCount, drawable.getRepeatCount());
465     }
466 
467     @Test
testGetOpacity()468     public void testGetOpacity() {
469         AnimatedImageDrawable drawable = createFromImageDecoder(RES_ID);
470         assertEquals(PixelFormat.TRANSLUCENT, drawable.getOpacity());
471     }
472 
473     @Test
testColorFilter()474     public void testColorFilter() {
475         AnimatedImageDrawable drawable = createFromImageDecoder(RES_ID);
476 
477         ColorFilter filter = new LightingColorFilter(0, Color.RED);
478         drawable.setColorFilter(filter);
479         assertEquals(filter, drawable.getColorFilter());
480 
481         Bitmap actual = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
482                 drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
483         {
484             Canvas canvas = new Canvas(actual);
485             drawable.draw(canvas);
486         }
487 
488         for (int i = 0; i < actual.getWidth(); ++i) {
489             for (int j = 0; j < actual.getHeight(); ++j) {
490                 int color = actual.getPixel(i, j);
491                 // The LightingColorFilter does not affect the transparent pixels,
492                 // so all pixels should either remain transparent or turn red.
493                 if (color != Color.RED && color != Color.TRANSPARENT) {
494                     fail("pixel at " + i + ", " + j + " does not match expected. "
495                             + "expected: " + Color.RED + " OR " + Color.TRANSPARENT
496                             + " actual: " + color);
497                 }
498             }
499         }
500     }
501 
502     @Test
testExif()503     public void testExif() {
504         // This animation has an exif orientation that makes it match R.drawable.animated (RES_ID).
505         AnimatedImageDrawable exifAnimation = createFromImageDecoder(R.drawable.animated_webp);
506 
507         Bitmap expected = decodeBitmap(RES_ID);
508         final int width = expected.getWidth();
509         final int height = expected.getHeight();
510 
511         assertEquals(width, exifAnimation.getIntrinsicWidth());
512         assertEquals(height, exifAnimation.getIntrinsicHeight());
513 
514         Bitmap actual = Bitmap.createBitmap(width, height, expected.getConfig(),
515                 expected.hasAlpha(), expected.getColorSpace());
516         {
517             Canvas canvas = new Canvas(actual);
518             exifAnimation.setBounds(0, 0, width, height);
519             exifAnimation.draw(canvas);
520         }
521 
522         // mseMargin was chosen by looking at the logs. The images are not exactly
523         // the same due to the fact that animated_webp's frames are encoded lossily,
524         // but the two images are perceptually identical.
525         final int mseMargin = 143;
526         final boolean lessThanMargin = true;
527         BitmapUtils.assertBitmapsMse(expected, actual, mseMargin, lessThanMargin,
528                 expected.isPremultiplied());
529     }
530 
531     @Test
testPostProcess()532     public void testPostProcess() {
533         // Compare post processing a Rect in the middle of the (not-animating)
534         // image with drawing manually. They should be exactly the same.
535         BiFunction<Integer, Integer, Rect> rectCreator = (width, height) -> {
536             int quarterWidth  = width  / 4;
537             int quarterHeight = height / 4;
538             return new Rect(quarterWidth, quarterHeight,
539                     3 * quarterWidth, 3 * quarterHeight);
540         };
541 
542         AnimatedImageDrawable drawable = createFromImageDecoder(RES_ID);
543         Bitmap expected = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
544                 drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
545 
546         Paint paint = new Paint();
547         paint.setColor(Color.RED);
548 
549         {
550             Rect r = rectCreator.apply(drawable.getIntrinsicWidth(),
551                                        drawable.getIntrinsicHeight());
552             Canvas canvas = new Canvas(expected);
553             drawable.draw(canvas);
554 
555             for (int i = r.left; i < r.right; ++i) {
556                 for (int j = r.top; j < r.bottom; ++j) {
557                     assertNotEquals(Color.RED, expected.getPixel(i, j));
558                 }
559             }
560 
561             canvas.drawRect(r, paint);
562 
563             for (int i = r.left; i < r.right; ++i) {
564                 for (int j = r.top; j < r.bottom; ++j) {
565                     assertEquals(Color.RED, expected.getPixel(i, j));
566                 }
567             }
568         }
569 
570 
571         AnimatedImageDrawable testDrawable = null;
572         Uri uri = null;
573         try {
574             uri = Utils.getAsResourceUri(RES_ID);
575             ImageDecoder.Source source = ImageDecoder.createSource(getContentResolver(), uri);
576             Drawable dr = ImageDecoder.decodeDrawable(source, (decoder, info, src) -> {
577                 decoder.setPostProcessor((canvas) -> {
578                     canvas.drawRect(rectCreator.apply(canvas.getWidth(),
579                                                       canvas.getHeight()), paint);
580                     return PixelFormat.TRANSLUCENT;
581                 });
582             });
583             assertTrue(dr instanceof AnimatedImageDrawable);
584             testDrawable = (AnimatedImageDrawable) dr;
585         } catch (IOException e) {
586             fail("failed to create image from " + uri);
587         }
588 
589         Bitmap actual = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
590                 drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
591 
592         {
593             Canvas canvas = new Canvas(actual);
594             testDrawable.draw(canvas);
595         }
596 
597         assertTrue(BitmapUtils.compareBitmaps(expected, actual));
598     }
599 
600     @Test
testCreateFromXml()601     public void testCreateFromXml() throws XmlPullParserException, IOException {
602         Resources res = getResources();
603         XmlPullParser parser = res.getXml(R.drawable.animatedimagedrawable_tag);
604         Drawable drawable = Drawable.createFromXml(res, parser);
605         assertNotNull(drawable);
606         assertTrue(drawable instanceof AnimatedImageDrawable);
607     }
608 
609     @Test
testCreateFromXmlClass()610     public void testCreateFromXmlClass() throws XmlPullParserException, IOException {
611         Resources res = getResources();
612         XmlPullParser parser = res.getXml(R.drawable.animatedimagedrawable);
613         Drawable drawable = Drawable.createFromXml(res, parser);
614         assertNotNull(drawable);
615         assertTrue(drawable instanceof AnimatedImageDrawable);
616     }
617 
618     @Test
testCreateFromXmlClassAttribute()619     public void testCreateFromXmlClassAttribute() throws XmlPullParserException, IOException {
620         Resources res = getResources();
621         XmlPullParser parser = res.getXml(R.drawable.animatedimagedrawable_class);
622         Drawable drawable = Drawable.createFromXml(res, parser);
623         assertNotNull(drawable);
624         assertTrue(drawable instanceof AnimatedImageDrawable);
625     }
626 
627     @Test(expected=XmlPullParserException.class)
testMissingSrcInflate()628     public void testMissingSrcInflate() throws XmlPullParserException, IOException  {
629         Resources res = getResources();
630         XmlPullParser parser = res.getXml(R.drawable.animatedimagedrawable_nosrc);
631         Drawable drawable = Drawable.createFromXml(res, parser);
632     }
633 
634     @Test
testAutoMirrored()635     public void testAutoMirrored() {
636         AnimatedImageDrawable drawable = createFromImageDecoder(RES_ID);
637         assertFalse(drawable.isAutoMirrored());
638 
639         drawable.setAutoMirrored(true);
640         assertTrue(drawable.isAutoMirrored());
641 
642         drawable.setAutoMirrored(false);
643         assertFalse(drawable.isAutoMirrored());
644     }
645 
646     @Test
testAutoMirroredFromXml()647     public void testAutoMirroredFromXml() throws XmlPullParserException, IOException {
648         AnimatedImageDrawable drawable = parseXml(R.drawable.animatedimagedrawable_tag);
649         assertFalse(drawable.isAutoMirrored());
650 
651         drawable = parseXml(R.drawable.animatedimagedrawable_automirrored);
652         assertTrue(drawable.isAutoMirrored());
653     }
654 
parseXml(int resId)655     private AnimatedImageDrawable parseXml(int resId) throws XmlPullParserException, IOException {
656         Resources res = getResources();
657         XmlPullParser parser = res.getXml(resId);
658         Drawable drawable = Drawable.createFromXml(res, parser);
659         assertNotNull(drawable);
660         assertTrue(drawable instanceof AnimatedImageDrawable);
661         return (AnimatedImageDrawable) drawable;
662     }
663 
664     @Test
testAutoStartFromXml()665     public void testAutoStartFromXml() throws XmlPullParserException, IOException {
666         AnimatedImageDrawable drawable = parseXml(R.drawable.animatedimagedrawable_tag);
667         assertFalse(drawable.isRunning());
668 
669         drawable = parseXml(R.drawable.animatedimagedrawable_autostart_false);
670         assertFalse(drawable.isRunning());
671 
672         drawable = parseXml(R.drawable.animatedimagedrawable_autostart);
673         assertTrue(drawable.isRunning());
674     }
675 
drawAndCompare(Bitmap expected, Drawable drawable)676     private void drawAndCompare(Bitmap expected, Drawable drawable) {
677         Bitmap test = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
678                 drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
679         Canvas canvas = new Canvas(test);
680         drawable.draw(canvas);
681         assertTrue(BitmapUtils.compareBitmaps(expected, test));
682     }
683 
684     @Test
testAutoMirroredDrawing()685     public void testAutoMirroredDrawing() {
686         AnimatedImageDrawable drawable = createFromImageDecoder(RES_ID);
687         assertFalse(drawable.isAutoMirrored());
688 
689         final int width = drawable.getIntrinsicWidth();
690         final int height = drawable.getIntrinsicHeight();
691         Bitmap normal = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
692         {
693             Canvas canvas = new Canvas(normal);
694             drawable.draw(canvas);
695         }
696 
697         Bitmap flipped = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
698         {
699             Canvas canvas = new Canvas(flipped);
700             canvas.translate(width, 0);
701             canvas.scale(-1, 1);
702             drawable.draw(canvas);
703         }
704 
705         for (int i = 0; i < width; ++i) {
706             for (int j = 0; j < height; ++j) {
707                 assertEquals(normal.getPixel(i, j), flipped.getPixel(width - 1 - i, j));
708             }
709         }
710 
711         drawable.setAutoMirrored(true);
712         drawAndCompare(normal, drawable);
713 
714         drawable.setLayoutDirection(View.LAYOUT_DIRECTION_RTL);
715         drawAndCompare(flipped, drawable);
716 
717         drawable.setAutoMirrored(false);
718         drawAndCompare(normal, drawable);
719     }
720 
721     @Test
testRepeatCountFromXml()722     public void testRepeatCountFromXml() throws XmlPullParserException, IOException {
723         Resources res = getResources();
724         XmlPullParser parser = res.getXml(R.drawable.animatedimagedrawable_loop_count);
725         Drawable drawable = Drawable.createFromXml(res, parser);
726         assertNotNull(drawable);
727         assertTrue(drawable instanceof AnimatedImageDrawable);
728 
729         AnimatedImageDrawable aid = (AnimatedImageDrawable) drawable;
730         assertEquals(17, aid.getRepeatCount());
731     }
732 
733     @Test
testInfiniteRepeatCountFromXml()734     public void testInfiniteRepeatCountFromXml() throws XmlPullParserException, IOException {
735         // This image has an encoded repeat count of 1. Verify that.
736         Resources res = getResources();
737         Drawable drawable = res.getDrawable(R.drawable.animated_one_loop);
738         assertNotNull(drawable);
739         assertTrue(drawable instanceof AnimatedImageDrawable);
740         AnimatedImageDrawable aid = (AnimatedImageDrawable) drawable;
741         assertEquals(1, aid.getRepeatCount());
742 
743         // This layout uses the same image and overrides the repeat count to infinity.
744         XmlPullParser parser = res.getXml(R.drawable.animatedimagedrawable_loop_count_infinite);
745         drawable = Drawable.createFromXml(res, parser);
746         assertNotNull(drawable);
747         assertTrue(drawable instanceof AnimatedImageDrawable);
748 
749         aid = (AnimatedImageDrawable) drawable;
750         assertEquals(AnimatedImageDrawable.REPEAT_INFINITE, aid.getRepeatCount());
751     }
752 
753     // Verify that decoding on the AnimatedImageThread works.
decodeInBackground(AnimatedImageDrawable drawable)754     private void decodeInBackground(AnimatedImageDrawable drawable) throws Throwable {
755         final Callback cb = new Callback(drawable);
756         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mImageView, () -> {
757             mImageView.setImageDrawable(drawable);
758 
759             drawable.registerAnimationCallback(cb);
760             drawable.start();
761         });
762 
763         // The first frame was decoded in the thread that created the
764         // AnimatedImageDrawable. Wait long enough to decode further threads on
765         // the AnimatedImageThread, which was not created with a JNI interface
766         // pointer.
767         cb.waitForStart();
768         cb.waitForEnd(DURATION * 2);
769     }
770 
771     @Test
testInputStream()772     public void testInputStream() throws Throwable {
773         setupActivity();
774         Resources res = getResources();
775         try (InputStream in = res.openRawResource(R.drawable.animated)) {
776             ImageDecoder.Source src =
777                     ImageDecoder.createSource(res, in, Bitmap.DENSITY_NONE);
778             AnimatedImageDrawable drawable =
779                     (AnimatedImageDrawable) ImageDecoder.decodeDrawable(src);
780             decodeInBackground(drawable);
781         }
782 
783     }
784 
getAsByteArray()785     private byte[] getAsByteArray() {
786         ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
787         try (InputStream in = getResources().openRawResource(RES_ID)) {
788             byte[] buf = new byte[4096];
789             int bytesRead;
790             while ((bytesRead = in.read(buf)) != -1) {
791                 outputStream.write(buf, 0, bytesRead);
792             }
793         } catch (IOException e) {
794             fail("Failed to read resource: " + e);
795         }
796 
797         return outputStream.toByteArray();
798     }
799 
getAsDirectByteBuffer()800     private ByteBuffer getAsDirectByteBuffer() {
801         byte[] array = getAsByteArray();
802         ByteBuffer byteBuffer = ByteBuffer.allocateDirect(array.length);
803         byteBuffer.put(array);
804         byteBuffer.position(0);
805         return byteBuffer;
806     }
807 
createFromByteBuffer(ByteBuffer byteBuffer)808     private AnimatedImageDrawable createFromByteBuffer(ByteBuffer byteBuffer) {
809         ImageDecoder.Source src = ImageDecoder.createSource(byteBuffer);
810         try {
811             return (AnimatedImageDrawable) ImageDecoder.decodeDrawable(src);
812         } catch (IOException e) {
813             fail("Failed to create decoder: " + e);
814             return null;
815         }
816     }
817 
818     @Test
testByteBuffer()819     public void testByteBuffer() throws Throwable {
820         setupActivity();
821         // Natively, this tests ByteArrayStream.
822         byte[] array = getAsByteArray();
823         ByteBuffer byteBuffer = ByteBuffer.wrap(array);
824         final AnimatedImageDrawable drawable = createFromByteBuffer(byteBuffer);
825         decodeInBackground(drawable);
826     }
827 
828     @Test
testReadOnlyByteBuffer()829     public void testReadOnlyByteBuffer() throws Throwable {
830         setupActivity();
831         // Natively, this tests ByteBufferStream.
832         byte[] array = getAsByteArray();
833         ByteBuffer byteBuffer = ByteBuffer.wrap(array).asReadOnlyBuffer();
834         final AnimatedImageDrawable drawable = createFromByteBuffer(byteBuffer);
835         decodeInBackground(drawable);
836     }
837 
838     @Test
testDirectByteBuffer()839     public void testDirectByteBuffer() throws Throwable {
840         setupActivity();
841         ByteBuffer byteBuffer = getAsDirectByteBuffer();
842         final AnimatedImageDrawable drawable = createFromByteBuffer(byteBuffer);
843         decodeInBackground(drawable);
844     }
845 }
846