From 1229c01a4e1a3b68f3e7c83f549793e7e6b625d2 Mon Sep 17 00:00:00 2001 From: Evan Rosky Date: Fri, 8 Jan 2021 11:52:42 -0800 Subject: [PATCH] Refactor default transit animations into handler for testing In order to make shell transitions testable, this makes the default animations into a TransitionHandler. This is also cleaner, more consistent, and will make expanding the default animations easier later. The reason this is needed is because animations themselves aren't very amenable to testing, so to test the interface logic, we need to be able to replace the default handler with one that simulates just the contract of the default handler (don't claim any requests, but always perform animation). Also added some tests for both basic transition flow and for the TransitionHandler contracts Bug: 169035082 Test: atest ShellTransitionTests Change-Id: I92e367af155831c11f583322e95151a2cb645caa --- .../transition/DefaultTransitionHandler.java | 149 +++++++++++ .../wm/shell/transition/Transitions.java | 99 ++----- .../android/wm/shell/TestShellExecutor.java | 51 ++++ .../transition/ShellTransitionTests.java | 243 ++++++++++++++++++ 4 files changed, 461 insertions(+), 81 deletions(-) create mode 100644 libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java create mode 100644 libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestShellExecutor.java create mode 100644 libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java new file mode 100644 index 0000000000000..4cd2c504c83e5 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2021 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.wm.shell.transition; + +import static android.view.WindowManager.TRANSIT_CLOSE; +import static android.view.WindowManager.TRANSIT_OPEN; +import static android.view.WindowManager.TRANSIT_TO_BACK; +import static android.view.WindowManager.TRANSIT_TO_FRONT; +import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.ActivityManager; +import android.os.IBinder; +import android.util.ArrayMap; +import android.view.SurfaceControl; +import android.window.TransitionInfo; +import android.window.WindowContainerTransaction; + +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.TransactionPool; + +import java.util.ArrayList; + +/** The default handler that handles anything not already handled. */ +public class DefaultTransitionHandler implements Transitions.TransitionHandler { + private final TransactionPool mTransactionPool; + private final ShellExecutor mMainExecutor; + private final ShellExecutor mAnimExecutor; + + /** Keeps track of the currently-running animations associated with each transition. */ + private final ArrayMap> mAnimations = new ArrayMap<>(); + + DefaultTransitionHandler(@NonNull TransactionPool transactionPool, + @NonNull ShellExecutor mainExecutor, @NonNull ShellExecutor animExecutor) { + mTransactionPool = transactionPool; + mMainExecutor = mainExecutor; + mAnimExecutor = animExecutor; + } + + @Override + public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction t, @NonNull Runnable finishCallback) { + if (mAnimations.containsKey(transition)) { + throw new IllegalStateException("Got a duplicate startAnimation call for " + + transition); + } + final ArrayList animations = new ArrayList<>(); + mAnimations.put(transition, animations); + final boolean isOpening = Transitions.isOpeningType(info.getType()); + + final Runnable onAnimFinish = () -> { + if (!animations.isEmpty()) return; + mAnimations.remove(transition); + finishCallback.run(); + }; + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change change = info.getChanges().get(i); + + // Don't animate anything with an animating parent + if (change.getParent() != null) continue; + + final int mode = change.getMode(); + if (isOpening && (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT)) { + if ((change.getFlags() & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) != 0) { + // This received a transferred starting window, so don't animate + continue; + } + // fade in + startExampleAnimation( + animations, change.getLeash(), true /* show */, onAnimFinish); + } else if (!isOpening && (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK)) { + // fade out + startExampleAnimation( + animations, change.getLeash(), false /* show */, onAnimFinish); + } + } + t.apply(); + // run finish now in-case there are no animations + onAnimFinish.run(); + return true; + } + + @Nullable + @Override + public WindowContainerTransaction handleRequest(int type, @NonNull IBinder transition, + @Nullable ActivityManager.RunningTaskInfo triggerTask) { + return null; + } + + // TODO(shell-transitions): real animations + private void startExampleAnimation(@NonNull ArrayList animations, + @NonNull SurfaceControl leash, boolean show, @NonNull Runnable finishCallback) { + final float end = show ? 1.f : 0.f; + final float start = 1.f - end; + final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); + final ValueAnimator va = ValueAnimator.ofFloat(start, end); + va.setDuration(500); + va.addUpdateListener(animation -> { + float fraction = animation.getAnimatedFraction(); + transaction.setAlpha(leash, start * (1.f - fraction) + end * fraction); + transaction.apply(); + }); + final Runnable finisher = () -> { + transaction.setAlpha(leash, end); + transaction.apply(); + mTransactionPool.release(transaction); + mMainExecutor.execute(() -> { + animations.remove(va); + finishCallback.run(); + }); + }; + va.addListener(new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { } + + @Override + public void onAnimationEnd(Animator animation) { + finisher.run(); + } + + @Override + public void onAnimationCancel(Animator animation) { + finisher.run(); + } + + @Override + public void onAnimationRepeat(Animator animation) { } + }); + animations.add(va); + mAnimExecutor.execute(va::start); + } +} 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 7ce71b0a11589..3b2ac70007e45 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 @@ -23,8 +23,6 @@ import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT; -import android.animation.Animator; -import android.animation.ValueAnimator; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; @@ -41,6 +39,7 @@ import android.window.WindowOrganizer; import androidx.annotation.BinderThread; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.ShellExecutor; @@ -48,6 +47,7 @@ import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.protolog.ShellProtoLogGroup; import java.util.ArrayList; +import java.util.Arrays; /** Plays transition animations */ public class Transitions { @@ -58,7 +58,6 @@ public class Transitions { SystemProperties.getBoolean("persist.debug.shell_transit", false); private final WindowOrganizer mOrganizer; - private final TransactionPool mTransactionPool; private final ShellExecutor mMainExecutor; private final ShellExecutor mAnimExecutor; private final TransitionPlayerImpl mPlayerImpl; @@ -67,7 +66,6 @@ public class Transitions { private final ArrayList mHandlers = new ArrayList<>(); private static final class ActiveTransition { - ArrayList mAnimations = null; TransitionHandler mFirstHandler = null; } @@ -77,10 +75,11 @@ public class Transitions { public Transitions(@NonNull WindowOrganizer organizer, @NonNull TransactionPool pool, @NonNull ShellExecutor mainExecutor, @NonNull ShellExecutor animExecutor) { mOrganizer = organizer; - mTransactionPool = pool; mMainExecutor = mainExecutor; mAnimExecutor = animExecutor; mPlayerImpl = new TransitionPlayerImpl(); + // The very last handler (0 in the list) should be the default one. + mHandlers.add(new DefaultTransitionHandler(pool, mainExecutor, animExecutor)); } /** Register this transition handler with Core */ @@ -104,47 +103,10 @@ public class Transitions { return mAnimExecutor; } - // TODO(shell-transitions): real animations - private void startExampleAnimation(@NonNull IBinder transition, @NonNull SurfaceControl leash, - boolean show) { - final float end = show ? 1.f : 0.f; - final float start = 1.f - end; - final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); - final ValueAnimator va = ValueAnimator.ofFloat(start, end); - va.setDuration(500); - va.addUpdateListener(animation -> { - float fraction = animation.getAnimatedFraction(); - transaction.setAlpha(leash, start * (1.f - fraction) + end * fraction); - transaction.apply(); - }); - final Runnable finisher = () -> { - transaction.setAlpha(leash, end); - transaction.apply(); - mTransactionPool.release(transaction); - mMainExecutor.execute(() -> { - mActiveTransitions.get(transition).mAnimations.remove(va); - onFinish(transition); - }); - }; - va.addListener(new Animator.AnimatorListener() { - @Override - public void onAnimationStart(Animator animation) { } - - @Override - public void onAnimationEnd(Animator animation) { - finisher.run(); - } - - @Override - public void onAnimationCancel(Animator animation) { - finisher.run(); - } - - @Override - public void onAnimationRepeat(Animator animation) { } - }); - mActiveTransitions.get(transition).mAnimations.add(va); - mAnimExecutor.execute(va::start); + /** Only use this in tests. This is used to avoid running animations during tests. */ + @VisibleForTesting + void replaceDefaultHandlerForTest(TransitionHandler handler) { + mHandlers.set(0, handler); } /** @return true if the transition was triggered by opening something vs closing something */ @@ -217,18 +179,16 @@ public class Transitions { } } - private void onTransitionReady(@NonNull IBinder transitionToken, @NonNull TransitionInfo info, + @VisibleForTesting + void onTransitionReady(@NonNull IBinder transitionToken, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "onTransitionReady %s: %s", transitionToken, info); final ActiveTransition active = mActiveTransitions.get(transitionToken); if (active == null) { throw new IllegalStateException("Got transitionReady for non-active transition " - + transitionToken + ". expecting one of " + mActiveTransitions.keySet()); - } - if (active.mAnimations != null) { - throw new IllegalStateException("Got a duplicate onTransitionReady call for " - + transitionToken); + + transitionToken + ". expecting one of " + + Arrays.toString(mActiveTransitions.keySet().toArray())); } if (!info.getRootLeash().isValid()) { // Invalid root-leash implies that the transition is empty/no-op, so just do @@ -253,44 +213,21 @@ public class Transitions { return; } } - - // No handler chose to perform this animation, so fall-back to the - // default animation handling. - final boolean isOpening = isOpeningType(info.getType()); - active.mAnimations = new ArrayList<>(); // Play fade animations - for (int i = info.getChanges().size() - 1; i >= 0; --i) { - final TransitionInfo.Change change = info.getChanges().get(i); - - // Don't animate anything with an animating parent - if (change.getParent() != null) continue; - - final int mode = info.getChanges().get(i).getMode(); - if (isOpening && (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT)) { - if ((change.getFlags() & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) != 0) { - // This received a transferred starting window, so don't animate - continue; - } - // fade in - startExampleAnimation(transitionToken, change.getLeash(), true /* show */); - } else if (!isOpening && (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK)) { - // fade out - startExampleAnimation(transitionToken, change.getLeash(), false /* show */); - } - } - t.apply(); - onFinish(transitionToken); + throw new IllegalStateException( + "This shouldn't happen, maybe the default handler is broken."); } private void onFinish(IBinder transition) { - final ActiveTransition active = mActiveTransitions.get(transition); - if (active.mAnimations != null && !active.mAnimations.isEmpty()) return; + if (!mActiveTransitions.containsKey(transition)) { + throw new IllegalStateException("Trying to finish an already-finished transition."); + } ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition animations finished, notifying core %s", transition); mActiveTransitions.remove(transition); mOrganizer.finishTransition(transition, null, null); } - private void requestStartTransition(int type, @NonNull IBinder transitionToken, + void requestStartTransition(int type, @NonNull IBinder transitionToken, @Nullable ActivityManager.RunningTaskInfo triggerTask) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition requested: type=%d %s", type, transitionToken); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestShellExecutor.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestShellExecutor.java new file mode 100644 index 0000000000000..88b8498f27123 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestShellExecutor.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2021 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.wm.shell; + +import com.android.wm.shell.common.ShellExecutor; + +import java.util.ArrayList; + +/** + * Really basic test executor. It just gathers all events in a blob. The only option is to + * execute everything at once. If better control over delayed execution is needed, please add it. + */ +public class TestShellExecutor implements ShellExecutor { + final ArrayList mRunnables = new ArrayList<>(); + + @Override + public void execute(Runnable runnable) { + mRunnables.add(runnable); + } + + @Override + public void executeDelayed(Runnable r, long delayMillis) { + mRunnables.add(r); + } + + @Override + public void removeCallbacks(Runnable r) { + mRunnables.remove(r); + } + + public void flushAll() { + for (int i = mRunnables.size() - 1; i >= 0; --i) { + mRunnables.get(i).run(); + } + mRunnables.clear(); + } +} 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 new file mode 100644 index 0000000000000..c46e59ad396a0 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java @@ -0,0 +1,243 @@ +/* + * Copyright (C) 2021 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.wm.shell.transition; + +import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; +import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; +import static android.view.WindowManager.TRANSIT_CHANGE; +import static android.view.WindowManager.TRANSIT_CLOSE; +import static android.view.WindowManager.TRANSIT_OPEN; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.app.ActivityManager.RunningTaskInfo; +import android.os.Binder; +import android.os.IBinder; +import android.view.SurfaceControl; +import android.view.WindowManager; +import android.window.TransitionInfo; +import android.window.WindowContainerTransaction; +import android.window.WindowOrganizer; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.TestShellExecutor; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.TransactionPool; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; + +/** + * Tests for the shell transitions. + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class ShellTransitionTests { + + private final WindowOrganizer mOrganizer = mock(WindowOrganizer.class); + private final TransactionPool mTransactionPool = mock(TransactionPool.class); + private final TestShellExecutor mMainExecutor = new TestShellExecutor(); + private final ShellExecutor mAnimExecutor = new TestShellExecutor(); + private final TestTransitionHandler mDefaultHandler = new TestTransitionHandler(); + + @Before + public void setUp() { + doAnswer(invocation -> invocation.getArguments()[1]) + .when(mOrganizer).startTransition(anyInt(), any(), any()); + } + + @Test + public void testBasicTransitionFlow() { + Transitions transitions = new Transitions(mOrganizer, mTransactionPool, mMainExecutor, + mAnimExecutor); + transitions.replaceDefaultHandlerForTest(mDefaultHandler); + + IBinder transitToken = new Binder(); + transitions.requestStartTransition(TRANSIT_OPEN, transitToken, null /* trigger */); + verify(mOrganizer, times(1)).startTransition(eq(TRANSIT_OPEN), eq(transitToken), any()); + TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); + transitions.onTransitionReady(transitToken, info, mock(SurfaceControl.Transaction.class)); + assertEquals(1, mDefaultHandler.activeCount()); + mDefaultHandler.finishAll(); + mMainExecutor.flushAll(); + verify(mOrganizer, times(1)).finishTransition(eq(transitToken), any(), any()); + } + + @Test + public void testNonDefaultHandler() { + Transitions transitions = new Transitions(mOrganizer, mTransactionPool, mMainExecutor, + mAnimExecutor); + transitions.replaceDefaultHandlerForTest(mDefaultHandler); + + final WindowContainerTransaction handlerWCT = new WindowContainerTransaction(); + // Make a test handler that only responds to multi-window triggers AND only animates + // Change transitions. + TestTransitionHandler testHandler = new TestTransitionHandler() { + @Override + public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction t, @NonNull Runnable finishCallback) { + for (TransitionInfo.Change chg : info.getChanges()) { + if (chg.getMode() == TRANSIT_CHANGE) { + return super.startAnimation(transition, info, t, finishCallback); + } + } + return false; + } + + @Nullable + @Override + public WindowContainerTransaction handleRequest(int type, @NonNull IBinder transition, + @Nullable RunningTaskInfo triggerTask) { + return (triggerTask != null + && triggerTask.getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW) + ? handlerWCT : null; + } + }; + transitions.addHandler(testHandler); + + IBinder transitToken = new Binder(); + TransitionInfo open = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); + + // Make a request that will be rejected by the testhandler. + transitions.requestStartTransition(TRANSIT_OPEN, transitToken, null /* trigger */); + verify(mOrganizer, times(1)).startTransition(eq(TRANSIT_OPEN), eq(transitToken), isNull()); + transitions.onTransitionReady(transitToken, open, mock(SurfaceControl.Transaction.class)); + assertEquals(1, mDefaultHandler.activeCount()); + assertEquals(0, testHandler.activeCount()); + mDefaultHandler.finishAll(); + mMainExecutor.flushAll(); + + // Make a request that will be handled by testhandler but not animated by it. + RunningTaskInfo mwTaskInfo = + createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD); + transitions.requestStartTransition(TRANSIT_OPEN, transitToken, mwTaskInfo); + verify(mOrganizer, times(1)).startTransition( + eq(TRANSIT_OPEN), eq(transitToken), eq(handlerWCT)); + transitions.onTransitionReady(transitToken, open, mock(SurfaceControl.Transaction.class)); + assertEquals(1, mDefaultHandler.activeCount()); + assertEquals(0, testHandler.activeCount()); + mDefaultHandler.finishAll(); + mMainExecutor.flushAll(); + + // Make a request that will be handled AND animated by testhandler. + // Add an aggressive handler (doesn't handle but always animates) on top to make sure that + // the test handler gets first shot at animating since it claimed to handle it. + TestTransitionHandler topHandler = new TestTransitionHandler(); + transitions.addHandler(topHandler); + transitions.requestStartTransition(TRANSIT_CHANGE, transitToken, mwTaskInfo); + verify(mOrganizer, times(1)).startTransition( + eq(TRANSIT_OPEN), eq(transitToken), eq(handlerWCT)); + TransitionInfo change = new TransitionInfoBuilder(TRANSIT_CHANGE) + .addChange(TRANSIT_CHANGE).build(); + transitions.onTransitionReady(transitToken, change, mock(SurfaceControl.Transaction.class)); + assertEquals(0, mDefaultHandler.activeCount()); + assertEquals(1, testHandler.activeCount()); + assertEquals(0, topHandler.activeCount()); + testHandler.finishAll(); + mMainExecutor.flushAll(); + } + + class TransitionInfoBuilder { + final TransitionInfo mInfo; + + TransitionInfoBuilder(@WindowManager.TransitionType int type) { + mInfo = new TransitionInfo(type, 0 /* flags */); + mInfo.setRootLeash(createMockSurface(true /* valid */), 0, 0); + } + + TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode, + RunningTaskInfo taskInfo) { + final TransitionInfo.Change change = + new TransitionInfo.Change(null /* token */, null /* leash */); + change.setMode(mode); + change.setTaskInfo(taskInfo); + mInfo.addChange(change); + return this; + } + + TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode) { + return addChange(mode, null /* taskInfo */); + } + + TransitionInfo build() { + return mInfo; + } + } + + class TestTransitionHandler implements Transitions.TransitionHandler { + final ArrayList mFinishes = new ArrayList<>(); + + @Override + public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction t, @NonNull Runnable finishCallback) { + mFinishes.add(finishCallback); + return true; + } + + @Nullable + @Override + public WindowContainerTransaction handleRequest(int type, @NonNull IBinder transition, + @Nullable RunningTaskInfo triggerTask) { + return null; + } + + void finishAll() { + for (int i = mFinishes.size() - 1; i >= 0; --i) { + mFinishes.get(i).run(); + } + mFinishes.clear(); + } + + int activeCount() { + return mFinishes.size(); + } + } + + private static SurfaceControl createMockSurface(boolean valid) { + SurfaceControl sc = mock(SurfaceControl.class); + doReturn(valid).when(sc).isValid(); + return sc; + } + + private static RunningTaskInfo createTaskInfo(int taskId, int windowingMode, int activityType) { + RunningTaskInfo taskInfo = new RunningTaskInfo(); + taskInfo.taskId = taskId; + taskInfo.configuration.windowConfiguration.setWindowingMode(windowingMode); + taskInfo.configuration.windowConfiguration.setActivityType(activityType); + return taskInfo; + } + +}