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

Commit 21c00578 authored by Ats Jenk's avatar Ats Jenk
Browse files

Remove a task from bubbles if it is moving to split

Add a check to BubblesTransitionObserver for the case when a bubbled
task is moving to split screen.
If we detect this, remove the bubble and the task view for it.

Blocks any of the task view transitions from running that are triggered
from removing the task view. This task is moving to split screen and we
do not need to update core or start any transitions. Just clean up on
the Shell side.
Adds a dummy transition into the task view transitions queue, this
allows us to remove any pending transitions that are scheduled after
removing the task view.

Bug: 396315875
Test: atest WMShellUnitTests:BubblesTransitionObserverTest
Flag: com.android.wm.shell.enable_create_any_bubble
Change-Id: Ic997627e9f1d229e958940831300d11621396a1e
parent 17c1aa57
Loading
Loading
Loading
Loading
+2 −1
Original line number Diff line number Diff line
@@ -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));
+55 −1
Original line number Diff line number Diff line
@@ -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.
@@ -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.
@@ -104,6 +127,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) {

+4 −0
Original line number Diff line number Diff line
@@ -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());
    }

+9 −0
Original line number Diff line number Diff line
@@ -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) {
+70 −9
Original line number Diff line number Diff line
@@ -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
@@ -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
@@ -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)
@@ -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() {
@@ -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,
@@ -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
            }
    }