blob: e2f2b71cea04455bf6aa944329590e59827a376e [file] [log] [blame]
/*
* 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_HOME;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS;
import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_UNSPECIFIED;
import static android.view.WindowManager.TRANSIT_CHANGE;
import static android.view.WindowManager.TRANSIT_CLOSE;
import static android.view.WindowManager.TRANSIT_FIRST_CUSTOM;
import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY;
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_DISPLAY_HAS_ALERT_WINDOWS;
import static android.window.TransitionInfo.FLAG_IS_DISPLAY;
import static android.window.TransitionInfo.FLAG_TRANSLUCENT;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
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 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.clearInvocations;
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.content.Context;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import android.view.IDisplayWindowListener;
import android.view.IWindowManager;
import android.view.Surface;
import android.view.SurfaceControl;
import android.view.WindowManager;
import android.window.IRemoteTransition;
import android.window.IRemoteTransitionFinishedCallback;
import android.window.RemoteTransition;
import android.window.TransitionFilter;
import android.window.TransitionInfo;
import android.window.TransitionRequestInfo;
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 androidx.test.platform.app.InstrumentationRegistry;
import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.TestShellExecutor;
import com.android.wm.shell.common.DisplayController;
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.
*
* Build/Install/Run:
* atest WMShellUnitTests:ShellTransitionTests
*/
@SmallTest
@RunWith(AndroidJUnit4.class)
public class ShellTransitionTests extends ShellTestCase {
private final WindowOrganizer mOrganizer = mock(WindowOrganizer.class);
private final TransactionPool mTransactionPool = mock(TransactionPool.class);
private final Context mContext =
InstrumentationRegistry.getInstrumentation().getTargetContext();
private final TestShellExecutor mMainExecutor = new TestShellExecutor();
private final ShellExecutor mAnimExecutor = new TestShellExecutor();
private final TestTransitionHandler mDefaultHandler = new TestTransitionHandler();
private final Handler mMainHandler = new Handler(Looper.getMainLooper());
@Before
public void setUp() {
doAnswer(invocation -> invocation.getArguments()[1])
.when(mOrganizer).startTransition(anyInt(), any(), any());
}
@Test
public void testBasicTransitionFlow() {
Transitions transitions = createTestTransitions();
transitions.replaceDefaultHandlerForTest(mDefaultHandler);
IBinder transitToken = new Binder();
transitions.requestStartTransition(transitToken,
new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */));
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),
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 = createTestTransitions();
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 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);
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(transitToken,
new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */));
verify(mOrganizer, times(1)).startTransition(eq(TRANSIT_OPEN), eq(transitToken), isNull());
transitions.onTransitionReady(transitToken, open, mock(SurfaceControl.Transaction.class),
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(transitToken,
new TransitionRequestInfo(TRANSIT_OPEN, mwTaskInfo, null /* remote */));
verify(mOrganizer, times(1)).startTransition(
eq(TRANSIT_OPEN), eq(transitToken), eq(handlerWCT));
transitions.onTransitionReady(transitToken, open, mock(SurfaceControl.Transaction.class),
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(transitToken,
new TransitionRequestInfo(TRANSIT_CHANGE, mwTaskInfo, null /* remote */));
verify(mOrganizer, times(1)).startTransition(
eq(TRANSIT_CHANGE), eq(transitToken), eq(handlerWCT));
TransitionInfo change = new TransitionInfoBuilder(TRANSIT_CHANGE)
.addChange(TRANSIT_CHANGE).build();
transitions.onTransitionReady(transitToken, change, mock(SurfaceControl.Transaction.class),
mock(SurfaceControl.Transaction.class));
assertEquals(0, mDefaultHandler.activeCount());
assertEquals(1, testHandler.activeCount());
assertEquals(0, topHandler.activeCount());
testHandler.finishAll();
mMainExecutor.flushAll();
}
@Test
public void testRequestRemoteTransition() {
Transitions transitions = createTestTransitions();
transitions.replaceDefaultHandlerForTest(mDefaultHandler);
final boolean[] remoteCalled = new boolean[]{false};
final WindowContainerTransaction remoteFinishWCT = new WindowContainerTransaction();
IRemoteTransition testRemote = new IRemoteTransition.Stub() {
@Override
public void startAnimation(IBinder token, TransitionInfo info,
SurfaceControl.Transaction t,
IRemoteTransitionFinishedCallback finishCallback) throws RemoteException {
remoteCalled[0] = true;
finishCallback.onTransitionFinished(remoteFinishWCT, null /* sct */);
}
@Override
public void mergeAnimation(IBinder token, TransitionInfo info,
SurfaceControl.Transaction t, IBinder mergeTarget,
IRemoteTransitionFinishedCallback finishCallback) throws RemoteException {
}
};
IBinder transitToken = new Binder();
transitions.requestStartTransition(transitToken,
new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */,
new RemoteTransition(testRemote)));
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),
mock(SurfaceControl.Transaction.class));
assertEquals(0, mDefaultHandler.activeCount());
assertTrue(remoteCalled[0]);
mDefaultHandler.finishAll();
mMainExecutor.flushAll();
verify(mOrganizer, times(1)).finishTransition(eq(transitToken), eq(remoteFinishWCT), any());
}
@Test
public void testTransitionFilterActivityType() {
TransitionFilter filter = new TransitionFilter();
filter.mRequirements =
new TransitionFilter.Requirement[]{new TransitionFilter.Requirement()};
filter.mRequirements[0].mActivityType = ACTIVITY_TYPE_HOME;
filter.mRequirements[0].mModes = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT};
final TransitionInfo openHome = new TransitionInfoBuilder(TRANSIT_OPEN)
.addChange(TRANSIT_OPEN,
createTaskInfo(1, WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_HOME)).build();
assertTrue(filter.matches(openHome));
final TransitionInfo openStd = new TransitionInfoBuilder(TRANSIT_OPEN)
.addChange(TRANSIT_OPEN, createTaskInfo(
1, WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD)).build();
assertFalse(filter.matches(openStd));
}
@Test
public void testTransitionFilterMultiRequirement() {
// filter that requires at-least one opening and one closing app
TransitionFilter filter = new TransitionFilter();
filter.mRequirements = new TransitionFilter.Requirement[]{
new TransitionFilter.Requirement(), new TransitionFilter.Requirement()};
filter.mRequirements[0].mModes = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT};
filter.mRequirements[1].mModes = new int[]{TRANSIT_CLOSE, TRANSIT_TO_BACK};
final TransitionInfo openOnly = new TransitionInfoBuilder(TRANSIT_OPEN)
.addChange(TRANSIT_OPEN).build();
assertFalse(filter.matches(openOnly));
final TransitionInfo openClose = new TransitionInfoBuilder(TRANSIT_OPEN)
.addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build();
assertTrue(filter.matches(openClose));
}
@Test
public void testTransitionFilterNotRequirement() {
// filter that requires one opening and NO translucent apps
TransitionFilter filter = new TransitionFilter();
filter.mRequirements = new TransitionFilter.Requirement[]{
new TransitionFilter.Requirement(), new TransitionFilter.Requirement()};
filter.mRequirements[0].mModes = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT};
filter.mRequirements[1].mFlags = FLAG_TRANSLUCENT;
filter.mRequirements[1].mNot = true;
final TransitionInfo openOnly = new TransitionInfoBuilder(TRANSIT_OPEN)
.addChange(TRANSIT_OPEN).build();
assertTrue(filter.matches(openOnly));
final TransitionInfo openAndTranslucent = new TransitionInfoBuilder(TRANSIT_OPEN)
.addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build();
openAndTranslucent.getChanges().get(1).setFlags(FLAG_TRANSLUCENT);
assertFalse(filter.matches(openAndTranslucent));
}
@Test
public void testTransitionFilterChecksTypeSet() {
TransitionFilter filter = new TransitionFilter();
filter.mTypeSet = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT};
final TransitionInfo openOnly = new TransitionInfoBuilder(TRANSIT_OPEN)
.addChange(TRANSIT_OPEN).build();
assertTrue(filter.matches(openOnly));
final TransitionInfo toFrontOnly = new TransitionInfoBuilder(TRANSIT_TO_FRONT)
.addChange(TRANSIT_TO_FRONT).build();
assertTrue(filter.matches(toFrontOnly));
final TransitionInfo closeOnly = new TransitionInfoBuilder(TRANSIT_CLOSE)
.addChange(TRANSIT_CLOSE).build();
assertFalse(filter.matches(closeOnly));
}
@Test
public void testTransitionFilterChecksFlags() {
TransitionFilter filter = new TransitionFilter();
filter.mFlags = TRANSIT_FLAG_KEYGUARD_GOING_AWAY;
final TransitionInfo withFlag = new TransitionInfoBuilder(TRANSIT_TO_BACK,
TRANSIT_FLAG_KEYGUARD_GOING_AWAY)
.addChange(TRANSIT_TO_BACK).build();
assertTrue(filter.matches(withFlag));
final TransitionInfo withoutFlag = new TransitionInfoBuilder(TRANSIT_OPEN)
.addChange(TRANSIT_OPEN).build();
assertFalse(filter.matches(withoutFlag));
}
@Test
public void testTransitionFilterChecksNotFlags() {
TransitionFilter filter = new TransitionFilter();
filter.mNotFlags = TRANSIT_FLAG_KEYGUARD_GOING_AWAY;
final TransitionInfo withFlag = new TransitionInfoBuilder(TRANSIT_TO_BACK,
TRANSIT_FLAG_KEYGUARD_GOING_AWAY)
.addChange(TRANSIT_TO_BACK).build();
assertFalse(filter.matches(withFlag));
final TransitionInfo withoutFlag = new TransitionInfoBuilder(TRANSIT_OPEN)
.addChange(TRANSIT_OPEN).build();
assertTrue(filter.matches(withoutFlag));
}
@Test
public void testRegisteredRemoteTransition() {
Transitions transitions = createTestTransitions();
transitions.replaceDefaultHandlerForTest(mDefaultHandler);
final boolean[] remoteCalled = new boolean[]{false};
IRemoteTransition testRemote = new IRemoteTransition.Stub() {
@Override
public void startAnimation(IBinder token, TransitionInfo info,
SurfaceControl.Transaction t,
IRemoteTransitionFinishedCallback finishCallback) throws RemoteException {
remoteCalled[0] = true;
finishCallback.onTransitionFinished(null /* wct */, null /* sct */);
}
@Override
public void mergeAnimation(IBinder token, TransitionInfo info,
SurfaceControl.Transaction t, IBinder mergeTarget,
IRemoteTransitionFinishedCallback finishCallback) throws RemoteException {
}
};
TransitionFilter filter = new TransitionFilter();
filter.mRequirements =
new TransitionFilter.Requirement[]{new TransitionFilter.Requirement()};
filter.mRequirements[0].mModes = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT};
transitions.registerRemote(filter, new RemoteTransition(testRemote));
mMainExecutor.flushAll();
IBinder transitToken = new Binder();
transitions.requestStartTransition(transitToken,
new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */));
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),
mock(SurfaceControl.Transaction.class));
assertEquals(0, mDefaultHandler.activeCount());
assertTrue(remoteCalled[0]);
mDefaultHandler.finishAll();
mMainExecutor.flushAll();
verify(mOrganizer, times(1)).finishTransition(eq(transitToken), any(), any());
}
@Test
public void testOneShotRemoteHandler() {
Transitions transitions = createTestTransitions();
transitions.replaceDefaultHandlerForTest(mDefaultHandler);
final boolean[] remoteCalled = new boolean[]{false};
final WindowContainerTransaction remoteFinishWCT = new WindowContainerTransaction();
IRemoteTransition testRemote = new IRemoteTransition.Stub() {
@Override
public void startAnimation(IBinder token, TransitionInfo info,
SurfaceControl.Transaction t,
IRemoteTransitionFinishedCallback finishCallback) throws RemoteException {
remoteCalled[0] = true;
finishCallback.onTransitionFinished(remoteFinishWCT, null /* sct */);
}
@Override
public void mergeAnimation(IBinder token, TransitionInfo info,
SurfaceControl.Transaction t, IBinder mergeTarget,
IRemoteTransitionFinishedCallback finishCallback) throws RemoteException {
}
};
final int transitType = TRANSIT_FIRST_CUSTOM + 1;
OneShotRemoteHandler oneShot = new OneShotRemoteHandler(mMainExecutor,
new RemoteTransition(testRemote));
// Verify that it responds to the remote but not other things.
IBinder transitToken = new Binder();
assertNotNull(oneShot.handleRequest(transitToken,
new TransitionRequestInfo(transitType, null, new RemoteTransition(testRemote))));
assertNull(oneShot.handleRequest(transitToken,
new TransitionRequestInfo(transitType, null, null)));
Transitions.TransitionFinishCallback testFinish =
mock(Transitions.TransitionFinishCallback.class);
// Verify that it responds to animation properly
oneShot.setTransition(transitToken);
IBinder anotherToken = new Binder();
assertFalse(oneShot.startAnimation(anotherToken, new TransitionInfo(transitType, 0),
mock(SurfaceControl.Transaction.class), mock(SurfaceControl.Transaction.class),
testFinish));
assertTrue(oneShot.startAnimation(transitToken, new TransitionInfo(transitType, 0),
mock(SurfaceControl.Transaction.class), mock(SurfaceControl.Transaction.class),
testFinish));
}
@Test
public void testTransitionQueueing() {
Transitions transitions = createTestTransitions();
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();
transitions.onTransitionReady(transitToken1, info1, mock(SurfaceControl.Transaction.class),
mock(SurfaceControl.Transaction.class));
assertEquals(1, mDefaultHandler.activeCount());
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();
transitions.onTransitionReady(transitToken2, info2, mock(SurfaceControl.Transaction.class),
mock(SurfaceControl.Transaction.class));
// default handler doesn't merge by default, so it shouldn't increment active count.
assertEquals(1, mDefaultHandler.activeCount());
assertEquals(0, mDefaultHandler.mergeCount());
verify(mOrganizer, times(0)).finishTransition(eq(transitToken1), any(), any());
verify(mOrganizer, times(0)).finishTransition(eq(transitToken2), any(), any());
mDefaultHandler.finishAll();
mMainExecutor.flushAll();
// first transition finished
verify(mOrganizer, times(1)).finishTransition(eq(transitToken1), any(), any());
verify(mOrganizer, times(0)).finishTransition(eq(transitToken2), any(), any());
// But now the "queued" transition is running
assertEquals(1, mDefaultHandler.activeCount());
mDefaultHandler.finishAll();
mMainExecutor.flushAll();
verify(mOrganizer, times(1)).finishTransition(eq(transitToken2), any(), any());
}
@Test
public void testTransitionMerging() {
Transitions transitions = createTestTransitions();
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();
transitions.onTransitionReady(transitToken1, info1, mock(SurfaceControl.Transaction.class),
mock(SurfaceControl.Transaction.class));
assertEquals(1, mDefaultHandler.activeCount());
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();
transitions.onTransitionReady(transitToken2, info2, mock(SurfaceControl.Transaction.class),
mock(SurfaceControl.Transaction.class));
// it should still only have 1 active, but then show 1 merged
assertEquals(1, mDefaultHandler.activeCount());
assertEquals(1, mDefaultHandler.mergeCount());
verify(mOrganizer, times(0)).finishTransition(eq(transitToken1), any(), any());
// We don't tell organizer it is finished yet (since we still want to maintain ordering)
verify(mOrganizer, times(0)).finishTransition(eq(transitToken2), any(), any());
mDefaultHandler.finishAll();
mMainExecutor.flushAll();
// transition + merged all finished.
verify(mOrganizer, times(1)).finishTransition(eq(transitToken1), any(), any());
verify(mOrganizer, times(1)).finishTransition(eq(transitToken2), any(), any());
// Make sure nothing was queued
assertEquals(0, mDefaultHandler.activeCount());
}
@Test
public void testShouldRotateSeamlessly() throws Exception {
final RunningTaskInfo taskInfo =
createTaskInfo(1, WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD);
final RunningTaskInfo taskInfoPip =
createTaskInfo(1, WINDOWING_MODE_PINNED, ACTIVITY_TYPE_STANDARD);
final DisplayController displays = createTestDisplayController();
final @Surface.Rotation int upsideDown = displays
.getDisplayLayout(DEFAULT_DISPLAY).getUpsideDownRotation();
final TransitionInfo normalDispRotate = new TransitionInfoBuilder(TRANSIT_CHANGE)
.addChange(new ChangeBuilder(TRANSIT_CHANGE).setFlags(FLAG_IS_DISPLAY).setRotate()
.build())
.addChange(new ChangeBuilder(TRANSIT_CHANGE).setTask(taskInfo).setRotate().build())
.build();
assertFalse(DefaultTransitionHandler.isRotationSeamless(normalDispRotate, displays));
// Seamless if all tasks are seamless
final TransitionInfo rotateSeamless = new TransitionInfoBuilder(TRANSIT_CHANGE)
.addChange(new ChangeBuilder(TRANSIT_CHANGE).setFlags(FLAG_IS_DISPLAY).setRotate()
.build())
.addChange(new ChangeBuilder(TRANSIT_CHANGE).setTask(taskInfo)
.setRotate(ROTATION_ANIMATION_SEAMLESS).build())
.build();
assertTrue(DefaultTransitionHandler.isRotationSeamless(rotateSeamless, displays));
// Not seamless if there is PiP (or any other non-seamless task)
final TransitionInfo pipDispRotate = new TransitionInfoBuilder(TRANSIT_CHANGE)
.addChange(new ChangeBuilder(TRANSIT_CHANGE).setFlags(FLAG_IS_DISPLAY).setRotate()
.build())
.addChange(new ChangeBuilder(TRANSIT_CHANGE).setTask(taskInfo)
.setRotate(ROTATION_ANIMATION_SEAMLESS).build())
.addChange(new ChangeBuilder(TRANSIT_CHANGE).setTask(taskInfoPip)
.setRotate().build())
.build();
assertFalse(DefaultTransitionHandler.isRotationSeamless(pipDispRotate, displays));
// Not seamless if one of rotations is upside-down
final TransitionInfo seamlessUpsideDown = new TransitionInfoBuilder(TRANSIT_CHANGE)
.addChange(new ChangeBuilder(TRANSIT_CHANGE).setFlags(FLAG_IS_DISPLAY)
.setRotate(upsideDown, ROTATION_ANIMATION_UNSPECIFIED).build())
.addChange(new ChangeBuilder(TRANSIT_CHANGE).setTask(taskInfo)
.setRotate(upsideDown, ROTATION_ANIMATION_SEAMLESS).build())
.build();
assertFalse(DefaultTransitionHandler.isRotationSeamless(seamlessUpsideDown, displays));
// Not seamless if system alert windows
final TransitionInfo seamlessButAlert = new TransitionInfoBuilder(TRANSIT_CHANGE)
.addChange(new ChangeBuilder(TRANSIT_CHANGE).setFlags(
FLAG_IS_DISPLAY | FLAG_DISPLAY_HAS_ALERT_WINDOWS).setRotate().build())
.addChange(new ChangeBuilder(TRANSIT_CHANGE).setTask(taskInfo)
.setRotate(ROTATION_ANIMATION_SEAMLESS).build())
.build();
assertFalse(DefaultTransitionHandler.isRotationSeamless(seamlessButAlert, displays));
// Not seamless if there is no changed task.
final TransitionInfo noTask = new TransitionInfoBuilder(TRANSIT_CHANGE)
.addChange(new ChangeBuilder(TRANSIT_CHANGE).setFlags(FLAG_IS_DISPLAY)
.setRotate().build())
.build();
assertFalse(DefaultTransitionHandler.isRotationSeamless(noTask, displays));
// Seamless if display is explicitly seamless.
final TransitionInfo seamlessDisplay = new TransitionInfoBuilder(TRANSIT_CHANGE)
.addChange(new ChangeBuilder(TRANSIT_CHANGE).setFlags(FLAG_IS_DISPLAY)
.setRotate(ROTATION_ANIMATION_SEAMLESS).build())
.build();
assertTrue(DefaultTransitionHandler.isRotationSeamless(seamlessDisplay, displays));
}
@Test
public void testRunWhenIdle() {
Transitions transitions = createTestTransitions();
transitions.replaceDefaultHandlerForTest(mDefaultHandler);
Runnable runnable1 = mock(Runnable.class);
Runnable runnable2 = mock(Runnable.class);
Runnable runnable3 = mock(Runnable.class);
Runnable runnable4 = mock(Runnable.class);
transitions.runOnIdle(runnable1);
// runnable1 is executed immediately because there are no active transitions.
verify(runnable1, times(1)).run();
clearInvocations(runnable1);
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();
transitions.onTransitionReady(transitToken1, info1, mock(SurfaceControl.Transaction.class),
mock(SurfaceControl.Transaction.class));
assertEquals(1, mDefaultHandler.activeCount());
transitions.runOnIdle(runnable2);
transitions.runOnIdle(runnable3);
// runnable2 and runnable3 aren't executed immediately because there is an active
// transaction.
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();
transitions.onTransitionReady(transitToken2, info2, mock(SurfaceControl.Transaction.class),
mock(SurfaceControl.Transaction.class));
assertEquals(1, mDefaultHandler.activeCount());
mDefaultHandler.finishAll();
mMainExecutor.flushAll();
// first transition finished
verify(mOrganizer, times(1)).finishTransition(eq(transitToken1), any(), any());
verify(mOrganizer, times(0)).finishTransition(eq(transitToken2), any(), any());
// But now the "queued" transition is running
assertEquals(1, mDefaultHandler.activeCount());
// runnable2 and runnable3 are still not executed because the second transition is still
// active.
verify(runnable2, times(0)).run();
verify(runnable3, times(0)).run();
mDefaultHandler.finishAll();
mMainExecutor.flushAll();
verify(mOrganizer, times(1)).finishTransition(eq(transitToken2), any(), any());
// runnable2 and runnable3 are executed after the second transition finishes because there
// are no other active transitions, runnable1 isn't executed again.
verify(runnable1, times(0)).run();
verify(runnable2, times(1)).run();
verify(runnable3, times(1)).run();
clearInvocations(runnable2);
clearInvocations(runnable3);
transitions.runOnIdle(runnable4);
// runnable4 is executed immediately because there are no active transitions, all other
// runnables aren't executed again.
verify(runnable1, times(0)).run();
verify(runnable2, times(0)).run();
verify(runnable3, times(0)).run();
verify(runnable4, times(1)).run();
}
class TransitionInfoBuilder {
final TransitionInfo mInfo;
TransitionInfoBuilder(@WindowManager.TransitionType int type) {
this(type, 0 /* flags */);
}
TransitionInfoBuilder(@WindowManager.TransitionType int type,
@WindowManager.TransitionFlags int flags) {
mInfo = new TransitionInfo(type, 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 */);
}
TransitionInfoBuilder addChange(TransitionInfo.Change change) {
mInfo.addChange(change);
return this;
}
TransitionInfo build() {
return mInfo;
}
}
class ChangeBuilder {
final TransitionInfo.Change mChange;
ChangeBuilder(@WindowManager.TransitionType int mode) {
mChange = new TransitionInfo.Change(null /* token */, null /* leash */);
mChange.setMode(mode);
}
ChangeBuilder setFlags(@TransitionInfo.ChangeFlags int flags) {
mChange.setFlags(flags);
return this;
}
ChangeBuilder setTask(RunningTaskInfo taskInfo) {
mChange.setTaskInfo(taskInfo);
return this;
}
ChangeBuilder setRotate(int anim) {
return setRotate(Surface.ROTATION_90, anim);
}
ChangeBuilder setRotate() {
return setRotate(ROTATION_ANIMATION_UNSPECIFIED);
}
ChangeBuilder setRotate(@Surface.Rotation int target, int anim) {
mChange.setRotation(Surface.ROTATION_0, target);
mChange.setRotationAnimation(anim);
return this;
}
TransitionInfo.Change build() {
return mChange;
}
}
class TestTransitionHandler implements Transitions.TransitionHandler {
ArrayList<Transitions.TransitionFinishCallback> mFinishes = new ArrayList<>();
final ArrayList<IBinder> mMerged = new ArrayList<>();
boolean mSimulateMerge = false;
@Override
public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction startTransaction,
@NonNull SurfaceControl.Transaction finishTransaction,
@NonNull Transitions.TransitionFinishCallback finishCallback) {
mFinishes.add(finishCallback);
return true;
}
@Override
public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget,
@NonNull Transitions.TransitionFinishCallback finishCallback) {
if (!mSimulateMerge) return;
mMerged.add(transition);
finishCallback.onTransitionFinished(null /* wct */, null /* wctCB */);
}
@Nullable
@Override
public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
@NonNull TransitionRequestInfo request) {
return null;
}
void setSimulateMerge(boolean sim) {
mSimulateMerge = sim;
}
void finishAll() {
final ArrayList<Transitions.TransitionFinishCallback> finishes = mFinishes;
mFinishes = new ArrayList<>();
for (int i = finishes.size() - 1; i >= 0; --i) {
finishes.get(i).onTransitionFinished(null /* wct */, null /* wctCB */);
}
}
int activeCount() {
return mFinishes.size();
}
int mergeCount() {
return mMerged.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;
}
private static RunningTaskInfo createTaskInfo(int taskId) {
RunningTaskInfo taskInfo = new RunningTaskInfo();
taskInfo.taskId = taskId;
return taskInfo;
}
private DisplayController createTestDisplayController() {
IWindowManager mockWM = mock(IWindowManager.class);
final IDisplayWindowListener[] displayListener = new IDisplayWindowListener[1];
try {
doReturn(new int[]{DEFAULT_DISPLAY}).when(mockWM).registerDisplayWindowListener(any());
} catch (RemoteException e) {
// No remote stuff happening, so this can't be hit
}
DisplayController out = new DisplayController(mContext, mockWM, mMainExecutor);
out.initialize();
return out;
}
private Transitions createTestTransitions() {
return new Transitions(mOrganizer, mTransactionPool, createTestDisplayController(),
mContext, mMainExecutor, mMainHandler, mAnimExecutor);
}
//
// private class TestDisplayController extends DisplayController {
// private final DisplayLayout mTestDisplayLayout;
// TestDisplayController() {
// super(mContext, mock(IWindowManager.class), mMainExecutor);
// mTestDisplayLayout = new DisplayLayout();
// mTestDisplayLayout.
// }
//
// @Override
// DisplayLayout
// }
}