• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 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.server.wm;
18 
19 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
20 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
21 import static android.server.wm.ActivityTransitionTests.EdgeExtensionActivity.BOTTOM;
22 import static android.server.wm.ActivityTransitionTests.EdgeExtensionActivity.DIRECTION_KEY;
23 import static android.server.wm.ActivityTransitionTests.EdgeExtensionActivity.LEFT;
24 import static android.server.wm.ActivityTransitionTests.EdgeExtensionActivity.RIGHT;
25 import static android.server.wm.ActivityTransitionTests.EdgeExtensionActivity.TOP;
26 import static android.server.wm.ActivityTransitionTests.OverridePendingTransitionActivity.BACKGROUND_COLOR_KEY;
27 import static android.server.wm.ActivityTransitionTests.OverridePendingTransitionActivity.ENTER_ANIM_KEY;
28 import static android.server.wm.ActivityTransitionTests.OverridePendingTransitionActivity.EXIT_ANIM_KEY;
29 import static android.server.wm.app.Components.TEST_ACTIVITY;
30 import static android.view.Display.DEFAULT_DISPLAY;
31 import static android.view.RoundedCorner.POSITION_BOTTOM_LEFT;
32 import static android.view.RoundedCorner.POSITION_BOTTOM_RIGHT;
33 import static android.view.RoundedCorner.POSITION_TOP_LEFT;
34 import static android.view.RoundedCorner.POSITION_TOP_RIGHT;
35 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
36 
37 import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
38 
39 import static org.junit.Assert.assertTrue;
40 import static org.junit.Assert.fail;
41 
42 import android.app.Activity;
43 import android.app.ActivityOptions;
44 import android.app.Instrumentation;
45 import android.content.ComponentName;
46 import android.content.Intent;
47 import android.graphics.Bitmap;
48 import android.graphics.Color;
49 import android.graphics.ColorSpace;
50 import android.graphics.Insets;
51 import android.graphics.Point;
52 import android.graphics.Rect;
53 import android.os.Bundle;
54 import android.os.Handler;
55 import android.os.Looper;
56 import android.os.SystemClock;
57 import android.platform.test.annotations.Presubmit;
58 import android.server.wm.cts.R;
59 import android.util.Range;
60 import android.view.RoundedCorner;
61 import android.view.View;
62 import android.view.ViewGroup;
63 import android.view.WindowInsets;
64 
65 import androidx.annotation.Nullable;
66 import androidx.test.platform.app.InstrumentationRegistry;
67 
68 import org.junit.After;
69 import org.junit.Before;
70 import org.junit.Test;
71 
72 import java.io.IOException;
73 import java.util.ArrayList;
74 import java.util.concurrent.CountDownLatch;
75 import java.util.concurrent.TimeUnit;
76 import java.util.concurrent.atomic.AtomicLong;
77 import java.util.function.Function;
78 
79 /**
80  * <p>Build/Install/Run:
81  * atest CtsWindowManagerDeviceTestCases:ActivityTransitionTests
82  */
83 @Presubmit
84 public class ActivityTransitionTests extends ActivityManagerTestBase {
85     // Duration of the R.anim.alpha animation.
86     private static final long CUSTOM_ANIMATION_DURATION = 2000L;
87 
88     // Allowable range with error error for the R.anim.alpha animation duration.
89     private static final Range<Long> CUSTOM_ANIMATION_DURATION_RANGE = new Range<>(
90             CUSTOM_ANIMATION_DURATION - 200L, CUSTOM_ANIMATION_DURATION + 1000L);
91 
92     private boolean mAnimationScaleResetRequired = false;
93     private String mInitialWindowAnimationScale;
94     private String mInitialTransitionAnimationScale;
95     private String mInitialAnimatorDurationScale;
96 
97     // We need to allow for some variation stemming from color conversions
98     private static final float COLOR_VALUE_VARIANCE_TOLERANCE = 0.03f;
99 
100     @Before
setUp()101     public void setUp() throws Exception {
102         super.setUp();
103         setDefaultAnimationScale();
104         mWmState.setSanityCheckWithFocusedWindow(false);
105         mWmState.waitForDisplayUnfrozen();
106     }
107 
108     @After
tearDown()109     public void tearDown() {
110         restoreAnimationScale();
111         mWmState.setSanityCheckWithFocusedWindow(true);
112     }
113 
startLauncherActivity()114     private LauncherActivity startLauncherActivity() {
115         final Intent intent = new Intent(mContext, LauncherActivity.class)
116                 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
117         final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
118         final ActivityOptions options = ActivityOptions.makeBasic();
119         options.setLaunchWindowingMode(WINDOWING_MODE_FULLSCREEN);
120         return (LauncherActivity) instrumentation.startActivitySync(intent, options.toBundle());
121     }
122 
123     @Test
testActivityTransitionOverride()124     public void testActivityTransitionOverride() throws Exception {
125         final CountDownLatch latch = new CountDownLatch(1);
126         AtomicLong transitionStartTime = new AtomicLong();
127         AtomicLong transitionEndTime = new AtomicLong();
128 
129         final ActivityOptions.OnAnimationStartedListener startedListener = transitionStartTime::set;
130         final ActivityOptions.OnAnimationFinishedListener finishedListener = (t) -> {
131             transitionEndTime.set(t);
132             latch.countDown();
133         };
134 
135         final LauncherActivity launcherActivity = startLauncherActivity();
136 
137         final ActivityOptions options = ActivityOptions.makeCustomAnimation(mContext,
138                 R.anim.alpha, 0 /* exitResId */, 0 /* backgroundColor */,
139                 new Handler(Looper.getMainLooper()), startedListener, finishedListener);
140         launcherActivity.startActivity(options, TransitionActivity.class);
141         mWmState.waitForAppTransitionIdleOnDisplay(DEFAULT_DISPLAY);
142         waitAndAssertTopResumedActivity(new ComponentName(mContext, TransitionActivity.class),
143                 DEFAULT_DISPLAY, "Activity must be launched");
144 
145         latch.await(5, TimeUnit.SECONDS);
146         final long totalTime = transitionEndTime.get() - transitionStartTime.get();
147         assertTrue("Actual transition duration should be in the range "
148                 + "<" + CUSTOM_ANIMATION_DURATION_RANGE.getLower() + ", "
149                 + CUSTOM_ANIMATION_DURATION_RANGE.getUpper() + "> ms, "
150                 + "actual=" + totalTime, CUSTOM_ANIMATION_DURATION_RANGE.contains(totalTime));
151     }
152 
153     @Test
testTaskTransitionOverrideDisabled()154     public void testTaskTransitionOverrideDisabled() throws Exception {
155         final CountDownLatch latch = new CountDownLatch(1);
156         AtomicLong transitionStartTime = new AtomicLong();
157         AtomicLong transitionEndTime = new AtomicLong();
158 
159         final ActivityOptions.OnAnimationStartedListener startedListener = transitionStartTime::set;
160         final ActivityOptions.OnAnimationFinishedListener finishedListener = (t) -> {
161             transitionEndTime.set(t);
162             latch.countDown();
163         };
164 
165         // Overriding task transit animation is disabled, so default wallpaper close animation
166         // is played.
167         final Bundle bundle = ActivityOptions.makeCustomAnimation(mContext,
168                 R.anim.alpha, 0 /* exitResId */, 0 /* backgroundColor */,
169                 new Handler(Looper.getMainLooper()), startedListener, finishedListener).toBundle();
170         final Intent intent = new Intent().setComponent(TEST_ACTIVITY)
171                 .addFlags(FLAG_ACTIVITY_NEW_TASK);
172         mContext.startActivity(intent, bundle);
173         mWmState.waitForAppTransitionIdleOnDisplay(DEFAULT_DISPLAY);
174         waitAndAssertTopResumedActivity(TEST_ACTIVITY, DEFAULT_DISPLAY,
175                 "Activity must be launched");
176 
177         latch.await(5, TimeUnit.SECONDS);
178         final long totalTime = transitionEndTime.get() - transitionStartTime.get();
179         assertTrue("Actual transition duration should be out of the range "
180                 + "<" + CUSTOM_ANIMATION_DURATION_RANGE.getLower() + ", "
181                 + CUSTOM_ANIMATION_DURATION_RANGE.getUpper() + "> ms, "
182                 + "actual=" + totalTime, !CUSTOM_ANIMATION_DURATION_RANGE.contains(totalTime));
183     }
184 
185     @Test
testTaskWindowAnimationOverrideDisabled()186     public void testTaskWindowAnimationOverrideDisabled() throws Exception {
187         final CountDownLatch latch = new CountDownLatch(1);
188         AtomicLong transitionStartTime = new AtomicLong();
189         AtomicLong transitionEndTime = new AtomicLong();
190 
191         final ActivityOptions.OnAnimationStartedListener startedListener = transitionStartTime::set;
192         final ActivityOptions.OnAnimationFinishedListener finishedListener = (t) -> {
193             transitionEndTime.set(t);
194             latch.countDown();
195         };
196 
197         // Overriding task transit animation is disabled, so default wallpaper close animation
198         // is played.
199         final Bundle bundle = ActivityOptions.makeCustomAnimation(mContext,
200                 R.anim.alpha, 0 /* exitResId */, 0 /* backgroundColor */,
201                 new Handler(Looper.getMainLooper()), startedListener, finishedListener).toBundle();
202 
203         final ComponentName customWindowAnimationActivity = new ComponentName(
204                 mContext, CustomWindowAnimationActivity.class);
205         final Intent intent = new Intent().setComponent(customWindowAnimationActivity)
206                 .addFlags(FLAG_ACTIVITY_NEW_TASK);
207         mContext.startActivity(intent, bundle);
208         mWmState.waitForAppTransitionIdleOnDisplay(DEFAULT_DISPLAY);
209         waitAndAssertTopResumedActivity(customWindowAnimationActivity, DEFAULT_DISPLAY,
210                 "Activity must be launched");
211 
212         latch.await(5, TimeUnit.SECONDS);
213         final long totalTime = transitionEndTime.get() - transitionStartTime.get();
214         assertTrue("Actual transition duration should be out of the range "
215                 + "<" + CUSTOM_ANIMATION_DURATION_RANGE.getLower() + ", "
216                 + CUSTOM_ANIMATION_DURATION_RANGE.getUpper() + "> ms, "
217                 + "actual=" + totalTime, !CUSTOM_ANIMATION_DURATION_RANGE.contains(totalTime));
218     }
219 
220     /**
221      * Checks that the activity's theme's background color is used as the default animation's
222      * background color when no override is specified.
223      */
224     @Test
testThemeBackgroundColorShowsDuringActivityTransition()225     public void testThemeBackgroundColorShowsDuringActivityTransition() {
226         final int backgroundColor = Color.WHITE;
227         final TestBounds testBounds = getTestBounds();
228 
229         getTestBuilder().setClass(TransitionActivityWithWhiteBackground.class)
230                 .setTestFunction(createAssertAppRegionOfScreenIsColor(backgroundColor, testBounds))
231                 .run();
232     }
233 
234     /**
235      * Checks that the background color set in the animation definition is used as the animation's
236      * background color instead of the theme's background color.
237      *
238      * @see R.anim.alpha_0_with_red_backdrop for animation defintition.
239      */
240     @Test
testAnimationBackgroundColorIsUsedDuringActivityTransition()241     public void testAnimationBackgroundColorIsUsedDuringActivityTransition() {
242         final int backgroundColor = Color.RED;
243         final ActivityOptions activityOptions = ActivityOptions.makeCustomAnimation(mContext,
244                 R.anim.alpha_0_with_red_backdrop, R.anim.alpha_0_with_red_backdrop);
245         final TestBounds testBounds = getTestBounds();
246 
247         getTestBuilder().setClass(TransitionActivityWithWhiteBackground.class)
248                 .setActivityOptions(activityOptions)
249                 .setTestFunction(createAssertAppRegionOfScreenIsColor(backgroundColor, testBounds))
250                 .run();
251     }
252 
253     /**
254      * Checks that we can override the default background color of the animation using the
255      * CustomAnimation activityOptions.
256      */
257     @Test
testCustomTransitionCanOverrideBackgroundColor()258     public void testCustomTransitionCanOverrideBackgroundColor() {
259         final int backgroundColor = Color.GREEN;
260         final ActivityOptions activityOptions = ActivityOptions.makeCustomAnimation(mContext,
261                 R.anim.alpha_0_with_backdrop, R.anim.alpha_0_with_backdrop, backgroundColor
262         );
263         final TestBounds testBounds = getTestBounds();
264 
265         getTestBuilder().setClass(TransitionActivityWithWhiteBackground.class)
266                 .setActivityOptions(activityOptions)
267                 .setTestFunction(createAssertAppRegionOfScreenIsColor(backgroundColor, testBounds))
268                 .run();
269     }
270 
271     /**
272      * Checks that we can override the default background color of the animation through
273      * overridePendingTransition.
274      */
275     @Test
testPendingTransitionCanOverrideBackgroundColor()276     public void testPendingTransitionCanOverrideBackgroundColor() {
277         final int backgroundColor = Color.GREEN;
278 
279         final Bundle extras = new Bundle();
280         extras.putInt(ENTER_ANIM_KEY, R.anim.alpha_0_with_backdrop);
281         extras.putInt(EXIT_ANIM_KEY, R.anim.alpha_0_with_backdrop);
282         extras.putInt(BACKGROUND_COLOR_KEY, backgroundColor);
283         final TestBounds testBounds = getTestBounds();
284 
285         getTestBuilder().setClass(OverridePendingTransitionActivity.class).setExtras(extras)
286                 .setTestFunction(createAssertAppRegionOfScreenIsColor(backgroundColor, testBounds))
287                 .run();
288     }
289 
290     /**
291      * Checks that when an activity transition with a left edge extension is run that the animating
292      * activity is extended on the left side by clamping the edge pixels of the activity.
293      *
294      * The test runs an activity transition where the animating activities are X scaled to 50%,
295      * positioned of the right side of the screen, and edge extended on the left. Because the
296      * animating activities are half red half blue (split at the middle of the X axis of the
297      * activity). We expect first 75% pixel columns of the screen to be red (50% from the edge
298      * extension and the next 25% from from the activity) and the remaining 25% columns after that
299      * to be blue (from the activity).
300      *
301      * @see R.anim.edge_extension_left for the transition applied.
302      */
303     @Test
testLeftEdgeExtensionWorksDuringActivityTransition()304     public void testLeftEdgeExtensionWorksDuringActivityTransition() {
305         final Bundle extras = new Bundle();
306         extras.putInt(DIRECTION_KEY, LEFT);
307         final TestBounds testBounds = getTestBounds();
308         final Rect appBounds = testBounds.appBounds;
309         final int xIndex = appBounds.left + (appBounds.right - appBounds.left) * 3 / 4;
310         getTestBuilder().setClass(EdgeExtensionActivity.class).setExtras(extras)
311                 .setTestFunction(createAssertColorChangeXIndex(xIndex, testBounds))
312                 .run();
313     }
314 
315     /**
316      * Checks that when an activity transition with a top edge extension is run that the animating
317      * activity is extended on the left side by clamping the edge pixels of the activity.
318      *
319      * The test runs an activity transition where the animating activities are Y scaled to 50%,
320      * positioned of the bottom of the screen, and edge extended on the top. Because the
321      * animating activities are half red half blue (split at the middle of the X axis of the
322      * activity). We expect first 50% pixel columns of the screen to be red (the top half from the
323      * extension and the bottom half from the activity) and the remaining 50% columns after that
324      * to be blue (the top half from the extension and the bottom half from the activity).
325      *
326      * @see R.anim.edge_extension_top for the transition applied.
327      */
328     @Test
testTopEdgeExtensionWorksDuringActivityTransition()329     public void testTopEdgeExtensionWorksDuringActivityTransition() {
330         final Bundle extras = new Bundle();
331         extras.putInt(DIRECTION_KEY, TOP);
332         final TestBounds testBounds = getTestBounds();
333         final Rect appBounds = testBounds.appBounds;
334         final int xIndex = (appBounds.left + appBounds.right) / 2;
335         getTestBuilder().setClass(EdgeExtensionActivity.class).setExtras(extras)
336                 .setTestFunction(createAssertColorChangeXIndex(xIndex, testBounds))
337                 .run();
338     }
339 
340     /**
341      * Checks that when an activity transition with a right edge extension is run that the animating
342      * activity is extended on the right side by clamping the edge pixels of the activity.
343      *
344      * The test runs an activity transition where the animating activities are X scaled to 50% and
345      * edge extended on the right. Because the animating activities are half red half blue. We
346      * expect first 25% pixel columns of the screen to be red (from the activity) and the remaining
347      * 75% columns after that to be blue (25% from the activity and 50% from the edge extension
348      * which should be extending the right edge pixel (so red pixels).
349      *
350      * @see R.anim.edge_extension_right for the transition applied.
351      */
352     @Test
testRightEdgeExtensionWorksDuringActivityTransition()353     public void testRightEdgeExtensionWorksDuringActivityTransition() {
354         final Bundle extras = new Bundle();
355         extras.putInt(DIRECTION_KEY, RIGHT);
356         final TestBounds testBounds = getTestBounds();
357         final Rect appBounds = testBounds.appBounds;
358         final int xIndex = appBounds.left + (appBounds.right - appBounds.left) / 4;
359         getTestBuilder().setClass(EdgeExtensionActivity.class).setExtras(extras)
360                 .setTestFunction(createAssertColorChangeXIndex(xIndex, testBounds))
361                 .run();
362     }
363 
364     /**
365      * Checks that when an activity transition with a bottom edge extension is run that the
366      * animating activity is extended on the bottom side by clamping the edge pixels of the
367      * activity.
368      *
369      * The test runs an activity transition where the animating activities are Y scaled to 50%,
370      * positioned of the top of the screen, and edge extended on the bottom. Because the
371      * animating activities are half red half blue (split at the middle of the X axis of the
372      * activity). We expect first 50% pixel columns of the screen to be red (the top half from the
373      * activity and the bottom half from gthe extensions) and the remaining 50% columns after that
374      * to be blue (the top half from the activity and the bottom half from the extension).
375      *
376      * @see R.anim.edge_extension_bottom for the transition applied.
377      */
378     @Test
testBottomEdgeExtensionWorksDuringActivityTransition()379     public void testBottomEdgeExtensionWorksDuringActivityTransition() {
380         final Bundle extras = new Bundle();
381         extras.putInt(DIRECTION_KEY, BOTTOM);
382         final TestBounds testBounds = getTestBounds();
383         final Rect appBounds = testBounds.appBounds;
384         final int xIndex = (appBounds.left + appBounds.right) / 2;
385         getTestBuilder().setClass(EdgeExtensionActivity.class).setExtras(extras)
386                 .setTestFunction(createAssertColorChangeXIndex(xIndex, testBounds))
387                 .run();
388     }
389 
getTestBuilder()390     private TestBuilder getTestBuilder() {
391         return new TestBuilder();
392     }
393 
394     private class TestBuilder {
395         private ActivityOptions mActivityOptions = ActivityOptions.makeBasic();
396         private Bundle mExtras = Bundle.EMPTY;
397         private Class<?> mKlass;
398         private Function<Bitmap, AssertionResult> mTestFunction;
399 
setActivityOptions(ActivityOptions activityOptions)400         public TestBuilder setActivityOptions(ActivityOptions activityOptions) {
401             this.mActivityOptions = activityOptions;
402             return this;
403         }
404 
setExtras(Bundle extra)405         public TestBuilder setExtras(Bundle extra) {
406             this.mExtras = extra;
407             return this;
408         }
409 
setClass(Class<?> klass)410         public TestBuilder setClass(Class<?> klass) {
411             this.mKlass = klass;
412             return this;
413         }
414 
setTestFunction(Function<Bitmap, AssertionResult> testFunction)415         public TestBuilder setTestFunction(Function<Bitmap, AssertionResult> testFunction) {
416             this.mTestFunction = testFunction;
417             return this;
418         }
419 
run()420         public void run() {
421             runAndAssertActivityTransition(mActivityOptions, mKlass, mExtras, mTestFunction);
422         }
423     }
424 
425     private static class TestBounds {
426         public Rect rect;
427         public Rect appBounds;
428         public ArrayList<Rect> excluded;
429     }
430 
getTestBounds()431     private TestBounds getTestBounds() {
432         final LauncherActivity activity = startLauncherActivity();
433         final TestBounds bounds = new TestBounds();
434         bounds.rect = activity.getActivityFullyVisibleRegion();
435         bounds.appBounds = getTopAppBounds();
436         bounds.excluded = activity.getRoundedCornersRegions();
437         launchHomeActivityNoWait();
438         removeRootTasksWithActivityTypes(ALL_ACTIVITY_TYPE_BUT_HOME);
439         mWmState.waitForAppTransitionIdleOnDisplay(DEFAULT_DISPLAY);
440         return bounds;
441     }
442 
runAndAssertActivityTransition(ActivityOptions activityOptions, Class<?> klass, Bundle extras, Function<Bitmap, AssertionResult> assertFunction)443     private void runAndAssertActivityTransition(ActivityOptions activityOptions,
444                                                 Class<?> klass, Bundle extras,
445                                                 Function<Bitmap, AssertionResult> assertFunction) {
446         final LauncherActivity launcherActivity = startLauncherActivity();
447         launcherActivity.startActivity(activityOptions, klass, extras);
448 
449         // Busy wait until we are running the transition to capture the screenshot
450         boolean isTransitioning;
451         do {
452             getWmState().computeState();
453             isTransitioning = getWmState().getDefaultDisplayAppTransitionState()
454                             .equals("APP_STATE_RUNNING");
455             SystemClock.sleep(10);
456         } while (!isTransitioning);
457 
458         // Because of differences in timing between devices we try the given assert function
459         // by taking multiple screenshots approximately to ensure we capture at least one screenshot
460         // around the beginning of the activity transition.
461         // The Timing issue exists around the beginning, so we use a sleep duration that increases
462         // exponentially. The total amount of sleep duration is between 5 and 10 seconds, which
463         // matches the most common wait time in CTS (2^0 + 2^1 + ... + 2^13 = about 8000).
464         final ArrayList<AssertionResult> failedResults = new ArrayList<>();
465         int sleepDurationMilliseconds = 1;
466         for (int i = 0; i < 13; i++) {
467             final AssertionResult result = assertFunction.apply(
468                     mInstrumentation.getUiAutomation().takeScreenshot());
469             if (!result.isFailure) {
470                 return;
471             }
472             failedResults.add(result);
473             SystemClock.sleep(sleepDurationMilliseconds);
474             sleepDurationMilliseconds *= 2;
475         }
476 
477         fail("No screenshot of the activity transition passed the assertions ::\n"
478                 + String.join(",\n", failedResults.stream().map(Object::toString)
479                 .toArray(String[]::new)));
480     }
481 
rectsContain(ArrayList<Rect> rect, int x, int y)482     private boolean rectsContain(ArrayList<Rect> rect, int x, int y) {
483         for (Rect r : rect) {
484             if (r.contains(x, y)) {
485                 return true;
486             }
487         }
488         return false;
489     }
490 
createAssertAppRegionOfScreenIsColor(int color, TestBounds testBounds)491     private Function<Bitmap, AssertionResult> createAssertAppRegionOfScreenIsColor(int color,
492             TestBounds testBounds) {
493         return (screen) -> getIsAppRegionOfScreenOfColorResult(screen, color, testBounds);
494     }
495 
496     private static class ColorCheckResult extends AssertionResult {
497         public final Point firstWrongPixel;
498         public final Color expectedColor;
499         public final Color actualColor;
500 
ColorCheckResult(boolean isFailure, Point firstWrongPixel, Color expectedColor, Color actualColor)501         private ColorCheckResult(boolean isFailure, Point firstWrongPixel, Color expectedColor,
502                 Color actualColor) {
503             super(isFailure);
504             this.firstWrongPixel = firstWrongPixel;
505             this.expectedColor = expectedColor;
506             this.actualColor = actualColor;
507         }
508 
ColorCheckResult(Point firstWrongPixel, Color expectedColor, Color actualColor)509         private ColorCheckResult(Point firstWrongPixel, Color expectedColor, Color actualColor) {
510             this(true, firstWrongPixel, expectedColor, actualColor);
511         }
512 
513         @Override
toString()514         public String toString() {
515             return "ColorCheckResult{"
516                     + "isFailure=" + isFailure
517                     + ", firstWrongPixel=" + firstWrongPixel
518                     + ", expectedColor=" + expectedColor
519                     + ", actualColor=" + actualColor
520                     + '}';
521         }
522     }
523 
getIsAppRegionOfScreenOfColorResult(Bitmap screen, int color, TestBounds testBounds)524     private AssertionResult getIsAppRegionOfScreenOfColorResult(Bitmap screen, int color,
525             TestBounds testBounds) {
526         for (int x = testBounds.rect.left; x < testBounds.rect.right; x++) {
527             for (int y = testBounds.rect.top;
528                     y < testBounds.rect.bottom; y++) {
529                 if (rectsContain(testBounds.excluded, x, y)) {
530                     continue;
531                 }
532 
533                 final Color rawColor = screen.getColor(x, y);
534                 final Color sRgbColor;
535                 if (!rawColor.getColorSpace().equals(ColorSpace.get(ColorSpace.Named.SRGB))) {
536                     // Conversion is required because the color space of the screenshot may be in
537                     // the DCI-P3 color space or some other color space and we want to compare the
538                     // color against once in the SRGB color space, so we must convert the color back
539                     // to the SRGB color space.
540                     sRgbColor = screen.getColor(x, y)
541                             .convert(ColorSpace.get(ColorSpace.Named.SRGB));
542                 } else {
543                     sRgbColor = rawColor;
544                 }
545                 final Color expectedColor = Color.valueOf(color);
546                 if (arrayEquals(new float[]{
547                                 expectedColor.red(), expectedColor.green(), expectedColor.blue()},
548                         new float[]{sRgbColor.red(), sRgbColor.green(), sRgbColor.blue()})) {
549                     return new ColorCheckResult(new Point(x, y), expectedColor, sRgbColor);
550                 }
551             }
552         }
553 
554         return AssertionResult.SUCCESS;
555     }
556 
arrayEquals(float[] array1, float[] array2)557     private boolean arrayEquals(float[] array1, float[] array2) {
558         return arrayEquals(array1, array2, COLOR_VALUE_VARIANCE_TOLERANCE);
559     }
560 
arrayEquals(float[] array1, float[] array2, float varianceTolerance)561     private boolean arrayEquals(float[] array1, float[] array2, float varianceTolerance) {
562         if (array1.length != array2.length) {
563             return true;
564         }
565         for (int i = 0; i < array1.length; i++) {
566             if (Math.abs(array1[i] - array2[i]) > varianceTolerance) {
567                 return true;
568             }
569         }
570         return false;
571     }
572 
getTopAppBounds()573     private Rect getTopAppBounds() {
574         getWmState().computeState();
575         final WindowManagerState.Activity activity = getWmState().getActivity(
576                 ComponentName.unflattenFromString(getWmState().getFocusedActivity()));
577         return activity.getAppBounds();
578     }
579 
580     private static class AssertionResult {
581         public final boolean isFailure;
582         public final String message;
583 
AssertionResult(boolean isFailure, String message)584         private AssertionResult(boolean isFailure, String message) {
585             this.isFailure = isFailure;
586             this.message = message;
587         }
588 
AssertionResult(boolean isFailure)589         private AssertionResult(boolean isFailure) {
590             this(isFailure, null);
591         }
592 
593         @Override
toString()594         public String toString() {
595             return "AssertionResult{"
596                     + "isFailure=" + isFailure
597                     + ", message='" + message + '\''
598                     + '}';
599         }
600 
601         private static final AssertionResult SUCCESS = new AssertionResult(false);
602         private static final AssertionResult FAILURE = new AssertionResult(true);
603     }
604 
createAssertColorChangeXIndex(int xIndex, TestBounds testBounds)605     private Function<Bitmap, AssertionResult> createAssertColorChangeXIndex(int xIndex,
606                                                                             TestBounds testBounds) {
607         return (screen) -> assertColorChangeXIndex(screen, xIndex, testBounds);
608     }
609 
assertColorChangeXIndex(Bitmap screen, int xIndex, TestBounds testBounds)610     private AssertionResult assertColorChangeXIndex(Bitmap screen, int xIndex,
611             TestBounds testBounds) {
612         // The activity we are extending is a half red, half blue.
613         // We are scaling the activity in the animation so if the extension doesn't work we should
614         // have a blue, then red, then black section, and if it does work we should see on a blue,
615         // followed by an extended red section.
616         for (int x = testBounds.rect.left; x < testBounds.rect.right; x++) {
617             for (int y = testBounds.rect.top;
618                     y < testBounds.rect.bottom; y++) {
619                 if (rectsContain(testBounds.excluded, x, y)) {
620                     continue;
621                 }
622 
623                 // Edge pixels can have any color depending on the blending strategy of the device.
624                 if (Math.abs(x - xIndex) <= 1) {
625                     continue;
626                 }
627 
628                 final Color expectedColor;
629                 if (x < xIndex) {
630                     expectedColor = Color.valueOf(Color.BLUE);
631                 } else {
632                     expectedColor = Color.valueOf(Color.RED);
633                 }
634 
635                 final Color rawColor = screen.getColor(x, y);
636                 final Color sRgbColor;
637                 if (!rawColor.getColorSpace().equals(ColorSpace.get(ColorSpace.Named.SRGB))) {
638                     // Conversion is required because the color space of the screenshot may be in
639                     // the DCI-P3 color space or some other color space and we want to compare the
640                     // color against once in the SRGB color space, so we must convert the color back
641                     // to the SRGB color space.
642                     sRgbColor = screen.getColor(x, y)
643                             .convert(ColorSpace.get(ColorSpace.Named.SRGB));
644                 } else {
645                     sRgbColor = rawColor;
646                 }
647 
648                 if (arrayEquals(new float[]{
649                                 expectedColor.red(), expectedColor.green(), expectedColor.blue()},
650                         new float[]{sRgbColor.red(), sRgbColor.green(), sRgbColor.blue()})) {
651                     return new ColorCheckResult(new Point(x, y), expectedColor, sRgbColor);
652                 }
653             }
654         }
655 
656         return AssertionResult.SUCCESS;
657     }
658 
setDefaultAnimationScale()659     private void setDefaultAnimationScale() {
660         mInitialWindowAnimationScale =
661                 runShellCommandSafe("settings get global window_animation_scale");
662         mInitialTransitionAnimationScale =
663                 runShellCommandSafe("settings get global transition_animation_scale");
664         mInitialAnimatorDurationScale =
665                 runShellCommandSafe("settings get global animator_duration_scale");
666 
667         if (!mInitialWindowAnimationScale.equals("1")
668                 || !mInitialTransitionAnimationScale.equals("1")
669                 || !mInitialAnimatorDurationScale.equals("1")) {
670             mAnimationScaleResetRequired = true;
671             runShellCommandSafe("settings put global window_animation_scale 1");
672             runShellCommandSafe("settings put global transition_animation_scale 1");
673             runShellCommandSafe("settings put global animator_duration_scale 1");
674         }
675     }
676 
restoreAnimationScale()677     private void restoreAnimationScale() {
678         if (mAnimationScaleResetRequired) {
679             runShellCommandSafe("settings put global window_animation_scale "
680                     + mInitialWindowAnimationScale);
681             runShellCommandSafe("settings put global transition_animation_scale "
682                     + mInitialTransitionAnimationScale);
683             runShellCommandSafe("settings put global animator_duration_scale "
684                     + mInitialAnimatorDurationScale);
685         }
686     }
687 
runShellCommandSafe(String cmd)688     private static String runShellCommandSafe(String cmd) {
689         try {
690             return runShellCommand(androidx.test.InstrumentationRegistry.getInstrumentation(), cmd);
691         } catch (IOException e) {
692             fail("Failed reading command output: " + e);
693             return "";
694         }
695     }
696 
697     public static class LauncherActivity extends Activity {
698 
699         private WindowInsets mInsets;
700 
701         @Override
onCreate(@ullable Bundle savedInstanceState)702         protected void onCreate(@Nullable Bundle savedInstanceState) {
703             super.onCreate(savedInstanceState);
704 
705             // Ensure the activity is edge-to-edge
706             // In tests we rely on the activity's content filling the entire window
707             getWindow().setDecorFitsSystemWindows(false);
708 
709             View view = new View(this);
710             view.setLayoutParams(new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
711             view.setOnApplyWindowInsetsListener((v, insets) -> mInsets = insets);
712             setContentView(view);
713         }
714 
getActivityFullyVisibleRegion()715         private Rect getActivityFullyVisibleRegion() {
716             final Rect activityBounds = getWindowManager().getCurrentWindowMetrics().getBounds();
717             final Insets insets = mInsets.getInsets(WindowInsets.Type.systemBars()
718                     | WindowInsets.Type.displayCutout());
719             activityBounds.inset(insets);
720 
721             return new Rect(activityBounds);
722         }
723 
getRoundedCornersRegions()724         private ArrayList<Rect> getRoundedCornersRegions() {
725             RoundedCorner topRightCorner = mInsets.getRoundedCorner(POSITION_TOP_RIGHT);
726             RoundedCorner topLeftCorner = mInsets.getRoundedCorner(POSITION_TOP_LEFT);
727             RoundedCorner bottomRightCorner = mInsets.getRoundedCorner(POSITION_BOTTOM_RIGHT);
728             RoundedCorner bottomLeftCorner = mInsets.getRoundedCorner(POSITION_BOTTOM_LEFT);
729 
730             final ArrayList<Rect> roundedCornersRects = new ArrayList<>();
731 
732             if (topRightCorner != null) {
733                 final Point center = topRightCorner.getCenter();
734                 final int radius = topRightCorner.getRadius();
735                 roundedCornersRects.add(
736                         new Rect(center.x, center.y - radius,
737                                 center.x + radius, center.y));
738             }
739             if (topLeftCorner != null) {
740                 final Point center = topLeftCorner.getCenter();
741                 final int radius = topLeftCorner.getRadius();
742                 roundedCornersRects.add(
743                         new Rect(center.x - radius, center.y - radius,
744                                 center.x, center.y));
745             }
746             if (bottomRightCorner != null) {
747                 final Point center = bottomRightCorner.getCenter();
748                 final int radius = bottomRightCorner.getRadius();
749                 roundedCornersRects.add(
750                         new Rect(center.x, center.y,
751                                 center.x + radius, center.y + radius));
752             }
753             if (bottomLeftCorner != null) {
754                 final Point center = bottomLeftCorner.getCenter();
755                 final int radius = bottomLeftCorner.getRadius();
756                 roundedCornersRects.add(
757                         new Rect(center.x - radius, center.y,
758                                 center.x, center.y + radius));
759             }
760 
761             return roundedCornersRects;
762         }
763 
startActivity(ActivityOptions activityOptions, Class<?> klass)764         public void startActivity(ActivityOptions activityOptions, Class<?> klass) {
765             startActivity(activityOptions, klass, new Bundle());
766         }
767 
startActivity(ActivityOptions activityOptions, Class<?> klass, Bundle extras)768         public void startActivity(ActivityOptions activityOptions, Class<?> klass,
769                 Bundle extras) {
770             final Intent i = new Intent(this, klass);
771             i.putExtras(extras);
772             startActivity(i, activityOptions != null ? activityOptions.toBundle() : null);
773         }
774     }
775 
776     public static class TransitionActivity extends Activity { }
777 
778     public static class OverridePendingTransitionActivity extends Activity {
779         static final String ENTER_ANIM_KEY = "enterAnim";
780         static final String EXIT_ANIM_KEY = "enterAnim";
781         static final String BACKGROUND_COLOR_KEY = "backgroundColor";
782 
783         @Override
onResume()784         protected void onResume() {
785             super.onResume();
786 
787             Bundle extras = getIntent().getExtras();
788             int enterAnim = extras.getInt(ENTER_ANIM_KEY);
789             int exitAnim = extras.getInt(EXIT_ANIM_KEY);
790             int backgroundColor = extras.getInt(BACKGROUND_COLOR_KEY);
791             overridePendingTransition(enterAnim, exitAnim, backgroundColor);
792         }
793     }
794 
795     public static class TransitionActivityWithWhiteBackground extends Activity { }
796 
797     public static class EdgeExtensionActivity extends Activity {
798         static final String DIRECTION_KEY = "direction";
799         static final int LEFT = 0;
800         static final int TOP = 1;
801         static final int RIGHT = 2;
802         static final int BOTTOM = 3;
803 
804         @Override
onCreate(@ullable Bundle savedInstanceState)805         protected void onCreate(@Nullable Bundle savedInstanceState) {
806             super.onCreate(savedInstanceState);
807             setContentView(R.layout.vertical_color_split);
808 
809             // Ensure the activity is edge-to-edge
810             // In tests we rely on the activity's content filling the entire window
811             getWindow().setDecorFitsSystemWindows(false);
812 
813             // Hide anything that the decor view might add to the window to avoid extending that
814             getWindow().getInsetsController()
815                     .hide(WindowInsets.Type.statusBars() | WindowInsets.Type.navigationBars());
816         }
817 
818         @Override
onResume()819         protected void onResume() {
820             super.onResume();
821 
822             Bundle extras = getIntent().getExtras();
823             int direction = extras.getInt(DIRECTION_KEY);
824             int enterAnim = 0;
825             switch (direction) {
826                 case LEFT:
827                     enterAnim = R.anim.edge_extension_left;
828                     break;
829                 case TOP:
830                     enterAnim = R.anim.edge_extension_top;
831                     break;
832                 case RIGHT:
833                     enterAnim = R.anim.edge_extension_right;
834                     break;
835                 case BOTTOM:
836                     enterAnim = R.anim.edge_extension_bottom;
837                     break;
838             }
839             overridePendingTransition(enterAnim, R.anim.alpha_0);
840         }
841     }
842 
843     public static class CustomWindowAnimationActivity extends Activity { }
844 }
845