diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java index 279d57a23e6e4..9335438cea508 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java @@ -119,6 +119,8 @@ public class Transitions implements RemoteCallable { /** List of possible handlers. Ordered by specificity (eg. tapped back to front). */ private final ArrayList mHandlers = new ArrayList<>(); + private final ArrayList mObservers = new ArrayList<>(); + /** List of {@link Runnable} instances to run when the last active transition has finished. */ private final ArrayList mRunWhenIdleQueue = new ArrayList<>(); @@ -242,6 +244,16 @@ public class Transitions implements RemoteCallable { mRemoteTransitionHandler.removeFiltered(remoteTransition); } + /** Registers an observer on the lifecycle of transitions. */ + public void registerObserver(@NonNull TransitionObserver observer) { + mObservers.add(observer); + } + + /** Unregisters the observer. */ + public void unregisterObserver(@NonNull TransitionObserver observer) { + mObservers.remove(observer); + } + /** Boosts the process priority of remote animation player. */ public static void setRunningRemoteTransitionDelegate(IApplicationThread appThread) { if (appThread == null) return; @@ -407,6 +419,11 @@ public class Transitions implements RemoteCallable { + Arrays.toString(mActiveTransitions.stream().map( activeTransition -> activeTransition.mToken).toArray())); } + + for (int i = 0; i < mObservers.size(); ++i) { + mObservers.get(i).onTransitionReady(transitionToken, info, t, finishT); + } + if (!info.getRootLeash().isValid()) { // Invalid root-leash implies that the transition is empty/no-op, so just do // housekeeping and return. @@ -474,6 +491,10 @@ public class Transitions implements RemoteCallable { } private void playTransition(@NonNull ActiveTransition active) { + for (int i = 0; i < mObservers.size(); ++i) { + mObservers.get(i).onTransitionStarting(active.mToken); + } + setupAnimHierarchy(active.mInfo, active.mStartT, active.mFinishT); // If a handler already chose to run this animation, try delegating to it first. @@ -546,6 +567,10 @@ public class Transitions implements RemoteCallable { active.mHandler.onTransitionConsumed( active.mToken, abort, abort ? null : active.mFinishT); } + for (int i = 0; i < mObservers.size(); ++i) { + mObservers.get(i).onTransitionMerged( + active.mToken, mActiveTransitions.get(0).mToken); + } return; } final ActiveTransition active = mActiveTransitions.get(activeIdx); @@ -555,6 +580,9 @@ public class Transitions implements RemoteCallable { active.mHandler.onTransitionConsumed( transition, true /* aborted */, null /* finishTransaction */); } + for (int i = 0; i < mObservers.size(); ++i) { + mObservers.get(i).onTransitionFinished(active.mToken, active.mAborted); + } ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition animation finished (abort=%b), notifying core %s", abort, transition); // Merge all relevant transactions together @@ -593,6 +621,9 @@ public class Transitions implements RemoteCallable { transition, true /* aborted */, null /* finishTransaction */); } mOrganizer.finishTransition(aborted.mToken, null /* wct */, null /* wctCB */); + for (int i = 0; i < mObservers.size(); ++i) { + mObservers.get(i).onTransitionFinished(active.mToken, true); + } } if (mActiveTransitions.size() <= activeIdx) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "All active transition animations " @@ -792,6 +823,52 @@ public class Transitions implements RemoteCallable { default void setAnimScaleSetting(float scale) {} } + /** + * Interface for something that needs to know the lifecycle of some transitions, but never + * handles any transition by itself. + */ + public interface TransitionObserver { + /** + * Called when the transition is ready to play. It may later be merged into other + * transitions. Note this doesn't mean this transition will be played anytime soon. + * + * @param transition the unique token of this transition + * @param startTransaction the transaction given to the handler to be applied before the + * transition animation. This will be applied when the transition + * handler that handles this transition starts the transition. + * @param finishTransaction the transaction given to the handler to be applied after the + * transition animation. The Transition system will apply it when + * finishCallback is called by the transition handler. + */ + void onTransitionReady(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction); + + /** + * Called when the transition is starting to play. It isn't called for merged transitions. + * + * @param transition the unique token of this transition + */ + void onTransitionStarting(@NonNull IBinder transition); + + /** + * Called when a transition is merged into another transition. There won't be any following + * lifecycle calls for the merged transition. + * + * @param merged the unique token of the transition that's merged to another one + * @param playing the unique token of the transition that accepts the merge + */ + void onTransitionMerged(@NonNull IBinder merged, @NonNull IBinder playing); + + /** + * Called when the transition is finished. This isn't called for merged transitions. + * + * @param transition the unique token of this transition + * @param aborted {@code true} if this transition is aborted; {@code false} otherwise. + */ + void onTransitionFinished(@NonNull IBinder transition, boolean aborted); + } + @BinderThread private class TransitionPlayerImpl extends ITransitionPlayer.Stub { @Override diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java index 388792b63db37..7c5e748df696c 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java @@ -43,11 +43,13 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -89,6 +91,7 @@ import com.android.wm.shell.sysui.ShellInit; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.InOrder; import java.util.ArrayList; @@ -688,6 +691,204 @@ public class ShellTransitionTests extends ShellTestCase { verify(runnable4, times(1)).run(); } + @Test + public void testObserverLifecycle_basicTransitionFlow() { + Transitions transitions = createTestTransitions(); + Transitions.TransitionObserver observer = mock(Transitions.TransitionObserver.class); + transitions.registerObserver(observer); + transitions.replaceDefaultHandlerForTest(mDefaultHandler); + + IBinder transitToken = new Binder(); + transitions.requestStartTransition(transitToken, + new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */)); + TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); + SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class); + SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class); + transitions.onTransitionReady(transitToken, info, startT, finishT); + + InOrder observerOrder = inOrder(observer); + observerOrder.verify(observer).onTransitionReady(transitToken, info, startT, finishT); + observerOrder.verify(observer).onTransitionStarting(transitToken); + verify(observer, times(0)).onTransitionFinished(eq(transitToken), anyBoolean()); + mDefaultHandler.finishAll(); + mMainExecutor.flushAll(); + verify(observer).onTransitionFinished(transitToken, false); + } + + @Test + public void testObserverLifecycle_queueing() { + Transitions transitions = createTestTransitions(); + Transitions.TransitionObserver observer = mock(Transitions.TransitionObserver.class); + transitions.registerObserver(observer); + transitions.replaceDefaultHandlerForTest(mDefaultHandler); + + IBinder transitToken1 = new Binder(); + transitions.requestStartTransition(transitToken1, + new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */)); + TransitionInfo info1 = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); + SurfaceControl.Transaction startT1 = mock(SurfaceControl.Transaction.class); + SurfaceControl.Transaction finishT1 = mock(SurfaceControl.Transaction.class); + transitions.onTransitionReady(transitToken1, info1, startT1, finishT1); + verify(observer).onTransitionReady(transitToken1, info1, startT1, finishT1); + + IBinder transitToken2 = new Binder(); + transitions.requestStartTransition(transitToken2, + new TransitionRequestInfo(TRANSIT_CLOSE, null /* trigger */, null /* remote */)); + TransitionInfo info2 = new TransitionInfoBuilder(TRANSIT_CLOSE) + .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); + SurfaceControl.Transaction startT2 = mock(SurfaceControl.Transaction.class); + SurfaceControl.Transaction finishT2 = mock(SurfaceControl.Transaction.class); + transitions.onTransitionReady(transitToken2, info2, startT2, finishT2); + verify(observer, times(1)).onTransitionReady(transitToken2, info2, startT2, finishT2); + verify(observer, times(0)).onTransitionStarting(transitToken2); + verify(observer, times(0)).onTransitionFinished(eq(transitToken1), anyBoolean()); + verify(observer, times(0)).onTransitionFinished(eq(transitToken2), anyBoolean()); + + mDefaultHandler.finishAll(); + mMainExecutor.flushAll(); + // first transition finished + verify(observer, times(1)).onTransitionFinished(transitToken1, false); + verify(observer, times(1)).onTransitionStarting(transitToken2); + verify(observer, times(0)).onTransitionFinished(eq(transitToken2), anyBoolean()); + + mDefaultHandler.finishAll(); + mMainExecutor.flushAll(); + verify(observer, times(1)).onTransitionFinished(transitToken2, false); + } + + + @Test + public void testObserverLifecycle_merging() { + Transitions transitions = createTestTransitions(); + Transitions.TransitionObserver observer = mock(Transitions.TransitionObserver.class); + transitions.registerObserver(observer); + mDefaultHandler.setSimulateMerge(true); + transitions.replaceDefaultHandlerForTest(mDefaultHandler); + + IBinder transitToken1 = new Binder(); + transitions.requestStartTransition(transitToken1, + new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */)); + TransitionInfo info1 = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); + SurfaceControl.Transaction startT1 = mock(SurfaceControl.Transaction.class); + SurfaceControl.Transaction finishT1 = mock(SurfaceControl.Transaction.class); + transitions.onTransitionReady(transitToken1, info1, startT1, finishT1); + + IBinder transitToken2 = new Binder(); + transitions.requestStartTransition(transitToken2, + new TransitionRequestInfo(TRANSIT_CLOSE, null /* trigger */, null /* remote */)); + TransitionInfo info2 = new TransitionInfoBuilder(TRANSIT_CLOSE) + .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); + SurfaceControl.Transaction startT2 = mock(SurfaceControl.Transaction.class); + SurfaceControl.Transaction finishT2 = mock(SurfaceControl.Transaction.class); + transitions.onTransitionReady(transitToken2, info2, startT2, finishT2); + + InOrder observerOrder = inOrder(observer); + observerOrder.verify(observer).onTransitionReady(transitToken2, info2, startT2, finishT2); + observerOrder.verify(observer).onTransitionMerged(transitToken2, transitToken1); + verify(observer, times(0)).onTransitionFinished(eq(transitToken1), anyBoolean()); + + mDefaultHandler.finishAll(); + mMainExecutor.flushAll(); + // transition + merged all finished. + verify(observer, times(1)).onTransitionFinished(transitToken1, false); + // Merged transition won't receive any lifecycle calls beyond ready + verify(observer, times(0)).onTransitionStarting(transitToken2); + verify(observer, times(0)).onTransitionFinished(eq(transitToken2), anyBoolean()); + } + + @Test + public void testObserverLifecycle_mergingAfterQueueing() { + Transitions transitions = createTestTransitions(); + Transitions.TransitionObserver observer = mock(Transitions.TransitionObserver.class); + transitions.registerObserver(observer); + mDefaultHandler.setSimulateMerge(true); + transitions.replaceDefaultHandlerForTest(mDefaultHandler); + + // Make a test handler that only responds to multi-window triggers AND only animates + // Change transitions. + final WindowContainerTransaction handlerWCT = new WindowContainerTransaction(); + TestTransitionHandler testHandler = new TestTransitionHandler() { + @Override + public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + for (TransitionInfo.Change chg : info.getChanges()) { + if (chg.getMode() == TRANSIT_CHANGE) { + return super.startAnimation(transition, info, startTransaction, + finishTransaction, finishCallback); + } + } + return false; + } + + @Nullable + @Override + public WindowContainerTransaction handleRequest(@NonNull IBinder transition, + @NonNull TransitionRequestInfo request) { + final RunningTaskInfo task = request.getTriggerTask(); + return (task != null && task.getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW) + ? handlerWCT : null; + } + }; + transitions.addHandler(testHandler); + + // Use test handler to play an animation + IBinder transitToken1 = new Binder(); + RunningTaskInfo mwTaskInfo = + createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD); + transitions.requestStartTransition(transitToken1, + new TransitionRequestInfo(TRANSIT_CHANGE, mwTaskInfo, null /* remote */)); + TransitionInfo change = new TransitionInfoBuilder(TRANSIT_CHANGE) + .addChange(TRANSIT_CHANGE).build(); + SurfaceControl.Transaction startT1 = mock(SurfaceControl.Transaction.class); + SurfaceControl.Transaction finishT1 = mock(SurfaceControl.Transaction.class); + transitions.onTransitionReady(transitToken1, change, startT1, finishT1); + + // Request the second transition that should be handled by the default handler + IBinder transitToken2 = new Binder(); + TransitionInfo open = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); + transitions.requestStartTransition(transitToken2, + new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */)); + SurfaceControl.Transaction startT2 = mock(SurfaceControl.Transaction.class); + SurfaceControl.Transaction finishT2 = mock(SurfaceControl.Transaction.class); + transitions.onTransitionReady(transitToken2, open, startT2, finishT2); + verify(observer).onTransitionReady(transitToken2, open, startT2, finishT2); + verify(observer, times(0)).onTransitionStarting(transitToken2); + + // Request the third transition that should be merged into the second one + IBinder transitToken3 = new Binder(); + transitions.requestStartTransition(transitToken3, + new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */)); + SurfaceControl.Transaction startT3 = mock(SurfaceControl.Transaction.class); + SurfaceControl.Transaction finishT3 = mock(SurfaceControl.Transaction.class); + transitions.onTransitionReady(transitToken3, open, startT3, finishT3); + verify(observer, times(0)).onTransitionStarting(transitToken2); + verify(observer).onTransitionReady(transitToken3, open, startT3, finishT3); + verify(observer, times(0)).onTransitionStarting(transitToken3); + + testHandler.finishAll(); + mMainExecutor.flushAll(); + + verify(observer).onTransitionFinished(transitToken1, false); + + mDefaultHandler.finishAll(); + mMainExecutor.flushAll(); + + InOrder observerOrder = inOrder(observer); + observerOrder.verify(observer).onTransitionStarting(transitToken2); + observerOrder.verify(observer).onTransitionMerged(transitToken3, transitToken2); + observerOrder.verify(observer).onTransitionFinished(transitToken2, false); + + // Merged transition won't receive any lifecycle calls beyond ready + verify(observer, times(0)).onTransitionStarting(transitToken3); + verify(observer, times(0)).onTransitionFinished(eq(transitToken3), anyBoolean()); + } + class TransitionInfoBuilder { final TransitionInfo mInfo;