diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java index b7de7b8d217b3..1c5b39e8dc16c 100644 --- a/core/java/android/window/TransitionInfo.java +++ b/core/java/android/window/TransitionInfo.java @@ -23,6 +23,7 @@ import static android.app.ActivityOptions.ANIM_SCALE_UP; import static android.app.ActivityOptions.ANIM_THUMBNAIL_SCALE_DOWN; import static android.app.ActivityOptions.ANIM_THUMBNAIL_SCALE_UP; import static android.app.WindowConfiguration.ROTATION_UNDEFINED; +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_NONE; @@ -94,8 +95,16 @@ public final class TransitionInfo implements Parcelable { /** The container can show on top of lock screen. */ public static final int FLAG_OCCLUDES_KEYGUARD = 1 << 6; + /** + * Only for IS_DISPLAY containers. Is set if the display has system alert windows. This is + * used to prevent seamless rotation. + * TODO(b/194540864): Once we can include all windows in transition, then replace this with + * something like FLAG_IS_SYSTEM_ALERT instead. Then we can do mixed rotations. + */ + public static final int FLAG_DISPLAY_HAS_ALERT_WINDOWS = 1 << 7; + /** The first unused bit. This can be used by remotes to attach custom flags to this change. */ - public static final int FLAG_FIRST_CUSTOM = 1 << 7; + public static final int FLAG_FIRST_CUSTOM = 1 << 8; /** @hide */ @IntDef(prefix = { "FLAG_" }, value = { @@ -107,6 +116,7 @@ public final class TransitionInfo implements Parcelable { FLAG_IS_VOICE_INTERACTION, FLAG_IS_DISPLAY, FLAG_OCCLUDES_KEYGUARD, + FLAG_DISPLAY_HAS_ALERT_WINDOWS, FLAG_FIRST_CUSTOM }) public @interface ChangeFlags {} @@ -209,6 +219,10 @@ public final class TransitionInfo implements Parcelable { return mOptions; } + /** + * @return the list of {@link Change}s in this transition. The list is sorted top-to-bottom + * in Z (meaning index 0 is the top-most container). + */ @NonNull public List getChanges() { return mChanges; @@ -290,6 +304,9 @@ public final class TransitionInfo implements Parcelable { if ((flags & FLAG_OCCLUDES_KEYGUARD) != 0) { sb.append((sb.length() == 0 ? "" : "|") + "OCCLUDES_KEYGUARD"); } + if ((flags & FLAG_DISPLAY_HAS_ALERT_WINDOWS) != 0) { + sb.append((sb.length() == 0 ? "" : "|") + "DISPLAY_HAS_ALERT_WINDOWS"); + } if ((flags & FLAG_FIRST_CUSTOM) != 0) { sb.append((sb.length() == 0 ? "" : "|") + "FIRST_CUSTOM"); } @@ -337,6 +354,7 @@ public final class TransitionInfo implements Parcelable { private ActivityManager.RunningTaskInfo mTaskInfo = null; private int mStartRotation = ROTATION_UNDEFINED; private int mEndRotation = ROTATION_UNDEFINED; + private int mRotationAnimation = ROTATION_ANIMATION_UNSPECIFIED; public Change(@Nullable WindowContainerToken container, @NonNull SurfaceControl leash) { mContainer = container; @@ -356,6 +374,7 @@ public final class TransitionInfo implements Parcelable { mTaskInfo = in.readTypedObject(ActivityManager.RunningTaskInfo.CREATOR); mStartRotation = in.readInt(); mEndRotation = in.readInt(); + mRotationAnimation = in.readInt(); } /** Sets the parent of this change's container. The parent must be a participant or null. */ @@ -402,6 +421,14 @@ public final class TransitionInfo implements Parcelable { mEndRotation = end; } + /** + * Sets the app-requested animation type for rotation. Will be one of the + * ROTATION_ANIMATION_ values in {@link android.view.WindowManager.LayoutParams}; + */ + public void setRotationAnimation(int anim) { + mRotationAnimation = anim; + } + /** @return the container that is changing. May be null if non-remotable (eg. activity) */ @Nullable public WindowContainerToken getContainer() { @@ -473,6 +500,11 @@ public final class TransitionInfo implements Parcelable { return mEndRotation; } + /** @return the rotation animation. */ + public int getRotationAnimation() { + return mRotationAnimation; + } + /** @hide */ @Override public void writeToParcel(@NonNull Parcel dest, int flags) { @@ -487,6 +519,7 @@ public final class TransitionInfo implements Parcelable { dest.writeTypedObject(mTaskInfo, flags); dest.writeInt(mStartRotation); dest.writeInt(mEndRotation); + dest.writeInt(mRotationAnimation); } @NonNull @@ -514,7 +547,7 @@ public final class TransitionInfo implements Parcelable { return "{" + mContainer + "(" + mParent + ") leash=" + mLeash + " m=" + modeToString(mMode) + " f=" + flagsToString(mFlags) + " sb=" + mStartAbsBounds + " eb=" + mEndAbsBounds + " eo=" + mEndRelOffset + " r=" - + mStartRotation + "->" + mEndRotation + "}"; + + mStartRotation + "->" + mEndRotation + ":" + mRotationAnimation + "}"; } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java index b7235a31af031..b90283feb1840 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java @@ -82,6 +82,9 @@ public class DisplayLayout { private boolean mHasNavigationBar = false; private boolean mHasStatusBar = false; private int mNavBarFrameHeight = 0; + private boolean mAllowSeamlessRotationDespiteNavBarMoving = false; + private boolean mNavigationBarCanMove = false; + private boolean mReverseDefaultRotation = false; @Override public boolean equals(Object o) { @@ -98,6 +101,10 @@ public class DisplayLayout { && Objects.equals(mStableInsets, other.mStableInsets) && mHasNavigationBar == other.mHasNavigationBar && mHasStatusBar == other.mHasStatusBar + && mAllowSeamlessRotationDespiteNavBarMoving + == other.mAllowSeamlessRotationDespiteNavBarMoving + && mNavigationBarCanMove == other.mNavigationBarCanMove + && mReverseDefaultRotation == other.mReverseDefaultRotation && mNavBarFrameHeight == other.mNavBarFrameHeight; } @@ -105,7 +112,8 @@ public class DisplayLayout { public int hashCode() { return Objects.hash(mUiMode, mWidth, mHeight, mCutout, mRotation, mDensityDpi, mNonDecorInsets, mStableInsets, mHasNavigationBar, mHasStatusBar, - mNavBarFrameHeight); + mNavBarFrameHeight, mAllowSeamlessRotationDespiteNavBarMoving, + mNavigationBarCanMove, mReverseDefaultRotation); } /** @@ -150,6 +158,9 @@ public class DisplayLayout { mDensityDpi = dl.mDensityDpi; mHasNavigationBar = dl.mHasNavigationBar; mHasStatusBar = dl.mHasStatusBar; + mAllowSeamlessRotationDespiteNavBarMoving = dl.mAllowSeamlessRotationDespiteNavBarMoving; + mNavigationBarCanMove = dl.mNavigationBarCanMove; + mReverseDefaultRotation = dl.mReverseDefaultRotation; mNavBarFrameHeight = dl.mNavBarFrameHeight; mNonDecorInsets.set(dl.mNonDecorInsets); mStableInsets.set(dl.mStableInsets); @@ -165,6 +176,10 @@ public class DisplayLayout { mDensityDpi = info.logicalDensityDpi; mHasNavigationBar = hasNavigationBar; mHasStatusBar = hasStatusBar; + mAllowSeamlessRotationDespiteNavBarMoving = res.getBoolean( + R.bool.config_allowSeamlessRotationDespiteNavBarMoving); + mNavigationBarCanMove = res.getBoolean(R.bool.config_navBarCanMove); + mReverseDefaultRotation = res.getBoolean(R.bool.config_reverseDefaultRotation); recalcInsets(res); } @@ -249,6 +264,28 @@ public class DisplayLayout { return mNavBarFrameHeight; } + /** @return whether we can seamlessly rotate even if nav-bar can change sides. */ + public boolean allowSeamlessRotationDespiteNavBarMoving() { + return mAllowSeamlessRotationDespiteNavBarMoving; + } + + /** @return whether the navigation bar will change sides during rotation. */ + public boolean navigationBarCanMove() { + return mNavigationBarCanMove; + } + + /** @return the rotation that would make the physical display "upside down". */ + public int getUpsideDownRotation() { + boolean displayHardwareIsLandscape = mWidth > mHeight; + if ((mRotation % 2) != 0) { + displayHardwareIsLandscape = !displayHardwareIsLandscape; + } + if (displayHardwareIsLandscape) { + return mReverseDefaultRotation ? Surface.ROTATION_270 : Surface.ROTATION_90; + } + return Surface.ROTATION_180; + } + /** Gets the orientation of this layout */ public int getOrientation() { return (mWidth > mHeight) ? ORIENTATION_LANDSCAPE : ORIENTATION_PORTRAIT; 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 index 01134a7c74f7b..b584038bf0e58 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java @@ -23,6 +23,10 @@ import static android.app.ActivityOptions.ANIM_OPEN_CROSS_PROFILE_APPS; import static android.app.ActivityOptions.ANIM_SCALE_UP; import static android.app.ActivityOptions.ANIM_THUMBNAIL_SCALE_DOWN; import static android.app.ActivityOptions.ANIM_THUMBNAIL_SCALE_UP; +import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_JUMPCUT; +import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_ROTATE; +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_KEYGUARD_GOING_AWAY; @@ -31,8 +35,10 @@ import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_RELAUNCH; 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_IS_VOICE_INTERACTION; +import static android.window.TransitionInfo.FLAG_IS_WALLPAPER; import static android.window.TransitionInfo.FLAG_SHOW_WALLPAPER; import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT; import static android.window.TransitionInfo.FLAG_TRANSLUCENT; @@ -68,9 +74,12 @@ import android.window.TransitionRequestInfo; import android.window.WindowContainerTransaction; import com.android.internal.R; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.policy.AttributeCache; import com.android.internal.policy.TransitionAnimation; import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.protolog.ShellProtoLogGroup; @@ -97,6 +106,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { SystemProperties.getBoolean(DISABLE_CUSTOM_TASK_ANIMATION_PROPERTY, true); private final TransactionPool mTransactionPool; + private final DisplayController mDisplayController; private final Context mContext; private final ShellExecutor mMainExecutor; private final ShellExecutor mAnimExecutor; @@ -114,8 +124,10 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { private ScreenRotationAnimation mRotationAnimation; - DefaultTransitionHandler(@NonNull TransactionPool transactionPool, Context context, + DefaultTransitionHandler(@NonNull DisplayController displayController, + @NonNull TransactionPool transactionPool, Context context, @NonNull ShellExecutor mainExecutor, @NonNull ShellExecutor animExecutor) { + mDisplayController = displayController; mTransactionPool = transactionPool; mContext = context; mMainExecutor = mainExecutor; @@ -126,6 +138,110 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { AttributeCache.init(context); } + @VisibleForTesting + static boolean isRotationSeamless(@NonNull TransitionInfo info, + DisplayController displayController) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + "Display is rotating, check if it should be seamless."); + boolean checkedDisplayLayout = false; + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change change = info.getChanges().get(i); + + // Only look at changing things. showing/hiding don't need to rotate. + if (change.getMode() != TRANSIT_CHANGE) continue; + + // This container isn't rotating, so we can ignore it. + if (change.getEndRotation() == change.getStartRotation()) continue; + + if ((change.getFlags() & FLAG_IS_DISPLAY) != 0) { + // In the presence of System Alert windows we can not seamlessly rotate. + if ((change.getFlags() & FLAG_DISPLAY_HAS_ALERT_WINDOWS) != 0) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + " display has system alert windows, so not seamless."); + return false; + } + } else if ((change.getFlags() & FLAG_IS_WALLPAPER) != 0) { + if (change.getRotationAnimation() != ROTATION_ANIMATION_SEAMLESS) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + " wallpaper is participating but isn't seamless."); + return false; + } + } else if (change.getTaskInfo() != null) { + // We only enable seamless rotation if all the visible task windows requested it. + if (change.getRotationAnimation() != ROTATION_ANIMATION_SEAMLESS) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + " task %s isn't requesting seamless, so not seamless.", + change.getTaskInfo().taskId); + return false; + } + + // This is the only way to get display-id currently, so we will check display + // capabilities here + if (!checkedDisplayLayout) { + // only need to check display once. + checkedDisplayLayout = true; + final DisplayLayout displayLayout = displayController.getDisplayLayout( + change.getTaskInfo().displayId); + // For the upside down rotation we don't rotate seamlessly as the navigation + // bar moves position. Note most apps (using orientation:sensor or user as + // opposed to fullSensor) will not enter the reverse portrait orientation, so + // actually the orientation won't change at all. + int upsideDownRotation = displayLayout.getUpsideDownRotation(); + if (change.getStartRotation() == upsideDownRotation + || change.getEndRotation() == upsideDownRotation) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + " rotation involves upside-down portrait, so not seamless."); + return false; + } + + // If the navigation bar can't change sides, then it will jump when we change + // orientations and we don't rotate seamlessly - unless that is allowed, eg. + // with gesture navigation where the navbar is low-profile enough that this + // isn't very noticeable. + if (!displayLayout.allowSeamlessRotationDespiteNavBarMoving() + && (!(displayLayout.navigationBarCanMove() + && (change.getStartAbsBounds().width() + != change.getStartAbsBounds().height())))) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + " nav bar changes sides, so not seamless."); + return false; + } + } + } + } + + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Rotation IS seamless."); + return true; + } + + /** + * Gets the rotation animation for the topmost task. Assumes that seamless is checked + * elsewhere, so it will default SEAMLESS to ROTATE. + */ + private int getRotationAnimation(@NonNull TransitionInfo info) { + // Traverse in top-to-bottom order so that the first task is top-most + for (int i = 0; i < info.getChanges().size(); ++i) { + final TransitionInfo.Change change = info.getChanges().get(i); + + // Only look at changing things. showing/hiding don't need to rotate. + if (change.getMode() != TRANSIT_CHANGE) continue; + + // This container isn't rotating, so we can ignore it. + if (change.getEndRotation() == change.getStartRotation()) continue; + + if (change.getTaskInfo() != null) { + final int anim = change.getRotationAnimation(); + if (anim == ROTATION_ANIMATION_UNSPECIFIED + // Fallback animation for seamless should also be default. + || anim == ROTATION_ANIMATION_SEAMLESS) { + return ROTATION_ANIMATION_ROTATE; + } + return anim; + } + } + return ROTATION_ANIMATION_ROTATE; + } + @Override public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @@ -168,11 +284,15 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { if (info.getType() == TRANSIT_CHANGE && change.getMode() == TRANSIT_CHANGE && (change.getEndRotation() != change.getStartRotation()) && (change.getFlags() & FLAG_IS_DISPLAY) != 0) { - mRotationAnimation = new ScreenRotationAnimation(mContext, mSurfaceSession, - mTransactionPool, startTransaction, change, info.getRootLeash()); - mRotationAnimation.startAnimation(animations, onAnimFinish, - mTransitionAnimationScaleSetting, mMainExecutor, mAnimExecutor); - continue; + boolean isSeamless = isRotationSeamless(info, mDisplayController); + final int anim = getRotationAnimation(info); + if (!(isSeamless || anim == ROTATION_ANIMATION_JUMPCUT)) { + mRotationAnimation = new ScreenRotationAnimation(mContext, mSurfaceSession, + mTransactionPool, startTransaction, change, info.getRootLeash()); + mRotationAnimation.startAnimation(animations, onAnimFinish, + mTransitionAnimationScaleSetting, mMainExecutor, mAnimExecutor); + continue; + } } if (change.getMode() == TRANSIT_CHANGE) { 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 01ef2a6d51115..09dfabf9bfdf6 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 @@ -54,6 +54,7 @@ import com.android.internal.R; 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.DisplayController; import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.TransactionPool; @@ -113,15 +114,16 @@ public class Transitions implements RemoteCallable { private final ArrayList mActiveTransitions = new ArrayList<>(); public Transitions(@NonNull WindowOrganizer organizer, @NonNull TransactionPool pool, - @NonNull Context context, @NonNull ShellExecutor mainExecutor, - @NonNull ShellExecutor animExecutor) { + @NonNull DisplayController displayController, @NonNull Context context, + @NonNull ShellExecutor mainExecutor, @NonNull ShellExecutor animExecutor) { mOrganizer = organizer; mContext = context; 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, context, mainExecutor, animExecutor)); + mHandlers.add(new DefaultTransitionHandler(displayController, pool, context, mainExecutor, + animExecutor)); // Next lowest priority is remote transitions. mRemoteTransitionHandler = new RemoteTransitionHandler(mainExecutor); mHandlers.add(mRemoteTransitionHandler); 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 21329c75ebb09..84565e529e9ec 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 @@ -20,12 +20,18 @@ 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_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; @@ -49,6 +55,9 @@ import android.content.Context; import android.os.Binder; import android.os.IBinder; 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; @@ -66,12 +75,15 @@ import androidx.test.filters.SmallTest; import androidx.test.platform.app.InstrumentationRegistry; 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 org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; import java.util.ArrayList; @@ -98,8 +110,7 @@ public class ShellTransitionTests { @Test public void testBasicTransitionFlow() { - Transitions transitions = new Transitions(mOrganizer, mTransactionPool, mContext, - mMainExecutor, mAnimExecutor); + Transitions transitions = createTestTransitions(); transitions.replaceDefaultHandlerForTest(mDefaultHandler); IBinder transitToken = new Binder(); @@ -118,8 +129,7 @@ public class ShellTransitionTests { @Test public void testNonDefaultHandler() { - Transitions transitions = new Transitions(mOrganizer, mTransactionPool, mContext, - mMainExecutor, mAnimExecutor); + Transitions transitions = createTestTransitions(); transitions.replaceDefaultHandlerForTest(mDefaultHandler); final WindowContainerTransaction handlerWCT = new WindowContainerTransaction(); @@ -202,8 +212,7 @@ public class ShellTransitionTests { @Test public void testRequestRemoteTransition() { - Transitions transitions = new Transitions(mOrganizer, mTransactionPool, mContext, - mMainExecutor, mAnimExecutor); + Transitions transitions = createTestTransitions(); transitions.replaceDefaultHandlerForTest(mDefaultHandler); final boolean[] remoteCalled = new boolean[]{false}; @@ -297,8 +306,7 @@ public class ShellTransitionTests { @Test public void testRegisteredRemoteTransition() { - Transitions transitions = new Transitions(mOrganizer, mTransactionPool, mContext, - mMainExecutor, mAnimExecutor); + Transitions transitions = createTestTransitions(); transitions.replaceDefaultHandlerForTest(mDefaultHandler); final boolean[] remoteCalled = new boolean[]{false}; @@ -343,8 +351,7 @@ public class ShellTransitionTests { @Test public void testOneShotRemoteHandler() { - Transitions transitions = new Transitions(mOrganizer, mTransactionPool, mContext, - mMainExecutor, mAnimExecutor); + Transitions transitions = createTestTransitions(); transitions.replaceDefaultHandlerForTest(mDefaultHandler); final boolean[] remoteCalled = new boolean[]{false}; @@ -390,8 +397,7 @@ public class ShellTransitionTests { @Test public void testTransitionQueueing() { - Transitions transitions = new Transitions(mOrganizer, mTransactionPool, mContext, - mMainExecutor, mAnimExecutor); + Transitions transitions = createTestTransitions(); transitions.replaceDefaultHandlerForTest(mDefaultHandler); IBinder transitToken1 = new Binder(); @@ -431,8 +437,7 @@ public class ShellTransitionTests { @Test public void testTransitionMerging() { - Transitions transitions = new Transitions(mOrganizer, mTransactionPool, mContext, - mMainExecutor, mAnimExecutor); + Transitions transitions = createTestTransitions(); mDefaultHandler.setSimulateMerge(true); transitions.replaceDefaultHandlerForTest(mDefaultHandler); @@ -468,6 +473,63 @@ public class ShellTransitionTests { 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)); + } + class TransitionInfoBuilder { final TransitionInfo mInfo; @@ -490,11 +552,53 @@ public class ShellTransitionTests { 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 mFinishes = new ArrayList<>(); final ArrayList mMerged = new ArrayList<>(); @@ -566,4 +670,45 @@ public class ShellTransitionTests { return taskInfo; } + private DisplayController createTestDisplayController() { + IWindowManager mockWM = mock(IWindowManager.class); + final IDisplayWindowListener[] displayListener = new IDisplayWindowListener[1]; + try { + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) { + displayListener[0] = invocation.getArgument(0); + return null; + } + }).when(mockWM).registerDisplayWindowListener(any()); + } catch (RemoteException e) { + // No remote stuff happening, so this can't be hit + } + DisplayController out = new DisplayController(mContext, mockWM, mMainExecutor); + try { + displayListener[0].onDisplayAdded(DEFAULT_DISPLAY); + mMainExecutor.flushAll(); + } catch (RemoteException e) { + // Again, no remote stuff + } + return out; + } + + private Transitions createTestTransitions() { + return new Transitions(mOrganizer, mTransactionPool, createTestDisplayController(), + mContext, mMainExecutor, mAnimExecutor); + } +// +// private class TestDisplayController extends DisplayController { +// private final DisplayLayout mTestDisplayLayout; +// TestDisplayController() { +// super(mContext, mock(IWindowManager.class), mMainExecutor); +// mTestDisplayLayout = new DisplayLayout(); +// mTestDisplayLayout. +// } +// +// @Override +// DisplayLayout +// } + } diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShellBaseModule.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShellBaseModule.java index 7e733a9721ef4..8344b1bb38c5f 100644 --- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShellBaseModule.java +++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShellBaseModule.java @@ -328,9 +328,11 @@ public abstract class WMShellBaseModule { @WMSingleton @Provides static Transitions provideTransitions(ShellTaskOrganizer organizer, TransactionPool pool, - Context context, @ShellMainThread ShellExecutor mainExecutor, + DisplayController displayController, Context context, + @ShellMainThread ShellExecutor mainExecutor, @ShellAnimationThread ShellExecutor animExecutor) { - return new Transitions(organizer, pool, context, mainExecutor, animExecutor); + return new Transitions(organizer, pool, displayController, context, mainExecutor, + animExecutor); } // diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index a55fc4e9c3abe..da16de7d0226f 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -20,6 +20,8 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.ROTATION_UNDEFINED; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.INVALID_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_FLAG_IS_RECENTS; @@ -36,6 +38,7 @@ import static android.view.WindowManager.TRANSIT_TO_FRONT; import static android.view.WindowManager.TransitionFlags; import static android.view.WindowManager.TransitionType; import static android.view.WindowManager.transitTypeToString; +import static android.window.TransitionInfo.FLAG_DISPLAY_HAS_ALERT_WINDOWS; import static android.window.TransitionInfo.FLAG_IS_DISPLAY; import static android.window.TransitionInfo.FLAG_IS_VOICE_INTERACTION; import static android.window.TransitionInfo.FLAG_IS_WALLPAPER; @@ -1066,6 +1069,7 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe final ActivityManager.RunningTaskInfo tinfo = new ActivityManager.RunningTaskInfo(); task.fillTaskInfo(tinfo); change.setTaskInfo(tinfo); + change.setRotationAnimation(getTaskRotationAnimation(task)); } out.addChange(change); } @@ -1073,6 +1077,23 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe return out; } + private static int getTaskRotationAnimation(@NonNull Task task) { + final ActivityRecord top = task.getTopVisibleActivity(); + if (top == null) return ROTATION_ANIMATION_UNSPECIFIED; + final WindowState mainWin = top.findMainWindow(false); + if (mainWin == null) return ROTATION_ANIMATION_UNSPECIFIED; + int anim = mainWin.getRotationAnimationHint(); + if (anim >= 0) return anim; + anim = mainWin.getAttrs().rotationAnimation; + if (anim != ROTATION_ANIMATION_SEAMLESS) return anim; + if (mainWin != task.mDisplayContent.getDisplayPolicy().getTopFullscreenOpaqueWindow() + || !top.matchParentBounds()) { + // At the moment, we only support seamless rotation if there is only one window showing. + return ROTATION_ANIMATION_UNSPECIFIED; + } + return mainWin.getAttrs().rotationAnimation; + } + boolean getLegacyIsReady() { return mState == STATE_STARTED && mSyncId >= 0 && mSyncEngine.isReady(mSyncId); } @@ -1166,6 +1187,9 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe final DisplayContent dc = wc.asDisplayContent(); if (dc != null) { flags |= FLAG_IS_DISPLAY; + if (dc.hasAlertWindowSurfaces()) { + flags |= FLAG_DISPLAY_HAS_ALERT_WINDOWS; + } } if (isWallpaper(wc)) { flags |= FLAG_IS_WALLPAPER;