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