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

Commit 4c22e225 authored by Jorge Gil's avatar Jorge Gil
Browse files

Run CD transitions on separate track than recents transitions

When the recents transition is running, let transitions with window
containers on different display run in parallel. This fixes an issue
where closing a task in CD while recents is running in the primary
display keeps the task surface (and decoration) visible because recents
was still running, and additional attempts to close it would lead to a
crash trying to find the now removed task.

Flag: com.android.window.flags.parallel_cd_transitions_during_recents
Fix: 416499938
Test: with a couple of desktop tasks open in CD, swipe up on the primary
display to invoke recents transition, then without releasing the gesture
close one of the tasks in the CD - verify the task closes with its
normal animation
Test: atest WmTests:TransitionControllerTest

Change-Id: I3d129369233ba579f0ab2d0039c9704822e9f7ea
parent 00395891
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -158,6 +158,8 @@ public enum DesktopExperienceFlags {
            Flags.FLAG_ENABLE_NON_DEFAULT_DISPLAY_SPLIT),
    ENABLE_NO_WINDOW_DECORATION_FOR_DESKS(Flags::enableNoWindowDecorationForDesks, true,
        Flags.FLAG_ENABLE_NO_WINDOW_DECORATION_FOR_DESKS),
    ENABLE_PARALLEL_CD_TRANSITIONS_DURING_RECENTS(Flags::parallelCdTransitionsDuringRecents, false,
            Flags.FLAG_PARALLEL_CD_TRANSITIONS_DURING_RECENTS),
    ENABLE_PERSISTING_DISPLAY_SIZE_FOR_CONNECTED_DISPLAYS(
            Flags::enablePersistingDisplaySizeForConnectedDisplays, true,
            Flags.FLAG_ENABLE_PERSISTING_DISPLAY_SIZE_FOR_CONNECTED_DISPLAYS),
+10 −0
Original line number Diff line number Diff line
@@ -684,6 +684,16 @@ flag {
    bug: "362720309"
}

flag {
    name: "parallel_cd_transitions_during_recents"
    namespace: "lse_desktop_experience"
    description: "Runs CD transitions in a separate track when the recents transition is running"
    bug: "416499938"
    metadata {
        purpose: PURPOSE_BUGFIX
    }
}

flag {
    name: "nested_tasks_with_independent_bounds_bugfix"
    namespace: "lse_desktop_experience"
+12 −0
Original line number Diff line number Diff line
@@ -2614,6 +2614,8 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener {
        sb.append(" id=" + mSyncId);
        sb.append(" type=" + transitTypeToString(mType));
        sb.append(" flags=0x" + Integer.toHexString(mFlags));
        sb.append(" parallelCollectType=" + parallelCollectTypeToString(mParallelCollectType));
        sb.append(" recentsDisplayId=" + mRecentsDisplayId);
        if (mOverrideOptions != null) {
            sb.append(" overrideAnimOptions=" + mOverrideOptions);
        }
@@ -3917,6 +3919,16 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener {
        }
    }

    @NonNull
    private static String parallelCollectTypeToString(@ParallelType int parallelCollectType) {
        return switch (parallelCollectType) {
            case PARALLEL_TYPE_NONE -> "NONE";
            case PARALLEL_TYPE_MUTUAL -> "MUTUAL";
            case PARALLEL_TYPE_RECENTS -> "RECENTS";
            default -> "UNKNOWN(" + parallelCollectType + ")";
        };
    }

    /**
     * Represents a condition that must be met before an associated transition can be considered
     * ready.
+22 −6
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import static android.view.WindowManager.TRANSIT_CHANGE;
import static android.view.WindowManager.TRANSIT_CLOSE;
import static android.view.WindowManager.TRANSIT_FLAG_DISPLAY_LEVEL_TRANSITION;
import static android.view.WindowManager.TRANSIT_NONE;
import static android.window.DesktopExperienceFlags.ENABLE_PARALLEL_CD_TRANSITIONS_DURING_RECENTS;

import static com.android.server.wm.ActivityTaskManagerService.POWER_MODE_REASON_CHANGE_DISPLAY;

@@ -1236,7 +1237,7 @@ class TransitionController {
     * `collecting` transition. It may still ultimately block in sync-engine or become dependent
     * in {@link #getIsIndependent} later.
     */
    boolean getCanBeIndependent(Transition collecting, @Nullable Transition queued) {
    static boolean getCanBeIndependent(Transition collecting, @Nullable Transition queued) {
        // For tests
        if (queued != null && queued.mParallelCollectType == Transition.PARALLEL_TYPE_MUTUAL
                && collecting.mParallelCollectType == Transition.PARALLEL_TYPE_MUTUAL) {
@@ -1248,12 +1249,19 @@ class TransitionController {
                // Must serialize with itself.
                return false;
            }
            // allow this if `collecting` only has activities
            for (int i = 0; i < collecting.mParticipants.size(); ++i) {
                final WindowContainer wc = collecting.mParticipants.valueAt(i);
                final boolean isOnDifferentDisplay = !queued.isOnDisplay(wc.mDisplayContent);
                if (isOnDifferentDisplay
                        && ENABLE_PARALLEL_CD_TRANSITIONS_DURING_RECENTS.isTrue()) {
                    // Running in a different display, could be independent.
                    continue;
                }
                final ActivityRecord ar = wc.asActivityRecord();
                if (ar == null && wc.asWindowState() == null && wc.asWindowToken() == null) {
                    // Is task or above, so can't be independent
                final boolean isTaskOrAbove = ar == null && wc.asWindowState() == null
                        && wc.asWindowToken() == null;
                if (isTaskOrAbove) {
                    // Is task or above, so can't be independent.
                    return false;
                }
                if (ar != null && ar.isActivityTypeHomeOrRecents()) {
@@ -1306,9 +1314,17 @@ class TransitionController {
        // actually animating.
        for (int i = 0; i < other.mTargets.size(); ++i) {
            final WindowContainer wc = other.mTargets.get(i).mContainer;
            final boolean isOnDifferentDisplay = !recents.isOnDisplay(wc.mDisplayContent);
            if (isOnDifferentDisplay
                    && ENABLE_PARALLEL_CD_TRANSITIONS_DURING_RECENTS.isTrue()) {
                // Running in a different display, could be independent.
                continue;
            }
            final ActivityRecord ar = wc.asActivityRecord();
            if (ar == null && wc.asWindowState() == null && wc.asWindowToken() == null) {
                // Is task or above, so for now don't let them be independent.
            final boolean isTaskOrAbove = ar == null && wc.asWindowState() == null
                    && wc.asWindowToken() == null;
            if (isTaskOrAbove) {
                // Is task or above, so for now don't let them be independent
                return false;
            }
            if (ar != null && recents.isTransientLaunch(ar)) {
+125 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.server.wm;

import static android.view.WindowManager.TRANSIT_CLOSE;
import static android.view.WindowManager.TRANSIT_OPEN;

import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
import static com.android.server.wm.Transition.PARALLEL_TYPE_RECENTS;
import static com.android.server.wm.TransitionController.getCanBeIndependent;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;

import android.annotation.NonNull;
import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.annotations.Presubmit;
import android.view.WindowManager.TransitionType;

import androidx.test.filters.SmallTest;

import com.android.window.flags.Flags;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

/**
 * Tests for {@link TransitionController}.
 *
 * Build/Install/Run:
 * atest WmTests:TransitionControllerTest
 */
@SmallTest
@Presubmit
@RunWith(WindowTestRunner.class)
public class TransitionControllerTest extends WindowTestsBase {

    private TransitionController mController;

    @Before
    public void setUp() {
        mController = new TransitionController(mAtm);
        mController.setSyncEngine(createTestBLASTSyncEngine());
    }

    @Test
    public void testGetCanBeIndependent_recentsParallelQueued_collectingTaskInSameDisplay_false() {
        final Task task = new ActivityBuilder(mAtm).setCreateTask(true).build().getTask();
        task.mDisplayContent = mDefaultDisplay;
        final Transition collecting = createTestTransition(TRANSIT_CLOSE);
        collecting.mParticipants.add(task);
        final Transition queued = createRecentsParallelTransition(mDefaultDisplay);

        final boolean canBeIndependent = getCanBeIndependent(collecting, queued);

        assertFalse(canBeIndependent);
    }

    @Test
    @DisableFlags(Flags.FLAG_PARALLEL_CD_TRANSITIONS_DURING_RECENTS)
    public void testGetCanBeIndependent_recentsParallelQueued_collectingTaskInOtherDisplay_false() {
        final Task task = new ActivityBuilder(mAtm).setCreateTask(true).build().getTask();
        task.mDisplayContent = mock(DisplayContent.class);
        final Transition collecting = createTestTransition(TRANSIT_CLOSE);
        collecting.mParticipants.add(task);
        final Transition queued = createRecentsParallelTransition(mDefaultDisplay);

        final boolean canBeIndependent = getCanBeIndependent(collecting, queued);

        assertFalse(canBeIndependent);
    }

    @Test
    @EnableFlags(Flags.FLAG_PARALLEL_CD_TRANSITIONS_DURING_RECENTS)
    public void testGetCanBeIndependent_recentsParallelQueued_collectingTaskInOtherDisplay_true() {
        final Transition collecting = createTestTransition(TRANSIT_CLOSE);
        final Task task = new ActivityBuilder(mAtm).setCreateTask(true).build().getTask();
        task.mDisplayContent = mock(DisplayContent.class);
        collecting.mParticipants.add(task);
        final Transition queued = createRecentsParallelTransition(mDefaultDisplay);

        final boolean canBeIndependent = getCanBeIndependent(collecting, queued);

        assertTrue(canBeIndependent);
    }

    private Transition createTestTransition(@TransitionType int type) {
        final Transition transition = new Transition(type, 0 /* flags */, mController,
                mController.mSyncEngine);
        spyOn(transition.mLogger);
        doNothing().when(transition.mLogger).logOnSendAsync(any());
        return transition;
    }

    private Transition createRecentsParallelTransition(@NonNull DisplayContent display) {
        final Transition transition = createTestTransition(TRANSIT_OPEN);
        transition.mParallelCollectType = PARALLEL_TYPE_RECENTS;
        transition.startCollecting(0 /* timeoutMs */);
        final ActivityRecord recent = new ActivityBuilder(mAtm)
                .setCreateTask(true)
                .setVisible(true)
                .build();
        recent.mDisplayContent = display;
        transition.collect(recent);
        return transition;
    }
}