Loading libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +2 −1 Original line number Diff line number Diff line Loading @@ -512,7 +512,8 @@ public class BubbleController implements ConfigurationChangeListener, } }, mMainHandler); mTransitions.registerObserver(new BubblesTransitionObserver(this, mBubbleData)); mTransitions.registerObserver(new BubblesTransitionObserver(this, mBubbleData, mBubbleTransitions.mTaskViewTransitions, mSplitScreenController)); mTaskStackListener.addListener( new BubbleTaskStackListener(this, mBubbleData, mSplitScreenController)); Loading libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesTransitionObserver.java +55 −1 Original line number Diff line number Diff line Loading @@ -21,17 +21,28 @@ import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BUBBLES_NOISY; import android.app.ActivityManager; import android.os.Binder; import android.os.IBinder; import android.view.SurfaceControl; import android.window.TransitionInfo; import android.window.WindowContainerTransaction; import androidx.annotation.NonNull; import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.bubbles.util.BubbleUtilsKt; import com.android.wm.shell.shared.TransitionUtil; import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper; import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.taskview.TaskViewTaskController; import com.android.wm.shell.taskview.TaskViewTransitions; import com.android.wm.shell.transition.Transitions; import dagger.Lazy; import java.util.Optional; /** * Observer used to identify tasks that are opening or moving to front. If a bubble activity is * currently opened when this happens, we'll collapse the bubbles. Loading @@ -42,18 +53,30 @@ public class BubblesTransitionObserver implements Transitions.TransitionObserver private final BubbleController mBubbleController; @NonNull private final BubbleData mBubbleData; private final TaskViewTransitions mTaskViewTransitions; private final Lazy<Optional<SplitScreenController>> mSplitScreenController; public BubblesTransitionObserver(@NonNull BubbleController controller, @NonNull BubbleData bubbleData) { @NonNull BubbleData bubbleData, TaskViewTransitions taskViewTransitions, Lazy<Optional<SplitScreenController>> splitScreenController) { mBubbleController = controller; mBubbleData = bubbleData; mTaskViewTransitions = taskViewTransitions; mSplitScreenController = splitScreenController; } @Override public void onTransitionReady(@NonNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction) { collapseBubbleIfNeeded(info); if (BubbleAnythingFlagHelper.enableCreateAnyBubble()) { removeBubbleIfLaunchingToSplit(info); } } private void collapseBubbleIfNeeded(@NonNull TransitionInfo info) { // --- Pre-conditions (Loop-invariant checks) --- // If bubbles aren't expanded, are animating, or no bubble is selected, // we don't need to process any transitions for collapsing. Loading Loading @@ -106,6 +129,37 @@ public class BubblesTransitionObserver implements Transitions.TransitionObserver } } private void removeBubbleIfLaunchingToSplit(@NonNull TransitionInfo info) { if (mSplitScreenController.get().isEmpty()) return; SplitScreenController splitScreenController = mSplitScreenController.get().get(); for (TransitionInfo.Change change : info.getChanges()) { ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); if (taskInfo == null) continue; Bubble bubble = mBubbleData.getBubbleInStackWithTaskId(taskInfo.taskId); if (bubble == null) continue; if (!splitScreenController.isTaskRootOrStageRoot(taskInfo.parentTaskId)) continue; // There is a bubble task that is moving to split screen ProtoLog.d(WM_SHELL_BUBBLES_NOISY, "TransitionObserver.onTransitionReady(): removing bubble for task launching " + "into split taskId=%d", taskInfo.taskId); TaskViewTaskController taskViewTaskController = bubble.getTaskView().getController(); ShellTaskOrganizer taskOrganizer = taskViewTaskController.getTaskOrganizer(); WindowContainerTransaction wct = BubbleUtilsKt.getExitBubbleTransaction(taskInfo.token, bubble.getTaskView().getCaptionInsetsOwner()); // Notify the task removal, but block all TaskViewTransitions during removal so we can // clear them without triggering final IBinder gate = new Binder(); mTaskViewTransitions.enqueueExternal(taskViewTaskController, () -> gate); taskOrganizer.applyTransaction(wct); taskViewTaskController.notifyTaskRemovalStarted(taskInfo); mTaskViewTransitions.removePendingTransitions(taskViewTaskController); mTaskViewTransitions.onExternalDone(gate); } } @Override public void onTransitionStarting(@NonNull IBinder transition) { Loading libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +4 −0 Original line number Diff line number Diff line Loading @@ -2036,6 +2036,10 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // TODO (b/336477473): Disallow enter PiP when launching a task in split by default; // this might have to be changed as more split-to-pip cujs are defined. options.setDisallowEnterPictureInPictureWhileLaunching(true); // Set an empty rect as the requested launch bounds. This ensures that if an existing // task is reused, and it has bounds set, they are cleared. options.setLaunchBounds(new Rect()); opts.putAll(options.toBundle()); } Loading libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java +9 −0 Original line number Diff line number Diff line Loading @@ -283,6 +283,15 @@ public class TaskViewTransitions implements Transitions.TransitionHandler, TaskV return !mPending.isEmpty(); } /** Removes all pending transitions for the given {@code taskView}. */ public void removePendingTransitions(TaskViewTaskController taskView) { for (int i = mPending.size() - 1; i >= 0; --i) { if (mPending.get(i).mTaskView != taskView) continue; if (mPending.get(i).mExternalTransition != null) continue; mPending.remove(i); } } @Override public WindowContainerTransaction handleRequest(@NonNull IBinder transition, @Nullable TransitionRequestInfo request) { Loading libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblesTransitionObserverTest.kt +70 −9 Original line number Diff line number Diff line Loading @@ -19,6 +19,7 @@ package com.android.wm.shell.bubbles import android.app.ActivityManager import android.app.ActivityTaskManager.INVALID_TASK_ID import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW import android.platform.test.annotations.EnableFlags import android.view.WindowManager.TRANSIT_CHANGE import android.view.WindowManager.TRANSIT_CLOSE Loading @@ -27,17 +28,27 @@ import android.view.WindowManager.TRANSIT_TO_BACK import android.view.WindowManager.TRANSIT_TO_FRONT import android.view.WindowManager.TransitionType import android.window.TransitionInfo import android.window.WindowContainerToken import android.window.WindowContainerTransaction import androidx.test.filters.SmallTest import com.android.wm.shell.Flags.FLAG_ENABLE_CREATE_ANY_BUBBLE import com.android.wm.shell.MockToken import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.ShellTestCase import com.android.wm.shell.bubbles.util.verifyExitBubbleTransaction import com.android.wm.shell.splitscreen.SplitScreenController import com.android.wm.shell.taskview.TaskView import com.android.wm.shell.taskview.TaskViewTaskController import com.android.wm.shell.taskview.TaskViewTransitions import com.android.wm.shell.transition.TransitionInfoBuilder import com.google.testing.junit.testparameterinjector.TestParameter import com.google.testing.junit.testparameterinjector.TestParameterInjector import java.util.Optional import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import org.mockito.kotlin.stub Loading @@ -45,8 +56,7 @@ import org.mockito.kotlin.stub /** * Unit tests of [BubblesTransitionObserver]. * * Build/Install/Run: * atest WMShellUnitTests:BubblesTransitionObserverTest * Build/Install/Run: atest WMShellUnitTests:BubblesTransitionObserverTest */ @SmallTest @RunWith(TestParameterInjector::class) Loading @@ -62,7 +72,17 @@ class BubblesTransitionObserverTest : ShellTestCase() { private val bubbleController = mock<BubbleController> { on { isStackAnimating } doReturn false } private val transitionObserver = BubblesTransitionObserver(bubbleController, bubbleData) private val taskViewTransitions = mock<TaskViewTransitions>() private val splitScreenController = mock<SplitScreenController> { on { isTaskRootOrStageRoot(anyInt()) } doReturn false } private val transitionObserver = BubblesTransitionObserver( bubbleController, bubbleData, taskViewTransitions, { Optional.of(splitScreenController) }, ) @Test fun testOnTransitionReady_openWithTaskTransition_collapsesStack() { Loading Loading @@ -179,6 +199,46 @@ class BubblesTransitionObserverTest : ShellTestCase() { verify(bubbleData, never()).setExpanded(false) } @EnableFlags(FLAG_ENABLE_CREATE_ANY_BUBBLE) @Test fun testOnTransitionReady_bubbleMovingToSplit_removeBubble() { val taskOrganizer = mock<ShellTaskOrganizer>() val taskViewTaskController = mock<TaskViewTaskController> { on { this.taskOrganizer } doReturn taskOrganizer } val taskView = mock<TaskView> { on { controller } doReturn taskViewTaskController } bubble.stub { on { this.taskView } doReturn taskView } bubbleData.stub { on { getBubbleInStackWithTaskId(bubble.taskId) } doReturn bubble } splitScreenController.stub { on { isTaskRootOrStageRoot(10) } doReturn true } val taskInfo = createTaskInfo(taskId = 1).apply { this.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_MULTI_WINDOW this.parentTaskId = 10 } val info = createTaskTransition(TRANSIT_TO_FRONT, taskInfo) transitionObserver.onTransitionReady(mock(), info, mock(), mock()) // Check that we remove the taskView verify(taskViewTaskController).notifyTaskRemovalStarted(taskInfo) val wctCaptor = argumentCaptor<WindowContainerTransaction>() // And clean up bubble specific overrides on a task verify(taskOrganizer).applyTransaction(wctCaptor.capture()) verifyExitBubbleTransaction( wctCaptor.firstValue, taskInfo.token.asBinder(), /* captionInsetsOwner */ null, ) } // Transits that aren't opening. enum class TransitNotOpeningTestCase( @TransitionType private val changeType: Int, Loading @@ -201,9 +261,10 @@ class BubblesTransitionObserverTest : ShellTestCase() { taskInfo: ActivityManager.RunningTaskInfo?, ) = TransitionInfoBuilder(TRANSIT_OPEN).addChange(changeType, taskInfo).build() private fun createTaskInfo(taskId: Int) = ActivityManager.RunningTaskInfo().apply { private fun createTaskInfo(taskId: Int) = ActivityManager.RunningTaskInfo().apply { this.taskId = taskId this.token = WindowContainerToken(mock() /* realToken */) this.token = MockToken().token() this.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN } } Loading Loading
libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +2 −1 Original line number Diff line number Diff line Loading @@ -512,7 +512,8 @@ public class BubbleController implements ConfigurationChangeListener, } }, mMainHandler); mTransitions.registerObserver(new BubblesTransitionObserver(this, mBubbleData)); mTransitions.registerObserver(new BubblesTransitionObserver(this, mBubbleData, mBubbleTransitions.mTaskViewTransitions, mSplitScreenController)); mTaskStackListener.addListener( new BubbleTaskStackListener(this, mBubbleData, mSplitScreenController)); Loading
libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblesTransitionObserver.java +55 −1 Original line number Diff line number Diff line Loading @@ -21,17 +21,28 @@ import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BUBBLES_NOISY; import android.app.ActivityManager; import android.os.Binder; import android.os.IBinder; import android.view.SurfaceControl; import android.window.TransitionInfo; import android.window.WindowContainerTransaction; import androidx.annotation.NonNull; import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.bubbles.util.BubbleUtilsKt; import com.android.wm.shell.shared.TransitionUtil; import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper; import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.taskview.TaskViewTaskController; import com.android.wm.shell.taskview.TaskViewTransitions; import com.android.wm.shell.transition.Transitions; import dagger.Lazy; import java.util.Optional; /** * Observer used to identify tasks that are opening or moving to front. If a bubble activity is * currently opened when this happens, we'll collapse the bubbles. Loading @@ -42,18 +53,30 @@ public class BubblesTransitionObserver implements Transitions.TransitionObserver private final BubbleController mBubbleController; @NonNull private final BubbleData mBubbleData; private final TaskViewTransitions mTaskViewTransitions; private final Lazy<Optional<SplitScreenController>> mSplitScreenController; public BubblesTransitionObserver(@NonNull BubbleController controller, @NonNull BubbleData bubbleData) { @NonNull BubbleData bubbleData, TaskViewTransitions taskViewTransitions, Lazy<Optional<SplitScreenController>> splitScreenController) { mBubbleController = controller; mBubbleData = bubbleData; mTaskViewTransitions = taskViewTransitions; mSplitScreenController = splitScreenController; } @Override public void onTransitionReady(@NonNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction) { collapseBubbleIfNeeded(info); if (BubbleAnythingFlagHelper.enableCreateAnyBubble()) { removeBubbleIfLaunchingToSplit(info); } } private void collapseBubbleIfNeeded(@NonNull TransitionInfo info) { // --- Pre-conditions (Loop-invariant checks) --- // If bubbles aren't expanded, are animating, or no bubble is selected, // we don't need to process any transitions for collapsing. Loading Loading @@ -106,6 +129,37 @@ public class BubblesTransitionObserver implements Transitions.TransitionObserver } } private void removeBubbleIfLaunchingToSplit(@NonNull TransitionInfo info) { if (mSplitScreenController.get().isEmpty()) return; SplitScreenController splitScreenController = mSplitScreenController.get().get(); for (TransitionInfo.Change change : info.getChanges()) { ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); if (taskInfo == null) continue; Bubble bubble = mBubbleData.getBubbleInStackWithTaskId(taskInfo.taskId); if (bubble == null) continue; if (!splitScreenController.isTaskRootOrStageRoot(taskInfo.parentTaskId)) continue; // There is a bubble task that is moving to split screen ProtoLog.d(WM_SHELL_BUBBLES_NOISY, "TransitionObserver.onTransitionReady(): removing bubble for task launching " + "into split taskId=%d", taskInfo.taskId); TaskViewTaskController taskViewTaskController = bubble.getTaskView().getController(); ShellTaskOrganizer taskOrganizer = taskViewTaskController.getTaskOrganizer(); WindowContainerTransaction wct = BubbleUtilsKt.getExitBubbleTransaction(taskInfo.token, bubble.getTaskView().getCaptionInsetsOwner()); // Notify the task removal, but block all TaskViewTransitions during removal so we can // clear them without triggering final IBinder gate = new Binder(); mTaskViewTransitions.enqueueExternal(taskViewTaskController, () -> gate); taskOrganizer.applyTransaction(wct); taskViewTaskController.notifyTaskRemovalStarted(taskInfo); mTaskViewTransitions.removePendingTransitions(taskViewTaskController); mTaskViewTransitions.onExternalDone(gate); } } @Override public void onTransitionStarting(@NonNull IBinder transition) { Loading
libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +4 −0 Original line number Diff line number Diff line Loading @@ -2036,6 +2036,10 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // TODO (b/336477473): Disallow enter PiP when launching a task in split by default; // this might have to be changed as more split-to-pip cujs are defined. options.setDisallowEnterPictureInPictureWhileLaunching(true); // Set an empty rect as the requested launch bounds. This ensures that if an existing // task is reused, and it has bounds set, they are cleared. options.setLaunchBounds(new Rect()); opts.putAll(options.toBundle()); } Loading
libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java +9 −0 Original line number Diff line number Diff line Loading @@ -283,6 +283,15 @@ public class TaskViewTransitions implements Transitions.TransitionHandler, TaskV return !mPending.isEmpty(); } /** Removes all pending transitions for the given {@code taskView}. */ public void removePendingTransitions(TaskViewTaskController taskView) { for (int i = mPending.size() - 1; i >= 0; --i) { if (mPending.get(i).mTaskView != taskView) continue; if (mPending.get(i).mExternalTransition != null) continue; mPending.remove(i); } } @Override public WindowContainerTransaction handleRequest(@NonNull IBinder transition, @Nullable TransitionRequestInfo request) { Loading
libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblesTransitionObserverTest.kt +70 −9 Original line number Diff line number Diff line Loading @@ -19,6 +19,7 @@ package com.android.wm.shell.bubbles import android.app.ActivityManager import android.app.ActivityTaskManager.INVALID_TASK_ID import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW import android.platform.test.annotations.EnableFlags import android.view.WindowManager.TRANSIT_CHANGE import android.view.WindowManager.TRANSIT_CLOSE Loading @@ -27,17 +28,27 @@ import android.view.WindowManager.TRANSIT_TO_BACK import android.view.WindowManager.TRANSIT_TO_FRONT import android.view.WindowManager.TransitionType import android.window.TransitionInfo import android.window.WindowContainerToken import android.window.WindowContainerTransaction import androidx.test.filters.SmallTest import com.android.wm.shell.Flags.FLAG_ENABLE_CREATE_ANY_BUBBLE import com.android.wm.shell.MockToken import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.ShellTestCase import com.android.wm.shell.bubbles.util.verifyExitBubbleTransaction import com.android.wm.shell.splitscreen.SplitScreenController import com.android.wm.shell.taskview.TaskView import com.android.wm.shell.taskview.TaskViewTaskController import com.android.wm.shell.taskview.TaskViewTransitions import com.android.wm.shell.transition.TransitionInfoBuilder import com.google.testing.junit.testparameterinjector.TestParameter import com.google.testing.junit.testparameterinjector.TestParameterInjector import java.util.Optional import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import org.mockito.kotlin.stub Loading @@ -45,8 +56,7 @@ import org.mockito.kotlin.stub /** * Unit tests of [BubblesTransitionObserver]. * * Build/Install/Run: * atest WMShellUnitTests:BubblesTransitionObserverTest * Build/Install/Run: atest WMShellUnitTests:BubblesTransitionObserverTest */ @SmallTest @RunWith(TestParameterInjector::class) Loading @@ -62,7 +72,17 @@ class BubblesTransitionObserverTest : ShellTestCase() { private val bubbleController = mock<BubbleController> { on { isStackAnimating } doReturn false } private val transitionObserver = BubblesTransitionObserver(bubbleController, bubbleData) private val taskViewTransitions = mock<TaskViewTransitions>() private val splitScreenController = mock<SplitScreenController> { on { isTaskRootOrStageRoot(anyInt()) } doReturn false } private val transitionObserver = BubblesTransitionObserver( bubbleController, bubbleData, taskViewTransitions, { Optional.of(splitScreenController) }, ) @Test fun testOnTransitionReady_openWithTaskTransition_collapsesStack() { Loading Loading @@ -179,6 +199,46 @@ class BubblesTransitionObserverTest : ShellTestCase() { verify(bubbleData, never()).setExpanded(false) } @EnableFlags(FLAG_ENABLE_CREATE_ANY_BUBBLE) @Test fun testOnTransitionReady_bubbleMovingToSplit_removeBubble() { val taskOrganizer = mock<ShellTaskOrganizer>() val taskViewTaskController = mock<TaskViewTaskController> { on { this.taskOrganizer } doReturn taskOrganizer } val taskView = mock<TaskView> { on { controller } doReturn taskViewTaskController } bubble.stub { on { this.taskView } doReturn taskView } bubbleData.stub { on { getBubbleInStackWithTaskId(bubble.taskId) } doReturn bubble } splitScreenController.stub { on { isTaskRootOrStageRoot(10) } doReturn true } val taskInfo = createTaskInfo(taskId = 1).apply { this.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_MULTI_WINDOW this.parentTaskId = 10 } val info = createTaskTransition(TRANSIT_TO_FRONT, taskInfo) transitionObserver.onTransitionReady(mock(), info, mock(), mock()) // Check that we remove the taskView verify(taskViewTaskController).notifyTaskRemovalStarted(taskInfo) val wctCaptor = argumentCaptor<WindowContainerTransaction>() // And clean up bubble specific overrides on a task verify(taskOrganizer).applyTransaction(wctCaptor.capture()) verifyExitBubbleTransaction( wctCaptor.firstValue, taskInfo.token.asBinder(), /* captionInsetsOwner */ null, ) } // Transits that aren't opening. enum class TransitNotOpeningTestCase( @TransitionType private val changeType: Int, Loading @@ -201,9 +261,10 @@ class BubblesTransitionObserverTest : ShellTestCase() { taskInfo: ActivityManager.RunningTaskInfo?, ) = TransitionInfoBuilder(TRANSIT_OPEN).addChange(changeType, taskInfo).build() private fun createTaskInfo(taskId: Int) = ActivityManager.RunningTaskInfo().apply { private fun createTaskInfo(taskId: Int) = ActivityManager.RunningTaskInfo().apply { this.taskId = taskId this.token = WindowContainerToken(mock() /* realToken */) this.token = MockToken().token() this.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN } } Loading