Loading core/java/android/window/DesktopExperienceFlags.java +2 −0 Original line number Diff line number Diff line Loading @@ -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), Loading core/java/android/window/flags/lse_desktop_experience.aconfig +10 −0 Original line number Diff line number Diff line Loading @@ -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" Loading services/core/java/com/android/server/wm/Transition.java +12 −0 Original line number Diff line number Diff line Loading @@ -2622,6 +2622,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); } Loading Loading @@ -3925,6 +3927,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. Loading services/core/java/com/android/server/wm/TransitionController.java +22 −6 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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) { Loading @@ -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()) { Loading Loading @@ -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)) { Loading services/tests/wmtests/src/com/android/server/wm/TransitionControllerTest.java 0 → 100644 +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; } } Loading
core/java/android/window/DesktopExperienceFlags.java +2 −0 Original line number Diff line number Diff line Loading @@ -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), Loading
core/java/android/window/flags/lse_desktop_experience.aconfig +10 −0 Original line number Diff line number Diff line Loading @@ -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" Loading
services/core/java/com/android/server/wm/Transition.java +12 −0 Original line number Diff line number Diff line Loading @@ -2622,6 +2622,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); } Loading Loading @@ -3925,6 +3927,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. Loading
services/core/java/com/android/server/wm/TransitionController.java +22 −6 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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) { Loading @@ -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()) { Loading Loading @@ -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)) { Loading
services/tests/wmtests/src/com/android/server/wm/TransitionControllerTest.java 0 → 100644 +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; } }