diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java index 4c482460543ac..57285f8343c0b 100644 --- a/core/java/android/window/TransitionInfo.java +++ b/core/java/android/window/TransitionInfo.java @@ -149,8 +149,11 @@ public final class TransitionInfo implements Parcelable { /** The task is launching behind home. */ public static final int FLAG_TASK_LAUNCHING_BEHIND = 1 << 19; + /** The task became the top-most task even if it didn't change visibility. */ + public static final int FLAG_MOVED_TO_TOP = 1 << 20; + /** 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 << 20; + public static final int FLAG_FIRST_CUSTOM = 1 << 21; /** The change belongs to a window that won't contain activities. */ public static final int FLAGS_IS_NON_APP_WINDOW = @@ -179,6 +182,7 @@ public final class TransitionInfo implements Parcelable { FLAG_BACK_GESTURE_ANIMATED, FLAG_NO_ANIMATION, FLAG_TASK_LAUNCHING_BEHIND, + FLAG_MOVED_TO_TOP, FLAG_FIRST_CUSTOM }) public @interface ChangeFlags {} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java index 5c64177ae8352..c8d6a5e8e00b2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java @@ -456,7 +456,10 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { cancel(mWillFinishToHome); return; } - hasChangingApp = true; + // Don't consider order-only changes as changing apps. + if (!TransitionUtil.isOrderOnly(change)) { + hasChangingApp = true; + } } } if (hasChangingApp && foundRecentsClosing) { @@ -484,13 +487,14 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { } boolean didMergeThings = false; if (closingTasks != null) { - // Cancelling a task-switch. Move the tasks back to mPausing from mOpening + // Potentially cancelling a task-switch. Move the tasks back to mPausing if they + // are in mOpening. for (int i = 0; i < closingTasks.size(); ++i) { final TransitionInfo.Change change = closingTasks.get(i); int openingIdx = TaskState.indexOf(mOpeningTasks, change); if (openingIdx < 0) { - Slog.e(TAG, "Back to existing recents animation from an unrecognized " - + "task: " + change.getTaskInfo().taskId); + Slog.w(TAG, "Closing a task that wasn't opening, this may be split or" + + " something unexpected: " + change.getTaskInfo().taskId); continue; } mPausingTasks.add(mOpeningTasks.remove(openingIdx)); 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 63c7969291a0b..3dd10a098310c 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 @@ -301,8 +301,8 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { return true; } - // check if no-animation and skip animation if so. - if (Transitions.isAllNoAnimation(info)) { + // Early check if the transition doesn't warrant an animation. + if (Transitions.isAllNoAnimation(info) || Transitions.isAllOrderOnly(info)) { startTransaction.apply(); finishTransaction.apply(); finishCallback.onTransitionFinished(null /* wct */, null /* wctCB */); 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 4284993a54482..ffd9d716ec039 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 @@ -516,6 +516,16 @@ public class Transitions implements RemoteCallable { return hasNoAnimation; } + /** + * Check if all changes in this transition are only ordering changes. If so, we won't animate. + */ + static boolean isAllOrderOnly(TransitionInfo info) { + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + if (!TransitionUtil.isOrderOnly(info.getChanges().get(i))) return false; + } + return true; + } + @VisibleForTesting void onTransitionReady(@NonNull IBinder transitionToken, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t, @NonNull SurfaceControl.Transaction finishT) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/util/TransitionUtil.java b/libs/WindowManager/Shell/src/com/android/wm/shell/util/TransitionUtil.java index 7595c9617709f..ce102917352d9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/util/TransitionUtil.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/util/TransitionUtil.java @@ -31,6 +31,7 @@ import static android.view.WindowManager.TRANSIT_TO_FRONT; import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY; import static android.window.TransitionInfo.FLAG_IS_DISPLAY; import static android.window.TransitionInfo.FLAG_IS_WALLPAPER; +import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP; import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT; import static com.android.wm.shell.common.split.SplitScreenConstants.FLAG_IS_DIVIDER_BAR; @@ -90,6 +91,15 @@ public class TransitionUtil { && !change.hasFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY); } + /** Returns `true` if `change` is only re-ordering. */ + public static boolean isOrderOnly(TransitionInfo.Change change) { + return change.getMode() == TRANSIT_CHANGE + && (change.getFlags() & FLAG_MOVED_TO_TOP) != 0 + && change.getStartAbsBounds().equals(change.getEndAbsBounds()) + && (change.getLastParent() == null + || change.getLastParent().equals(change.getParent())); + } + /** * Filter that selects leaf-tasks only. THIS IS ORDER-DEPENDENT! For it to work properly, you * MUST call `test` in the same order that the changes appear in the TransitionInfo. diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java index 44f9d43f5470a..f094102ad88f9 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java @@ -63,6 +63,7 @@ public class RemoteAnimationTargetCompat { final ArrayList out = new ArrayList<>(); for (int i = 0; i < info.getChanges().size(); i++) { TransitionInfo.Change change = info.getChanges().get(i); + if (TransitionUtil.isOrderOnly(change)) continue; if (filter.test(change)) { out.add(TransitionUtil.newTarget( change, info.getChanges().size() - i, info, t, leashMap)); diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index b7e2265e3a16f..db44532970017 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -4687,6 +4687,7 @@ class Task extends TaskFragment { if (!isAttached()) { return; } + mTransitionController.collect(this); final TaskDisplayArea taskDisplayArea = getDisplayArea(); diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index c6ba60026cdf4..21c5e79d6dd54 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -50,6 +50,7 @@ import static android.window.TransitionInfo.FLAG_IS_DISPLAY; import static android.window.TransitionInfo.FLAG_IS_INPUT_METHOD; import static android.window.TransitionInfo.FLAG_IS_VOICE_INTERACTION; import static android.window.TransitionInfo.FLAG_IS_WALLPAPER; +import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP; import static android.window.TransitionInfo.FLAG_NO_ANIMATION; import static android.window.TransitionInfo.FLAG_OCCLUDES_KEYGUARD; import static android.window.TransitionInfo.FLAG_SHOW_WALLPAPER; @@ -182,6 +183,12 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { /** The displays that this transition is running on. */ private final ArrayList mTargetDisplays = new ArrayList<>(); + /** + * The (non alwaysOnTop) tasks which were on-top of their display before the transition. If + * tasks are nested, all the tasks that are parents of the on-top task are also included. + */ + private final ArrayList mOnTopTasksStart = new ArrayList<>(); + /** * Set of participating windowtokens (activity/wallpaper) which are visible at the end of * the transition animation. @@ -515,6 +522,7 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { mParticipants.add(wc); if (wc.getDisplayContent() != null && !mTargetDisplays.contains(wc.getDisplayContent())) { mTargetDisplays.add(wc.getDisplayContent()); + addOnTopTasks(wc.getDisplayContent(), mOnTopTasksStart); } if (info.mShowWallpaper) { // Collect the wallpaper token (for isWallpaper(wc)) so it is part of the sync set. @@ -526,6 +534,27 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { } } + /** Adds the top non-alwaysOnTop tasks within `task` to `out`. */ + private static void addOnTopTasks(Task task, ArrayList out) { + for (int i = task.getChildCount() - 1; i >= 0; --i) { + final Task child = task.getChildAt(i).asTask(); + if (child == null) return; + if (child.getWindowConfiguration().isAlwaysOnTop()) continue; + out.add(child); + addOnTopTasks(child, out); + break; + } + } + + /** Get the top non-alwaysOnTop leaf task on the display `dc`. */ + private static void addOnTopTasks(DisplayContent dc, ArrayList out) { + final Task topNotAlwaysOnTop = dc.getRootTask( + t -> !t.getWindowConfiguration().isAlwaysOnTop()); + if (topNotAlwaysOnTop == null) return; + out.add(topNotAlwaysOnTop); + addOnTopTasks(topNotAlwaysOnTop, out); + } + /** * Records wc as changing its state of existence during this transition. For example, a new * task is considered an existence change while moving a task to front is not. wc is added @@ -1000,11 +1029,13 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { InsetsControlTarget prevImeTarget = dc.getImeTarget( DisplayContent.IME_TARGET_CONTROL); InsetsControlTarget newImeTarget = null; + TaskDisplayArea transientTDA = null; // Transient-launch activities cannot be IME target (WindowState#canBeImeTarget), // so re-compute in case the IME target is changed after transition. for (int t = 0; t < mTransientLaunches.size(); ++t) { if (mTransientLaunches.keyAt(t).getDisplayContent() == dc) { newImeTarget = dc.computeImeTarget(true /* updateImeTarget */); + transientTDA = mTransientLaunches.keyAt(i).getTaskDisplayArea(); break; } } @@ -1015,6 +1046,12 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { InputMethodManagerInternal.get().updateImeWindowStatus( false /* disableImeIcon */); } + // An uncommitted transient launch can leave incomplete lifecycles if visibilities + // didn't change (eg. re-ordering with translucent tasks will leave launcher + // in RESUMED state), so force an update here. + if (!hasVisibleTransientLaunch && transientTDA != null) { + transientTDA.pauseBackTasks(null /* resuming */); + } } dc.removeImeSurfaceImmediately(); dc.handleCompleteDeferredRemoval(); @@ -1140,6 +1177,9 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { } // Check whether the participants were animated from back navigation. mController.mAtm.mBackNavigationController.onTransactionReady(this); + + collectOrderChanges(); + // Resolve the animating targets from the participants. mTargets = calculateTargets(mParticipants, mChanges); final TransitionInfo info = calculateTransitionInfo(mType, mFlags, mTargets, transaction); @@ -1291,6 +1331,27 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { info.releaseAnimSurfaces(); } + /** Collect tasks which moved-to-top but didn't change otherwise. */ + @VisibleForTesting + void collectOrderChanges() { + if (mOnTopTasksStart.isEmpty()) return; + final ArrayList onTopTasksEnd = new ArrayList<>(); + for (int i = 0; i < mTargetDisplays.size(); ++i) { + addOnTopTasks(mTargetDisplays.get(i), onTopTasksEnd); + } + for (int i = 0; i < onTopTasksEnd.size(); ++i) { + final Task task = onTopTasksEnd.get(i); + if (mOnTopTasksStart.contains(task)) continue; + mParticipants.add(task); + int changeIdx = mChanges.indexOfKey(task); + if (changeIdx < 0) { + mChanges.put(task, new ChangeInfo(task)); + changeIdx = mChanges.indexOfKey(task); + } + mChanges.valueAt(changeIdx).mFlags |= ChangeInfo.FLAG_CHANGE_MOVED_TO_TOP; + } + } + private void postCleanupOnFailure() { mController.mAtm.mH.post(() -> { synchronized (mController.mAtm.mGlobalLock) { @@ -2246,13 +2307,17 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { */ private static final int FLAG_CHANGE_YES_ANIMATION = 0x10; + /** Whether this change's container moved to the top. */ + private static final int FLAG_CHANGE_MOVED_TO_TOP = 0x20; + @IntDef(prefix = { "FLAG_" }, value = { FLAG_NONE, FLAG_SEAMLESS_ROTATION, FLAG_TRANSIENT_LAUNCH, FLAG_ABOVE_TRANSIENT_LAUNCH, FLAG_CHANGE_NO_ANIMATION, - FLAG_CHANGE_YES_ANIMATION + FLAG_CHANGE_YES_ANIMATION, + FLAG_CHANGE_MOVED_TO_TOP }) @Retention(RetentionPolicy.SOURCE) @interface Flag {} @@ -2283,7 +2348,7 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { int mDisplayId = -1; @ActivityInfo.Config int mKnownConfigChanges; - /** These are just extra info. They aren't used for change-detection. */ + /** Extra information about this change. */ @Flag int mFlags = FLAG_NONE; /** Snapshot surface and luma, if relevant. */ @@ -2335,7 +2400,8 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { || (mWindowingMode != 0 && mContainer.getWindowingMode() != mWindowingMode) || !mContainer.getBounds().equals(mAbsoluteBounds) || mRotation != mContainer.getWindowConfiguration().getRotation() - || mDisplayId != getDisplayId(mContainer); + || mDisplayId != getDisplayId(mContainer) + || (mFlags & ChangeInfo.FLAG_CHANGE_MOVED_TO_TOP) != 0; } @TransitionInfo.TransitionMode @@ -2436,6 +2502,9 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { && (mFlags & FLAG_CHANGE_YES_ANIMATION) == 0) { flags |= FLAG_NO_ANIMATION; } + if ((mFlags & FLAG_CHANGE_MOVED_TO_TOP) != 0) { + flags |= FLAG_MOVED_TO_TOP; + } return flags; } diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java index d7bf4b0e02b2f..90506d4f86513 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java @@ -1885,6 +1885,39 @@ public class TransitionTests extends WindowTestsBase { assertEquals(newParent.getDisplayArea(), change.mCommonAncestor); } + @Test + public void testMoveToTopWhileVisible() { + final Transition transition = createTestTransition(TRANSIT_OPEN); + final ArrayMap changes = transition.mChanges; + final ArraySet participants = transition.mParticipants; + + // Start with taskB on top and taskA on bottom but both visible. + final Task rootTaskA = createTask(mDisplayContent); + final Task leafTaskA = createTaskInRootTask(rootTaskA, 0 /* userId */); + final Task taskB = createTask(mDisplayContent); + leafTaskA.setVisibleRequested(true); + taskB.setVisibleRequested(true); + // manually collect since this is a test transition and not known by transitionController. + transition.collect(leafTaskA); + rootTaskA.moveToFront("test", leafTaskA); + + // All the tasks were already visible, so there shouldn't be any changes + ArrayList targets = Transition.calculateTargets( + participants, changes); + assertTrue(targets.isEmpty()); + + // After collecting order changes, it should recognize that a task moved to top. + transition.collectOrderChanges(); + targets = Transition.calculateTargets(participants, changes); + assertEquals(1, targets.size()); + + // Make sure the flag is set + final TransitionInfo info = Transition.calculateTransitionInfo( + transition.mType, 0 /* flags */, targets, mMockT); + assertTrue((info.getChanges().get(0).getFlags() & TransitionInfo.FLAG_MOVED_TO_TOP) != 0); + assertEquals(TRANSIT_CHANGE, info.getChanges().get(0).getMode()); + } + private static void makeTaskOrganized(Task... tasks) { final ITaskOrganizer organizer = mock(ITaskOrganizer.class); for (Task t : tasks) {