Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit ae941d4e authored by Riddle Hsu's avatar Riddle Hsu
Browse files

Pause transient hide activities before resuming next

This aligns the same lifecycle order as legacy transition:
When using recents or gesture navigation to switch tasks,
pause current -> resume next -> pause recents.

Previously, the order with shell transition is:
pause recent -> resume next -> pause current.
Because isInTransientHide will keep current visible until the
transition is finished.

So this change adds a method isTransientVisible to detect if
the transient (recents) activity is occluded by other task,
then report invisible for the transient-hide activities.

Bug: 290550081
Test: atest TransitionTests#testIsTransientVisible
Change-Id: I15c32fa4fa06f175e0cfd430dbc5ae806c599e52
parent 39759a9e
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -1020,7 +1020,7 @@ class TaskFragment extends WindowContainer<WindowContainer> {
        final WindowContainer<?> parent = getParent();
        final Task thisTask = asTask();
        if (thisTask != null && parent.asTask() == null
                && mTransitionController.isTransientHide(thisTask)) {
                && mTransitionController.isTransientVisible(thisTask)) {
            // Keep transient-hide root tasks visible. Non-root tasks still follow standard rule.
            return TASK_FRAGMENT_VISIBILITY_VISIBLE;
        }
+30 −0
Original line number Diff line number Diff line
@@ -407,6 +407,36 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener {
        return false;
    }

    /** Returns {@code true} if the task should keep visible if this is a transient transition. */
    boolean isTransientVisible(@NonNull Task task) {
        if (mTransientLaunches == null) return false;
        int occludedCount = 0;
        final int numTransient = mTransientLaunches.size();
        for (int i = numTransient - 1; i >= 0; --i) {
            final Task transientRoot = mTransientLaunches.keyAt(i).getRootTask();
            if (transientRoot == null) continue;
            final WindowContainer<?> rootParent = transientRoot.getParent();
            if (rootParent == null || rootParent.getTopChild() == transientRoot) continue;
            final ActivityRecord topOpaque = mController.mAtm.mTaskSupervisor
                    .mOpaqueActivityHelper.getOpaqueActivity(rootParent);
            if (transientRoot.compareTo(topOpaque.getRootTask()) < 0) {
                occludedCount++;
            }
        }
        if (occludedCount == numTransient) {
            for (int i = mTransientLaunches.size() - 1; i >= 0; --i) {
                if (mTransientLaunches.keyAt(i).isDescendantOf(task)) {
                    // Keep transient activity visible until transition finished, so it won't pause
                    // with transient-hide tasks that may delay resuming the next top.
                    return true;
                }
            }
            // Let transient-hide activities pause before transition is finished.
            return false;
        }
        return isInTransientHide(task);
    }

    boolean canApplyDim(@NonNull Task task) {
        if (mTransientLaunches == null) return true;
        final Dimmer dimmer = task.getDimmer();
+10 −3
Original line number Diff line number Diff line
@@ -468,15 +468,22 @@ class TransitionController {
        if (mCollectingTransition != null && mCollectingTransition.isInTransientHide(task)) {
            return true;
        }
        for (int i = mWaitingTransitions.size() - 1; i >= 0; --i) {
            if (mWaitingTransitions.get(i).isInTransientHide(task)) return true;
        }
        for (int i = mPlayingTransitions.size() - 1; i >= 0; --i) {
            if (mPlayingTransitions.get(i).isInTransientHide(task)) return true;
        }
        return false;
    }

    boolean isTransientVisible(@NonNull Task task) {
        if (mCollectingTransition != null && mCollectingTransition.isTransientVisible(task)) {
            return true;
        }
        for (int i = mPlayingTransitions.size() - 1; i >= 0; --i) {
            if (mPlayingTransitions.get(i).isTransientVisible(task)) return true;
        }
        return false;
    }

    boolean canApplyDim(@Nullable Task task) {
        if (task == null) {
            // Always allow non-activity window.
+1 −1
Original line number Diff line number Diff line
@@ -656,7 +656,7 @@ public class RootTaskTests extends WindowTestsBase {
                topSplitPrimary.getVisibility(null /* starting */));
        // Make primary split root transient-hide.
        spyOn(splitPrimary.mTransitionController);
        doReturn(true).when(splitPrimary.mTransitionController).isTransientHide(
        doReturn(true).when(splitPrimary.mTransitionController).isTransientVisible(
                organizer.mPrimary);
        // The split root and its top become visible.
        assertEquals(TASK_FRAGMENT_VISIBILITY_VISIBLE,
+41 −0
Original line number Diff line number Diff line
@@ -1473,6 +1473,47 @@ public class TransitionTests extends WindowTestsBase {
        assertTrue(enteringAnimReports.contains(activity2));
    }

    @Test
    public void testIsTransientVisible() {
        final ActivityRecord appB = new ActivityBuilder(mAtm).setCreateTask(true)
                .setVisible(false).build();
        final ActivityRecord recent = new ActivityBuilder(mAtm).setCreateTask(true)
                .setVisible(false).build();
        final ActivityRecord appA = new ActivityBuilder(mAtm).setCreateTask(true).build();
        final Task taskA = appA.getTask();
        final Task taskB = appB.getTask();
        final Task taskRecent = recent.getTask();
        registerTestTransitionPlayer();
        final TransitionController controller = mRootWindowContainer.mTransitionController;
        final Transition transition = createTestTransition(TRANSIT_OPEN, controller);
        controller.moveToCollecting(transition);
        transition.collect(recent);
        transition.collect(taskA);
        transition.setTransientLaunch(recent, taskA);
        taskRecent.moveToFront("move-recent-to-front");

        // During collecting and playing, the recent is on top so it is visible naturally.
        // While B needs isTransientVisible to keep visibility because it is occluded by recents.
        assertFalse(controller.isTransientVisible(taskB));
        assertTrue(controller.isTransientVisible(taskA));
        assertFalse(controller.isTransientVisible(taskRecent));
        // Switch to playing state.
        transition.onTransactionReady(transition.getSyncId(), mMockT);
        assertTrue(controller.isTransientVisible(taskA));

        // Switch to another task. For example, use gesture navigation to switch tasks.
        taskB.moveToFront("move-b-to-front");
        // The previous app (taskA) should be paused first so it loses transient visible. Because
        // visually it is taskA -> taskB, the pause -> resume order should be the same.
        assertFalse(controller.isTransientVisible(taskA));
        // Keep the recent visible so there won't be 2 activities pausing at the same time. It is
        // to avoid the latency to resume the current top, i.e. appB.
        assertTrue(controller.isTransientVisible(taskRecent));
        // The recent is paused after the transient transition is finished.
        controller.finishTransition(transition);
        assertFalse(controller.isTransientVisible(taskRecent));
    }

    @Test
    public void testNotReadyPushPop() {
        final TransitionController controller = new TestTransitionController(mAtm);