From 211e1dd825811b6a0359f1eca066157ed18262c9 Mon Sep 17 00:00:00 2001 From: Riddle Hsu Date: Tue, 14 Mar 2023 18:24:27 +0800 Subject: [PATCH] Keep visible requested for transient hide activities If a process doesn't have other active component and its activity is not visible requested, it will become background state and may lose the access of various system services. The invisible requested will also cause other animation issues such as incorrect dim layer, window surface position. Because isGoneForLayout will return false by the visible state. By checking isTransientHide in TaskFragment#getVisibility, the task can keep visible even if it is occluded by the transient launch target. And then add FLAG_ABOVE_TRANSIENT_LAUNCH to the tree of transient-hide task so the ChangeInfo will always report as TRANSIT_TO_BACK. When the transient transition is finished, if the transient launch target is visible, then update the visibility to make the transient-hide activities to be invisible requested and add the activities to stopping list. So when checking the visibility participants, it can continue to commit the state. Also add a condition for mAvoidMoveToFront for the case that the moving-to-back task may be moved to front again after transient launch if the app uses a trampoline activity. - Previously during the transient transition, the trampoline launch will be blocked by BAL because invisible. - In legacy transition, the task is already on top (recent is launch-behind) and finish-recents-animation will move home to front. Now it will launch the new activity into its task without moving the task to front, e.g. during the swipe-return-to-home animation, the going-to-invisible activity launches next activity, it will still stay at home. And then launch the app again, the latest new activity on its task can be resumed directly. Fix: 269577449 Fix: 271282193 Test: atest TransitionTests#testTransientLaunch Change-Id: I887f63a4985804a59e6602817dce6aec0e6f2c69 --- .../android/server/wm/ActivityStarter.java | 7 +- .../com/android/server/wm/TaskFragment.java | 4 + .../com/android/server/wm/Transition.java | 78 +++++++++++++------ .../server/wm/TransitionController.java | 4 +- .../android/server/wm/TransitionTests.java | 16 +++- 5 files changed, 81 insertions(+), 28 deletions(-) diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index 211c230b4ece8..ce29564d0b02e 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -1680,6 +1680,11 @@ class ActivityStarter { targetTask.removeImmediately("bulky-task"); return START_ABORTED; } + // When running transient transition, the transient launch target should keep on top. + // So disallow the transient hide activity to move itself to front, e.g. trampoline. + if (!mAvoidMoveToFront && r.mTransitionController.isTransientHide(targetTask)) { + mAvoidMoveToFront = true; + } mPriorAboveTask = TaskDisplayArea.getRootTaskAbove(targetTask.getRootTask()); } @@ -1796,7 +1801,7 @@ class ActivityStarter { // root-task to the will not update the focused root-task. If starting the new // activity now allows the task root-task to be focusable, then ensure that we // now update the focused root-task accordingly. - if (mTargetRootTask.isTopActivityFocusable() + if (!mAvoidMoveToFront && mTargetRootTask.isTopActivityFocusable() && !mRootWindowContainer.isTopDisplayFocusedRootTask(mTargetRootTask)) { mTargetRootTask.moveToFront("startActivityInner"); } diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index 2ddb307ea430b..7c57dc17e8024 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -1012,6 +1012,10 @@ class TaskFragment extends WindowContainer { if (isTopActivityLaunchedBehind()) { return TASK_FRAGMENT_VISIBILITY_VISIBLE; } + final Task thisTask = asTask(); + if (thisTask != null && mTransitionController.isTransientHide(thisTask)) { + return TASK_FRAGMENT_VISIBILITY_VISIBLE; + } boolean gotTranslucentFullscreen = false; boolean gotTranslucentAdjacent = false; diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index 89feb0cc5e4a9..97f48b7d03097 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -194,6 +194,13 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { */ private ArrayMap mTransientLaunches = null; + /** + * The tasks that may be occluded by the transient activity. Assume the task stack is + * [Home, A(opaque), B(opaque), C(translucent)] (bottom to top), then A is the restore-below + * task, and [B, C] are the transient-hide tasks. + */ + private ArrayList mTransientHideTasks; + /** Custom activity-level animation options and callbacks. */ private TransitionInfo.AnimationOptions mOverrideOptions; private IRemoteCallback mClientAnimationStartCallback = null; @@ -265,35 +272,51 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { void setTransientLaunch(@NonNull ActivityRecord activity, @Nullable Task restoreBelow) { if (mTransientLaunches == null) { mTransientLaunches = new ArrayMap<>(); + mTransientHideTasks = new ArrayList<>(); } mTransientLaunches.put(activity, restoreBelow); setTransientLaunchToChanges(activity); if (restoreBelow != null) { - final ChangeInfo info = mChanges.get(restoreBelow); - if (info != null) { - info.mFlags |= ChangeInfo.FLAG_ABOVE_TRANSIENT_LAUNCH; + // Collect all visible activities which can be occluded by the transient activity to + // make sure they are in the participants so their visibilities can be updated when + // finishing transition. + ((WindowContainer) restoreBelow.getParent()).forAllTasks(t -> { + if (t.isVisibleRequested() && !t.isAlwaysOnTop() + && !t.getWindowConfiguration().tasksAreFloating()) { + if (t.isRootTask()) { + mTransientHideTasks.add(t); + } + if (t.isLeafTask()) { + t.forAllActivities(r -> { + if (r.isVisibleRequested()) { + collect(r); + } + }); + } + } + return t == restoreBelow; + }); + // Add FLAG_ABOVE_TRANSIENT_LAUNCH to the tree of transient-hide tasks, + // so ChangeInfo#hasChanged() can return true to report the transition info. + for (int i = mChanges.size() - 1; i >= 0; --i) { + final WindowContainer wc = mChanges.keyAt(i); + if (wc.asTaskFragment() == null && wc.asActivityRecord() == null) continue; + if (isInTransientHide(wc)) { + mChanges.valueAt(i).mFlags |= ChangeInfo.FLAG_ABOVE_TRANSIENT_LAUNCH; + } } } ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Transition %d: Set %s as " + "transient-launch", mSyncId, activity); } - boolean isTransientHide(@NonNull Task task) { - if (mTransientLaunches == null) return false; - for (int i = 0; i < mTransientLaunches.size(); ++i) { - if (mTransientLaunches.valueAt(i) == task) { - return true; - } - } - return false; - } - /** @return whether `wc` is a descendent of a transient-hide window. */ boolean isInTransientHide(@NonNull WindowContainer wc) { - if (mTransientLaunches == null) return false; - for (int i = 0; i < mTransientLaunches.size(); ++i) { - if (wc.isDescendantOf(mTransientLaunches.valueAt(i))) { + if (mTransientHideTasks == null) return false; + for (int i = mTransientHideTasks.size() - 1; i >= 0; --i) { + final Task task = mTransientHideTasks.get(i); + if (wc == task || wc.isDescendantOf(task)) { return true; } } @@ -816,6 +839,14 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { } mController.mFinishingTransition = this; + if (mTransientHideTasks != null && !mTransientHideTasks.isEmpty()) { + // The transient hide tasks could be occluded now, e.g. returning to home. So trigger + // the update to make the activities in the tasks invisible-requested, then the next + // step can continue to commit the visibility. + mController.mAtm.mRootWindowContainer.ensureActivitiesVisible(null /* starting */, + 0 /* configChanges */, true /* preserveWindows */); + } + boolean hasParticipatedDisplay = false; boolean hasVisibleTransientLaunch = false; // Commit all going-invisible containers @@ -1175,14 +1206,13 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { // Record windowtokens (activity/wallpaper) that are expected to be visible after the // transition animation. This will be used in finishTransition to prevent prematurely - // committing visibility. - for (int i = mParticipants.size() - 1; i >= 0; --i) { - final WindowContainer wc = mParticipants.valueAt(i); - if (wc.asWindowToken() == null || !wc.isVisibleRequested()) continue; - // don't include transient launches, though, since those are only temporarily visible. - if (mTransientLaunches != null && wc.asActivityRecord() != null - && mTransientLaunches.containsKey(wc.asActivityRecord())) continue; - mVisibleAtTransitionEndTokens.add(wc.asWindowToken()); + // committing visibility. Skip transient launches since those are only temporarily visible. + if (mTransientLaunches == null) { + for (int i = mParticipants.size() - 1; i >= 0; --i) { + final WindowContainer wc = mParticipants.valueAt(i); + if (wc.asWindowToken() == null || !wc.isVisibleRequested()) continue; + mVisibleAtTransitionEndTokens.add(wc.asWindowToken()); + } } // Take task snapshots before the animation so that we can capture IME before it gets diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java index 59264e9b6ea00..86bb6b58d14c5 100644 --- a/services/core/java/com/android/server/wm/TransitionController.java +++ b/services/core/java/com/android/server/wm/TransitionController.java @@ -366,11 +366,11 @@ class TransitionController { } boolean isTransientHide(@NonNull Task task) { - if (mCollectingTransition != null && mCollectingTransition.isTransientHide(task)) { + if (mCollectingTransition != null && mCollectingTransition.isInTransientHide(task)) { return true; } for (int i = mPlayingTransitions.size() - 1; i >= 0; --i) { - if (mPlayingTransitions.get(i).isTransientHide(task)) return true; + if (mPlayingTransitions.get(i).isInTransientHide(task)) return true; } return false; } 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 3cbe2de137027..616d528c67fae 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java @@ -40,6 +40,7 @@ import static android.window.TransitionInfo.FLAG_SHOW_WALLPAPER; import static android.window.TransitionInfo.FLAG_TRANSLUCENT; import static android.window.TransitionInfo.isIndependent; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doCallRealMethod; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; @@ -54,6 +55,7 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assume.assumeFalse; import static org.junit.Assume.assumeTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; @@ -1400,7 +1402,13 @@ public class TransitionTests extends WindowTestsBase { closeTransition.collectExistenceChange(activity1); closeTransition.collectExistenceChange(task2); closeTransition.collectExistenceChange(activity2); - closeTransition.setTransientLaunch(activity2, null /* restoreBelow */); + closeTransition.setTransientLaunch(activity2, task1); + final Transition.ChangeInfo task1ChangeInfo = closeTransition.mChanges.get(task1); + assertNotNull(task1ChangeInfo); + assertTrue(task1ChangeInfo.hasChanged()); + final Transition.ChangeInfo activity1ChangeInfo = closeTransition.mChanges.get(activity1); + assertNotNull(activity1ChangeInfo); + assertTrue(activity1ChangeInfo.hasChanged()); activity1.setVisibleRequested(false); activity2.setVisibleRequested(true); @@ -1416,6 +1424,8 @@ public class TransitionTests extends WindowTestsBase { verify(snapshotController, times(0)).recordSnapshot(eq(task1), eq(false)); enteringAnimReports.clear(); + doCallRealMethod().when(mWm.mRoot).ensureActivitiesVisible(any(), + anyInt(), anyBoolean(), anyBoolean()); final boolean[] wasInFinishingTransition = { false }; controller.registerLegacyListener(new WindowManagerInternal.AppTransitionListener() { @Override @@ -1430,7 +1440,11 @@ public class TransitionTests extends WindowTestsBase { assertTrue(wasInFinishingTransition[0]); assertNull(controller.mFinishingTransition); + assertTrue(activity2.isVisible()); assertEquals(ActivityTaskManagerService.APP_SWITCH_DISALLOW, mAtm.getBalAppSwitchesState()); + // Because task1 is occluded by task2, finishTransition should make activity1 invisible. + assertFalse(activity1.isVisibleRequested()); + assertFalse(activity1.isVisible()); assertFalse(activity1.app.hasActivityInVisibleTask()); verify(snapshotController, times(1)).recordSnapshot(eq(task1), eq(false));