Merge "Refactor default transit animations into handler for testing"

This commit is contained in:
Evan Rosky
2021-01-13 18:23:05 +00:00
committed by Android (Google) Code Review
4 changed files with 461 additions and 81 deletions

View File

@@ -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<IBinder, ArrayList<Animator>> 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<Animator> 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<Animator> 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);
}
}

View File

@@ -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<TransitionHandler> mHandlers = new ArrayList<>();
private static final class ActiveTransition {
ArrayList<Animator> 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);

View File

@@ -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<Runnable> 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();
}
}

View File

@@ -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<Runnable> 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;
}
}