diff options
6 files changed, 275 insertions, 3 deletions
diff --git a/services/core/java/com/android/server/wm/AnimatingAppWindowTokenRegistry.java b/services/core/java/com/android/server/wm/AnimatingAppWindowTokenRegistry.java new file mode 100644 index 000000000000..ae343da30c74 --- /dev/null +++ b/services/core/java/com/android/server/wm/AnimatingAppWindowTokenRegistry.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.server.wm; + +import android.util.ArrayMap; +import android.util.ArraySet; + +import java.util.ArrayList; + +/** + * Keeps track of all {@link AppWindowToken} that are animating and makes sure all animations are + * finished at the same time such that we don't run into issues with z-ordering: An activity A + * that has a shorter animation that is above another activity B with a longer animation in the same + * task, the animation layer would put the B on top of A, but from the hierarchy, A needs to be on + * top of B. Thus, we defer reparenting A to the original hierarchy such that it stays on top of B + * until B finishes animating. + */ +class AnimatingAppWindowTokenRegistry { + + private ArraySet<AppWindowToken> mAnimatingTokens = new ArraySet<>(); + private ArrayMap<AppWindowToken, Runnable> mFinishedTokens = new ArrayMap<>(); + + private ArrayList<Runnable> mTmpRunnableList = new ArrayList<>(); + + /** + * Notifies that an {@link AppWindowToken} has started animating. + */ + void notifyStarting(AppWindowToken token) { + mAnimatingTokens.add(token); + } + + /** + * Notifies that an {@link AppWindowToken} has finished animating. + */ + void notifyFinished(AppWindowToken token) { + mAnimatingTokens.remove(token); + mFinishedTokens.remove(token); + } + + /** + * Called when an {@link AppWindowToken} is about to finish animating. + * + * @param endDeferFinishCallback Callback to run when defer finish should be ended. + * @return {@code true} if finishing the animation should be deferred, {@code false} otherwise. + */ + boolean notifyAboutToFinish(AppWindowToken token, Runnable endDeferFinishCallback) { + final boolean removed = mAnimatingTokens.remove(token); + if (!removed) { + return false; + } + + if (mAnimatingTokens.isEmpty()) { + + // If no animations are animating anymore, finish all others. + endDeferringFinished(); + return false; + } else { + + // Otherwise let's put it into the pending list of to be finished animations. + mFinishedTokens.put(token, endDeferFinishCallback); + return true; + } + } + + private void endDeferringFinished() { + // Copy it into a separate temp list to avoid modifying the collection while iterating as + // calling the callback may call back into notifyFinished. + for (int i = mFinishedTokens.size() - 1; i >= 0; i--) { + mTmpRunnableList.add(mFinishedTokens.valueAt(i)); + } + mFinishedTokens.clear(); + for (int i = mTmpRunnableList.size() - 1; i >= 0; i--) { + mTmpRunnableList.get(i).run(); + } + mTmpRunnableList.clear(); + } +} diff --git a/services/core/java/com/android/server/wm/AppWindowToken.java b/services/core/java/com/android/server/wm/AppWindowToken.java index 1f71b8fa74ec..8540feb7ece3 100644 --- a/services/core/java/com/android/server/wm/AppWindowToken.java +++ b/services/core/java/com/android/server/wm/AppWindowToken.java @@ -252,6 +252,7 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree private final Point mTmpPoint = new Point(); private final Rect mTmpRect = new Rect(); private RemoteAnimationDefinition mRemoteAnimationDefinition; + private AnimatingAppWindowTokenRegistry mAnimatingAppWindowTokenRegistry; AppWindowToken(WindowManagerService service, IApplicationToken token, boolean voiceInteraction, DisplayContent dc, long inputDispatchingTimeoutNanos, boolean fullscreen, @@ -781,6 +782,16 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree task.mStack.mExitingAppTokens.remove(this); } } + final TaskStack stack = getStack(); + + // If we reparent, make sure to remove ourselves from the old animation registry. + if (mAnimatingAppWindowTokenRegistry != null) { + mAnimatingAppWindowTokenRegistry.notifyFinished(this); + } + mAnimatingAppWindowTokenRegistry = stack != null + ? stack.getAnimatingAppWindowTokenRegistry() + : null; + mLastParent = task; } @@ -1784,6 +1795,21 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree } @Override + public boolean shouldDeferAnimationFinish(Runnable endDeferFinishCallback) { + return mAnimatingAppWindowTokenRegistry != null + && mAnimatingAppWindowTokenRegistry.notifyAboutToFinish( + this, endDeferFinishCallback); + } + + @Override + public void onAnimationLeashDestroyed(Transaction t) { + super.onAnimationLeashDestroyed(t); + if (mAnimatingAppWindowTokenRegistry != null) { + mAnimatingAppWindowTokenRegistry.notifyFinished(this); + } + } + + @Override protected void setLayer(Transaction t, int layer) { if (!mSurfaceAnimator.hasLeash()) { t.setLayer(mSurfaceControl, layer); @@ -1825,6 +1851,9 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree final DisplayContent dc = getDisplayContent(); dc.assignStackOrdering(t); + if (mAnimatingAppWindowTokenRegistry != null) { + mAnimatingAppWindowTokenRegistry.notifyStarting(this); + } } /** diff --git a/services/core/java/com/android/server/wm/SurfaceAnimator.java b/services/core/java/com/android/server/wm/SurfaceAnimator.java index c06caaf9e900..f10ff8c1dd81 100644 --- a/services/core/java/com/android/server/wm/SurfaceAnimator.java +++ b/services/core/java/com/android/server/wm/SurfaceAnimator.java @@ -81,9 +81,14 @@ class SurfaceAnimator { if (anim != mAnimation) { return; } - reset(mAnimatable.getPendingTransaction(), true /* destroyLeash */); - if (animationFinishedCallback != null) { - animationFinishedCallback.run(); + final Runnable resetAndInvokeFinish = () -> { + reset(mAnimatable.getPendingTransaction(), true /* destroyLeash */); + if (animationFinishedCallback != null) { + animationFinishedCallback.run(); + } + }; + if (!mAnimatable.shouldDeferAnimationFinish(resetAndInvokeFinish)) { + resetAndInvokeFinish.run(); } } }; @@ -407,5 +412,17 @@ class SurfaceAnimator { * @return The height of the surface to be animated. */ int getSurfaceHeight(); + + /** + * Gets called when the animation is about to finish and gives the client the opportunity to + * defer finishing the animation, i.e. it keeps the leash around until the client calls + * {@link #cancelAnimation}. + * + * @param endDeferFinishCallback The callback to call when defer finishing should be ended. + * @return Whether the client would like to defer the animation finish. + */ + default boolean shouldDeferAnimationFinish(Runnable endDeferFinishCallback) { + return false; + } } } diff --git a/services/core/java/com/android/server/wm/TaskStack.java b/services/core/java/com/android/server/wm/TaskStack.java index b5d00a75d7e6..460edece0f61 100644 --- a/services/core/java/com/android/server/wm/TaskStack.java +++ b/services/core/java/com/android/server/wm/TaskStack.java @@ -155,6 +155,9 @@ public class TaskStack extends WindowContainer<Task> implements final Rect mTmpDimBoundsRect = new Rect(); private final Point mLastSurfaceSize = new Point(); + private final AnimatingAppWindowTokenRegistry mAnimatingAppWindowTokenRegistry = + new AnimatingAppWindowTokenRegistry(); + TaskStack(WindowManagerService service, int stackId, StackWindowController controller) { super(service); mStackId = stackId; @@ -1782,4 +1785,8 @@ public class TaskStack extends WindowContainer<Task> implements outPos.x -= outset; outPos.y -= outset; } + + AnimatingAppWindowTokenRegistry getAnimatingAppWindowTokenRegistry() { + return mAnimatingAppWindowTokenRegistry; + } } diff --git a/services/tests/servicestests/src/com/android/server/wm/AnimatingAppWindowTokenRegistryTest.java b/services/tests/servicestests/src/com/android/server/wm/AnimatingAppWindowTokenRegistryTest.java new file mode 100644 index 000000000000..8b78f10b0b5b --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/wm/AnimatingAppWindowTokenRegistryTest.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.server.wm; + +import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertTrue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +import android.platform.test.annotations.Presubmit; +import android.support.test.filters.FlakyTest; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import com.android.server.wm.SurfaceAnimator.OnAnimationFinishedCallback; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Tests for the {@link TaskStack} class. + * + * Build/Install/Run: + * atest FrameworksServicesTests:com.android.server.wm.AnimatingAppWindowTokenRegistryTest + */ +@SmallTest +@Presubmit +@FlakyTest(detail = "Promote once confirmed non-flaky") +@RunWith(AndroidJUnit4.class) +public class AnimatingAppWindowTokenRegistryTest extends WindowTestsBase { + + @Mock + AnimationAdapter mAdapter; + + @Mock + Runnable mMockEndDeferFinishCallback1; + @Mock + Runnable mMockEndDeferFinishCallback2; + @Before + public void setUp() throws Exception { + super.setUp(); + MockitoAnnotations.initMocks(this); + } + + @Test + public void testDeferring() throws Exception { + final AppWindowToken window1 = createAppWindowToken(mDisplayContent, + WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD); + final AppWindowToken window2 = createAppWindow(window1.getTask(), ACTIVITY_TYPE_STANDARD, + "window2").mAppToken; + final AnimatingAppWindowTokenRegistry registry = + window1.getStack().getAnimatingAppWindowTokenRegistry(); + + window1.startAnimation(window1.getPendingTransaction(), mAdapter, false /* hidden */); + window2.startAnimation(window1.getPendingTransaction(), mAdapter, false /* hidden */); + assertTrue(window1.isSelfAnimating()); + assertTrue(window2.isSelfAnimating()); + + // Make sure that first animation finish is deferred, second one is not deferred, and first + // one gets cancelled. + assertTrue(registry.notifyAboutToFinish(window1, mMockEndDeferFinishCallback1)); + assertFalse(registry.notifyAboutToFinish(window2, mMockEndDeferFinishCallback2)); + verify(mMockEndDeferFinishCallback1).run(); + verifyZeroInteractions(mMockEndDeferFinishCallback2); + } +} diff --git a/services/tests/servicestests/src/com/android/server/wm/SurfaceAnimatorTest.java b/services/tests/servicestests/src/com/android/server/wm/SurfaceAnimatorTest.java index a120eba623a3..650687245858 100644 --- a/services/tests/servicestests/src/com/android/server/wm/SurfaceAnimatorTest.java +++ b/services/tests/servicestests/src/com/android/server/wm/SurfaceAnimatorTest.java @@ -28,6 +28,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; import android.platform.test.annotations.Presubmit; +import android.support.test.filters.FlakyTest; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; import android.view.SurfaceControl; @@ -64,6 +65,7 @@ public class SurfaceAnimatorTest extends WindowTestsBase { private SurfaceSession mSession = new SurfaceSession(); private MyAnimatable mAnimatable; private MyAnimatable mAnimatable2; + private DeferFinishAnimatable mDeferFinishAnimatable; @Before public void setUp() throws Exception { @@ -71,6 +73,7 @@ public class SurfaceAnimatorTest extends WindowTestsBase { MockitoAnnotations.initMocks(this); mAnimatable = new MyAnimatable(); mAnimatable2 = new MyAnimatable(); + mDeferFinishAnimatable = new DeferFinishAnimatable(); } @Test @@ -166,6 +169,29 @@ public class SurfaceAnimatorTest extends WindowTestsBase { verify(mTransaction).destroy(eq(leash)); } + @Test + @FlakyTest(detail = "Promote once confirmed non-flaky") + public void testDeferFinish() throws Exception { + + // Start animation + mDeferFinishAnimatable.mSurfaceAnimator.startAnimation(mTransaction, mSpec, + true /* hidden */); + final ArgumentCaptor<OnAnimationFinishedCallback> callbackCaptor = ArgumentCaptor.forClass( + OnAnimationFinishedCallback.class); + assertAnimating(mDeferFinishAnimatable); + verify(mSpec).startAnimation(any(), any(), callbackCaptor.capture()); + + // Finish the animation but then make sure we are deferring. + callbackCaptor.getValue().onAnimationFinished(mSpec); + assertAnimating(mDeferFinishAnimatable); + + // Now end defer finishing. + mDeferFinishAnimatable.endDeferFinishCallback.run(); + assertNotAnimating(mAnimatable2); + assertTrue(mDeferFinishAnimatable.mFinishedCallbackCalled); + verify(mTransaction).destroy(eq(mDeferFinishAnimatable.mLeash)); + } + private void assertAnimating(MyAnimatable animatable) { assertTrue(animatable.mSurfaceAnimator.isAnimating()); assertNotNull(animatable.mSurfaceAnimator.getAnimation()); @@ -254,4 +280,15 @@ public class SurfaceAnimatorTest extends WindowTestsBase { private final Runnable mFinishedCallback = () -> mFinishedCallbackCalled = true; } + + private class DeferFinishAnimatable extends MyAnimatable { + + Runnable endDeferFinishCallback; + + @Override + public boolean shouldDeferAnimationFinish(Runnable endDeferFinishCallback) { + this.endDeferFinishCallback = endDeferFinishCallback; + return true; + } + } } |