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