diff --git a/services/core/java/com/android/server/wm/AppWindowThumbnail.java b/services/core/java/com/android/server/wm/AppWindowThumbnail.java index 0e14e46e77de6..5519729c17f5a 100644 --- a/services/core/java/com/android/server/wm/AppWindowThumbnail.java +++ b/services/core/java/com/android/server/wm/AppWindowThumbnail.java @@ -143,7 +143,7 @@ class AppWindowThumbnail implements Animatable { void destroy() { mSurfaceAnimator.cancelAnimation(); - mSurfaceControl.remove(); + getPendingTransaction().remove(mSurfaceControl); } /** diff --git a/services/core/java/com/android/server/wm/AppWindowToken.java b/services/core/java/com/android/server/wm/AppWindowToken.java index 2cc85e2076e01..bcf6abaac5dae 100644 --- a/services/core/java/com/android/server/wm/AppWindowToken.java +++ b/services/core/java/com/android/server/wm/AppWindowToken.java @@ -1275,7 +1275,19 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree void onDisplayChanged(DisplayContent dc) { DisplayContent prevDc = mDisplayContent; super.onDisplayChanged(dc); - if (prevDc != null && prevDc.mFocusedApp == this) { + if (prevDc == null) { + return; + } + if (prevDc.mChangingApps.contains(this)) { + // This gets called *after* the AppWindowToken has been reparented to the new display. + // That reparenting resulted in this window changing modes (eg. FREEFORM -> FULLSCREEN), + // so this token is now "frozen" while waiting for the animation to start on prevDc + // (which will be cancelled since the window is no-longer a child). However, since this + // is no longer a child of prevDc, this won't be notified of the cancelled animation, + // so we need to cancel the change transition here. + clearChangeLeash(getPendingTransaction(), true /* cancel */); + } + if (prevDc.mFocusedApp == this) { prevDc.setFocusedApp(null); final TaskStack stack = dc.getTopStack(); if (stack != null) { @@ -1584,7 +1596,10 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree } private boolean shouldStartChangeTransition(int prevWinMode, int newWinMode) { - if (!isVisible() || getDisplayContent().mAppTransition.isTransitionSet()) { + if (mWmService.mDisableTransitionAnimation + || !isVisible() + || getDisplayContent().mAppTransition.isTransitionSet() + || getSurfaceControl() == null) { return false; } // Only do an animation into and out-of freeform mode for now. Other mode @@ -2561,9 +2576,7 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree return; } else if (mTransitChangeLeash != null) { // unparent mTransitChangeLeash for clean-up - t.hide(mTransitChangeLeash); - t.reparent(mTransitChangeLeash, null); - mTransitChangeLeash = null; + clearChangeLeash(t, false /* cancel */); } if (mAnimatingAppWindowTokenRegistry != null) { @@ -2659,15 +2672,36 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree return super.isSelfAnimating(); } + /** + * @param cancel {@code true} if clearing the leash due to cancelling instead of transferring + * to another leash. + */ + private void clearChangeLeash(Transaction t, boolean cancel) { + if (mTransitChangeLeash == null) { + return; + } + if (cancel) { + clearThumbnail(); + SurfaceControl sc = getSurfaceControl(); + SurfaceControl parentSc = getParentSurfaceControl(); + // Don't reparent if surface is getting destroyed + if (parentSc != null && sc != null) { + t.reparent(sc, getParentSurfaceControl()); + } + } + t.hide(mTransitChangeLeash); + t.reparent(mTransitChangeLeash, null); + mTransitChangeLeash = null; + if (cancel) { + onAnimationLeashDestroyed(t); + } + } + @Override void cancelAnimation() { cancelAnimationOnly(); clearThumbnail(); - if (mTransitChangeLeash != null) { - getPendingTransaction().hide(mTransitChangeLeash); - getPendingTransaction().reparent(mTransitChangeLeash, null); - mTransitChangeLeash = null; - } + clearChangeLeash(getPendingTransaction(), true /* cancel */); } /** @@ -3003,7 +3037,9 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree void removeFromPendingTransition() { if (isWaitingForTransitionStart() && mDisplayContent != null) { mDisplayContent.mOpeningApps.remove(this); - mDisplayContent.mChangingApps.remove(this); + if (mDisplayContent.mChangingApps.remove(this)) { + clearChangeLeash(getPendingTransaction(), true /* cancel */); + } mDisplayContent.mClosingApps.remove(this); } } diff --git a/services/tests/wmtests/src/com/android/server/wm/AppChangeTransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/AppChangeTransitionTests.java index defe9811ebcfa..a7520dcbcff9f 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppChangeTransitionTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppChangeTransitionTests.java @@ -16,24 +16,29 @@ package com.android.server.wm; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.view.WindowManager.TRANSIT_TASK_CHANGE_WINDOWING_MODE; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import android.graphics.Rect; import android.os.IBinder; +import android.view.Display; import android.view.IRemoteAnimationFinishedCallback; import android.view.IRemoteAnimationRunner; import android.view.RemoteAnimationAdapter; import android.view.RemoteAnimationDefinition; import android.view.RemoteAnimationTarget; -import androidx.test.filters.FlakyTest; import androidx.test.filters.SmallTest; -import org.junit.Before; import org.junit.Test; /** @@ -49,14 +54,20 @@ public class AppChangeTransitionTests extends WindowTestsBase { private Task mTask; private WindowTestUtils.TestAppWindowToken mToken; - @Before - public void setUp() throws Exception { - mStack = createTaskStackOnDisplay(mDisplayContent); + public void setUpOnDisplay(DisplayContent dc) { + mStack = createTaskStackOnDisplay(WINDOWING_MODE_UNDEFINED, ACTIVITY_TYPE_STANDARD, dc); mTask = createTaskInStack(mStack, 0 /* userId */); - mToken = WindowTestUtils.createTestAppWindowToken(mDisplayContent); + mToken = WindowTestUtils.createTestAppWindowToken(dc); mToken.mSkipOnParentChanged = false; mTask.addChild(mToken, 0); + + // Set a remote animator with snapshot disabled. Snapshots don't work in wmtests. + RemoteAnimationDefinition definition = new RemoteAnimationDefinition(); + RemoteAnimationAdapter adapter = + new RemoteAnimationAdapter(new TestRemoteAnimationRunner(), 10, 1, false); + definition.addRemoteAnimation(TRANSIT_TASK_CHANGE_WINDOWING_MODE, adapter); + dc.registerRemoteAnimations(definition); } class TestRemoteAnimationRunner implements IRemoteAnimationRunner { @@ -85,14 +96,58 @@ public class AppChangeTransitionTests extends WindowTestsBase { @Test public void testModeChangeRemoteAnimatorNoSnapshot() { - RemoteAnimationDefinition definition = new RemoteAnimationDefinition(); - RemoteAnimationAdapter adapter = - new RemoteAnimationAdapter(new TestRemoteAnimationRunner(), 10, 1, false); - definition.addRemoteAnimation(TRANSIT_TASK_CHANGE_WINDOWING_MODE, adapter); - mDisplayContent.registerRemoteAnimations(definition); + // setup currently defaults to no snapshot. + setUpOnDisplay(mDisplayContent); mTask.setWindowingMode(WINDOWING_MODE_FREEFORM); assertEquals(1, mDisplayContent.mChangingApps.size()); + + // Verify we are in a change transition, but without a snapshot. + // Though, the test will actually have crashed by now if a snapshot is attempted. + assertNull(mToken.getThumbnail()); + assertTrue(mToken.isInChangeTransition()); + + waitUntilHandlersIdle(); + mToken.removeImmediately(); + } + + @Test + public void testCancelPendingChangeOnRemove() { + // setup currently defaults to no snapshot. + setUpOnDisplay(mDisplayContent); + + mTask.setWindowingMode(WINDOWING_MODE_FREEFORM); + assertEquals(1, mDisplayContent.mChangingApps.size()); + assertTrue(mToken.isInChangeTransition()); + + // Removing the app-token from the display should clean-up the + // the change leash. + mDisplayContent.removeAppToken(mToken.token); + assertEquals(0, mDisplayContent.mChangingApps.size()); + assertFalse(mToken.isInChangeTransition()); + + waitUntilHandlersIdle(); + mToken.removeImmediately(); + } + + @Test + public void testNoChangeWhenMoveDisplay() { + mDisplayContent.setWindowingMode(WINDOWING_MODE_FULLSCREEN); + final DisplayContent dc1 = createNewDisplay(Display.STATE_ON); + dc1.setWindowingMode(WINDOWING_MODE_FREEFORM); + setUpOnDisplay(dc1); + + assertEquals(WINDOWING_MODE_FREEFORM, mTask.getWindowingMode()); + + // Reparenting to a display with different windowing mode may trigger + // a change transition internally, but it should be cleaned-up once + // the display change is complete. + mStack.reparent(mDisplayContent.getDisplayId(), new Rect(), true); + + assertEquals(WINDOWING_MODE_FULLSCREEN, mTask.getWindowingMode()); + + // Make sure we're not waiting for a change animation (no leash) + assertFalse(mToken.isInChangeTransition()); assertNull(mToken.getThumbnail()); waitUntilHandlersIdle();