1 /*
2  * Copyright (C) 2016 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 junit.framework.TestCase.assertTrue;
20 
21 import static org.junit.Assert.assertEquals;
22 import static org.junit.Assert.fail;
23 
24 import android.app.Activity;
25 import android.content.res.Resources;
26 import android.graphics.Bitmap;
27 import android.graphics.Canvas;
28 import android.graphics.Color;
29 import android.graphics.Rect;
30 import android.graphics.cts.R;
31 import android.graphics.drawable.AnimatedVectorDrawable;
32 import android.util.Log;
33 import android.view.PixelCopy;
34 import android.view.View;
35 import android.widget.ImageView;
36 
37 import androidx.test.InstrumentationRegistry;
38 import androidx.test.filters.FlakyTest;
39 import androidx.test.filters.LargeTest;
40 import androidx.test.filters.SmallTest;
41 import androidx.test.rule.ActivityTestRule;
42 
43 import com.android.compatibility.common.util.OverrideAnimationScaleRule;
44 import com.android.compatibility.common.util.SynchronousPixelCopy;
45 import com.android.compatibility.common.util.SystemUtil;
46 import com.android.compatibility.common.util.WidgetTestUtils;
47 
48 import org.junit.AfterClass;
49 import org.junit.Assert;
50 import org.junit.Before;
51 import org.junit.BeforeClass;
52 import org.junit.Rule;
53 import org.junit.Test;
54 import org.junit.runner.RunWith;
55 import org.junit.runners.Parameterized;
56 
57 import java.util.concurrent.CountDownLatch;
58 import java.util.concurrent.TimeUnit;
59 
60 @LargeTest
61 @RunWith(Parameterized.class)
62 public class AnimatedVectorDrawableParameterizedTest {
63     @Rule
64     public ActivityTestRule<DrawableStubActivity> mActivityRule =
65             new ActivityTestRule<>(DrawableStubActivity.class);
66 
67     // Some of these tests require animations to work normally, which may not be the case
68     // depending on the current state of the test device
69     @Rule
70     public final OverrideAnimationScaleRule animationScaleRule =
71             new OverrideAnimationScaleRule(1f);
72 
73     private static final int IMAGE_WIDTH = 64;
74     private static final int IMAGE_HEIGHT = 64;
75     private static final long MAX_TIMEOUT_MS = 1000;
76 
77     private static float sTransitionScaleBefore = Float.NaN;
78 
79     private Activity mActivity = null;
80     private Resources mResources = null;
81     private final int mLayerType;
82 
83     @Parameterized.Parameters
data()84     public static Object[] data() {
85         return new Object[] {
86                 View.LAYER_TYPE_HARDWARE,
87                 View.LAYER_TYPE_NONE,
88                 View.LAYER_TYPE_SOFTWARE
89         };
90     }
91 
92     @BeforeClass
setUpClass()93     public static void setUpClass() throws Exception {
94         try {
95             sTransitionScaleBefore = Float.parseFloat(SystemUtil.runShellCommand(
96                     InstrumentationRegistry.getInstrumentation(),
97                     "settings get global transition_animation_scale"));
98 
99             SystemUtil.runShellCommand(InstrumentationRegistry.getInstrumentation(),
100                     "settings put global transition_animation_scale 0");
101         } catch (NumberFormatException e) {
102             Log.e("AnimatedVectorDrawableTest", "Could not read transition_animation_scale", e);
103             sTransitionScaleBefore = Float.NaN;
104         }
105     }
106 
107     @AfterClass
tearDownClass()108     public static void tearDownClass() throws Exception {
109         if (!Float.isNaN(sTransitionScaleBefore)) {
110             SystemUtil.runShellCommand(InstrumentationRegistry.getInstrumentation(),
111                     "settings put global transition_animation_scale " +
112                             sTransitionScaleBefore);
113         }
114     }
115 
AnimatedVectorDrawableParameterizedTest(final int layerType)116     public AnimatedVectorDrawableParameterizedTest(final int layerType) throws Throwable {
117         mLayerType = layerType;
118     }
119 
120     @Before
setup()121     public void setup() {
122         mActivity = mActivityRule.getActivity();
123         mResources = mActivity.getResources();
124     }
125 
126     @Test
127     @FlakyTest (bugId = 72737527)
testAnimationOnLayer()128     public void testAnimationOnLayer() throws Throwable {
129         final Animatable2Callback callback = new Animatable2Callback();
130         // Can't simply use final here, b/c it needs to be initialized and referred later in UI
131         // thread.
132         final ImageView[] imageView = new ImageView[1];
133         mActivityRule.runOnUiThread(() -> {
134             mActivity.setContentView(R.layout.fixed_sized_imageview);
135             imageView[0] = (ImageView) mActivity.findViewById(R.id.imageview);
136         });
137         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, imageView[0],
138                 (Runnable) () -> {
139                     imageView[0].setImageDrawable(
140                             mResources.getDrawable(R.drawable.animated_vector_favorite));
141                     imageView[0].setLayerType(mLayerType, null);
142                     AnimatedVectorDrawable avd =
143                             (AnimatedVectorDrawable) imageView[0].getDrawable();
144                     avd.registerAnimationCallback(callback);
145                     avd.start();
146                 });
147         callback.waitForStart();
148         waitWhilePumpingFrames(5, imageView[0], 200);
149 
150         Bitmap lastScreenShot = null;
151         final Rect srcRect = new Rect();
152         mActivityRule.runOnUiThread(() -> {
153             imageView[0].getGlobalVisibleRect(srcRect);
154         });
155 
156         int counter = 0;
157         while (!callback.endIsCalled()) {
158             // Take a screen shot every 50ms, and compare with previous screenshot for the ImageView
159             // content, to make sure the AVD is animating when set on HW layer.
160             Bitmap screenShot = takeScreenshot(srcRect);
161             if (callback.endIsCalled()) {
162                 // Animation already ended, the screenshot may not contain valid animation content,
163                 // skip the comparison.
164                 break;
165             }
166             counter++;
167             boolean isIdentical = isAlmostIdenticalInRect(screenShot, lastScreenShot);
168             if (isIdentical) {
169                 String outputFolder = mActivity.getExternalFilesDir(null).getAbsolutePath();
170                 DrawableTestUtils.saveVectorDrawableIntoPNG(screenShot, outputFolder,
171                         "screenshot_" + counter);
172                 DrawableTestUtils.saveVectorDrawableIntoPNG(lastScreenShot, outputFolder,
173                         "screenshot_" + (counter - 1));
174                 fail("Two consecutive screenshots of AVD are identical, AVD is "
175                         + "likely not animating");
176             }
177             lastScreenShot = screenShot;
178 
179             // Wait 50ms before the next screen shot. If animation ended during the wait, exit the
180             // loop.
181             if (callback.waitForEnd(50)) {
182                 break;
183             }
184         }
185         // In this test, we want to make sure that we at least have 5 screenshots.
186         assertTrue(counter >= 5);
187 
188         mActivityRule.runOnUiThread(() -> {
189             AnimatedVectorDrawable avd = (AnimatedVectorDrawable) imageView[0].getDrawable();
190             avd.stop();
191         });
192     }
193 
194     // Pump frames by repeatedly invalidating the given view. Return true if successfully pumped
195     // the given number of frames before timeout, false otherwise.
waitWhilePumpingFrames(int frameCount, final View view, long timeout)196     private boolean waitWhilePumpingFrames(int frameCount, final View view, long timeout)
197             throws Throwable {
198         final CountDownLatch frameLatch = new CountDownLatch(frameCount);
199         mActivityRule.runOnUiThread(() -> {
200             view.getViewTreeObserver().addOnPreDrawListener(() -> {
201                 if (frameLatch.getCount() > 0) {
202                     frameLatch.countDown();
203                     view.postInvalidate();
204                 }
205                 return true;
206             });
207         });
208         return frameLatch.await(timeout, TimeUnit.MILLISECONDS);
209     }
210 
211     @SmallTest
212     @Test
testSingleFrameAnimation()213     public void testSingleFrameAnimation() throws Throwable {
214         int resId = R.drawable.avd_single_frame;
215         final AnimatedVectorDrawable d1 =
216                 (AnimatedVectorDrawable) mResources.getDrawable(resId);
217         // The AVD has a duration as 16ms.
218         mActivityRule.runOnUiThread(() -> {
219             Bitmap bitmap =
220                     Bitmap.createBitmap(IMAGE_WIDTH, IMAGE_HEIGHT, Bitmap.Config.ARGB_8888);
221             Canvas canvas = new Canvas(bitmap);
222 
223             mActivity.setContentView(R.layout.animated_vector_drawable_source);
224             ImageView imageView = (ImageView) mActivity.findViewById(R.id.avd_view);
225             imageView.setLayerType(mLayerType, null);
226             imageView.setImageDrawable(d1);
227             d1.start();
228             d1.stop();
229             d1.setBounds(0, 0, IMAGE_WIDTH, IMAGE_HEIGHT);
230             bitmap.eraseColor(0);
231             d1.draw(canvas);
232             int endColor = bitmap.getPixel(IMAGE_WIDTH / 2, IMAGE_HEIGHT / 2);
233             assertEquals("Center point's color must be green", 0xFF00FF00, endColor);
234         });
235     }
236 
237     @LargeTest
238     @Test
testEmptyAnimatorSet()239     public void testEmptyAnimatorSet() throws Throwable {
240         int resId = R.drawable.avd_empty_animator;
241         final Animatable2Callback callback = new Animatable2Callback();
242         final AnimatedVectorDrawable d1 =
243                 (AnimatedVectorDrawable) mResources.getDrawable(resId);
244         d1.registerAnimationCallback(callback);
245         mActivityRule.runOnUiThread(() -> {
246             mActivity.setContentView(R.layout.animated_vector_drawable_source);
247             ImageView imageView = (ImageView) mActivity.findViewById(R.id.avd_view);
248             imageView.setLayerType(mLayerType, null);
249             imageView.setImageDrawable(d1);
250             d1.registerAnimationCallback(callback);
251             d1.start();
252         });
253         Assert.assertTrue(callback.waitForStart());
254         AnimatedVectorDrawableTest.waitForAVDStop(callback, MAX_TIMEOUT_MS);
255         // Check that the AVD with empty AnimatorSet has finished
256         callback.assertEnded(true);
257         callback.assertAVDRuntime(0, TimeUnit.MILLISECONDS.toNanos(900));
258     }
259 
260     // Does a fuzzy comparison between two images.
isAlmostIdenticalInRect(Bitmap image1, Bitmap image2)261     private static boolean isAlmostIdenticalInRect(Bitmap image1, Bitmap image2) {
262         if (image1 == null || image2 == null) {
263             return false;
264         }
265 
266         if (image1.getWidth() != image2.getWidth() || image1.getHeight() != image2.getHeight()) {
267             throw new IllegalArgumentException("Images size are not the same. image1:" + image1
268                     + "image2:" + image2);
269         }
270 
271         Rect rangeRect = new Rect(0, 0, image1.getWidth(), image1.getHeight());
272 
273         for (int x = rangeRect.left; x < rangeRect.right; x++) {
274             for (int y = rangeRect.top; y < rangeRect.bottom; y++) {
275                 int color1 = image1.getPixel(x, y);
276                 int color2 = image2.getPixel(x, y);
277                 int rDiff = Math.abs(Color.red(color1) - Color.red(color2));
278                 int gDiff = Math.abs(Color.green(color1) - Color.green(color2));
279                 int bDiff = Math.abs(Color.blue(color1) - Color.blue(color2));
280                 if (rDiff + gDiff + bDiff > 8) {
281                     return false;
282                 }
283             }
284         }
285         return true;
286     }
287 
288     @Test
289     @FlakyTest (bugId = 72737527)
testInfiniteAVD()290     public void testInfiniteAVD() throws Throwable {
291         final Animatable2Callback callback = new Animatable2Callback();
292         // Can't simply use final here, b/c it needs to be initialized and referred later in UI
293         // thread.
294         final ImageView[] imageView = new ImageView[1];
295         mActivityRule.runOnUiThread(() -> {
296             mActivity.setContentView(R.layout.fixed_sized_imageview);
297             imageView[0] = (ImageView) mActivity.findViewById(R.id.imageview);
298         });
299         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, imageView[0],
300                 (Runnable) () -> {
301                     imageView[0].setImageDrawable(mResources.getDrawable(R.drawable.infinite_avd));
302                     imageView[0].setLayerType(mLayerType, null);
303                     AnimatedVectorDrawable avd = (AnimatedVectorDrawable) imageView[0].getDrawable();
304                     avd.registerAnimationCallback(callback);
305 
306                     avd.start();
307                 });
308 
309         callback.waitForStart();
310         waitWhilePumpingFrames(5, imageView[0], 200);
311         Bitmap lastScreenShot = null;
312         final Rect srcRect = new Rect();
313         mActivityRule.runOnUiThread(() -> {
314             mActivity.findViewById(R.id.imageview).getGlobalVisibleRect(srcRect);
315         });
316 
317         for (int counter = 0; counter < 10; counter++) {
318             // Take a screen shot every 100ms, and compare with previous screenshot for the ImageView
319             // content, to make sure the AVD is animating when set on HW layer.
320             Bitmap screenShot = takeScreenshot(srcRect);
321             boolean isIdentical = isAlmostIdenticalInRect(screenShot, lastScreenShot);
322             if (isIdentical) {
323                 String outputFolder = mActivity.getExternalFilesDir(null).getAbsolutePath();
324                 DrawableTestUtils.saveVectorDrawableIntoPNG(screenShot, outputFolder,
325                         "inf_avd_screenshot_" + mLayerType + "_" + counter);
326                 DrawableTestUtils.saveVectorDrawableIntoPNG(lastScreenShot, outputFolder,
327                         "inf_avd_screenshot_" + mLayerType + "_" + (counter - 1));
328                 fail("Two consecutive screenshots of AVD are identical, AVD is "
329                         + "likely not animating");
330             }
331             lastScreenShot = screenShot;
332             counter++;
333 
334             // Wait 100ms before the next screen shot. If animation ended during the wait, fail the
335             // test, as the infinite avd should not end until we call stop().
336             if (callback.waitForEnd(100)) {
337                 fail("Infinite AnimatedVectorDrawable should not end on its own.");
338             }
339         }
340         Assert.assertFalse(callback.endIsCalled());
341         mActivityRule.runOnUiThread(() -> {
342             AnimatedVectorDrawable avd = (AnimatedVectorDrawable) imageView[0].getDrawable();
343             avd.stop();
344         });
345     }
346 
347     // Copy the source rectangle from the screen into the returned bitmap.
takeScreenshot(Rect srcRect)348     private Bitmap takeScreenshot(Rect srcRect) {
349         SynchronousPixelCopy copy = new SynchronousPixelCopy();
350         Bitmap dest = Bitmap.createBitmap(
351                 srcRect.width(), srcRect.height(), Bitmap.Config.ARGB_8888);
352         int copyResult = copy.request(mActivity.getWindow(), srcRect, dest);
353         Assert.assertEquals(PixelCopy.SUCCESS, copyResult);
354         return dest;
355     }
356 }
357