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

Commit 4c9b22a3 authored by Ats Jenk's avatar Ats Jenk
Browse files

Remove a task from bubbles when it is moving to split

Initial change got tagged as a perf regression: ag/33580600

Adding a bugfix flag for the initial change and also including an
improvement to filter the changes processed from: ag/33652568

Bug: 396315875
Test: atest WMShellUnitTests:BubblesTransitionObserverTest
Test: launch Calculator in a bubble, move back to home screen, start
  split screen from Messages app, choose Calculator as the second app,
  confirm that Messages and Calculator are both in split screen
Flag: com.android.wm.shell.enable_enter_split_remove_bubble
Change-Id: I43cc0983a99ca89aef32c06aab67782ea9af8a59
parent 305624c3
Loading
Loading
Loading
Loading
+2 −1
Original line number Diff line number Diff line
@@ -510,7 +510,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));
+54 −1
Original line number Diff line number Diff line
@@ -18,21 +18,33 @@ package com.android.wm.shell.bubbles;

import static android.app.ActivityTaskManager.INVALID_TASK_ID;

import static com.android.wm.shell.Flags.enableEnterSplitRemoveBubble;
import static com.android.wm.shell.bubbles.util.BubbleUtils.getExitBubbleTransaction;
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.ActivityTransitionInfo;
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.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.
@@ -43,11 +55,18 @@ public class BubblesTransitionObserver implements Transitions.TransitionObserver
    private final BubbleController mBubbleController;
    @NonNull
    private final BubbleData mBubbleData;
    @NonNull
    private final TaskViewTransitions mTaskViewTransitions;
    private final Lazy<Optional<SplitScreenController>> mSplitScreenController;

    public BubblesTransitionObserver(@NonNull BubbleController controller,
            @NonNull BubbleData bubbleData) {
            @NonNull BubbleData bubbleData,
            @NonNull TaskViewTransitions taskViewTransitions,
            Lazy<Optional<SplitScreenController>> splitScreenController) {
        mBubbleController = controller;
        mBubbleData = bubbleData;
        mTaskViewTransitions = taskViewTransitions;
        mSplitScreenController = splitScreenController;
    }

    @Override
@@ -55,6 +74,11 @@ public class BubblesTransitionObserver implements Transitions.TransitionObserver
            @NonNull SurfaceControl.Transaction startTransaction,
            @NonNull SurfaceControl.Transaction finishTransaction) {
        collapseBubbleIfNeeded(info);
        if (enableEnterSplitRemoveBubble() && BubbleAnythingFlagHelper.enableCreateAnyBubble()) {
            if (TransitionUtil.isOpeningType(info.getType()) && mBubbleData.hasBubbles()) {
                removeBubbleIfLaunchingToSplit(info);
            }
        }
    }

    private void collapseBubbleIfNeeded(@NonNull TransitionInfo info) {
@@ -133,6 +157,35 @@ public class BubblesTransitionObserver implements Transitions.TransitionObserver
        return false;
    }

    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, "BubblesTransitionObserver.onTransitionReady(): "
                    + "removing bubble for task launching into split taskId=%d", taskInfo.taskId);
            TaskViewTaskController taskViewTaskController = bubble.getTaskView().getController();
            ShellTaskOrganizer taskOrganizer = taskViewTaskController.getTaskOrganizer();
            WindowContainerTransaction wct = 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) {

+7 −0
Original line number Diff line number Diff line
@@ -38,6 +38,7 @@ import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP
import static com.android.window.flags.Flags.enableFullScreenWindowOnRemovingSplitScreenStageBugfix;
import static com.android.window.flags.Flags.enableMultiDisplaySplit;
import static com.android.window.flags.Flags.enableNonDefaultDisplaySplit;
import static com.android.wm.shell.Flags.enableEnterSplitRemoveBubble;
import static com.android.wm.shell.Flags.enableFlexibleSplit;
import static com.android.wm.shell.Flags.enableFlexibleTwoAppSplit;
import static com.android.wm.shell.common.split.SplitLayout.PARALLAX_ALIGN_CENTER;
@@ -2057,6 +2058,12 @@ 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);
        if (enableEnterSplitRemoveBubble()) {
            // 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());
    }

+72 −2
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.content.ComponentName
import android.platform.test.annotations.EnableFlags
import android.view.WindowManager.TRANSIT_CHANGE
@@ -29,19 +30,29 @@ import android.view.WindowManager.TRANSIT_TO_FRONT
import android.view.WindowManager.TransitionType
import android.window.ActivityTransitionInfo
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.Flags.FLAG_ENABLE_ENTER_SPLIT_REMOVE_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.BubbleTestUtils.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.android.wm.shell.transition.TransitionInfoBuilder.Companion.DEFAULT_DISPLAY_ID
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.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.kotlin.any
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.stub
@@ -68,10 +79,16 @@ class BubblesTransitionObserverTest : ShellTestCase() {
    private val bubbleController = mock<BubbleController> {
        on { isStackAnimating } doReturn false
    }
    private val taskViewTransitions = mock<TaskViewTransitions>()
    private val splitScreenController = mock<SplitScreenController> {
        on { isTaskRootOrStageRoot(any()) } doReturn false
    }
    private val transitionObserver =
        BubblesTransitionObserver(
            bubbleController,
            bubbleData,
            taskViewTransitions,
            { Optional.of(splitScreenController) },
        )

    @Test
@@ -165,6 +182,7 @@ class BubblesTransitionObserverTest : ShellTestCase() {
        transitionObserver.onTransitionReady(mock(), tc.info, mock(), mock())

        verify(bubbleData, never()).setExpanded(false)
        verifyNoInteractions(splitScreenController)
    }

    @Test
@@ -213,6 +231,58 @@ class BubblesTransitionObserverTest : ShellTestCase() {
        verify(bubbleData, never()).setExpanded(false)
    }

    @EnableFlags(FLAG_ENABLE_CREATE_ANY_BUBBLE, FLAG_ENABLE_ENTER_SPLIT_REMOVE_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,
        )
    }

    @EnableFlags(FLAG_ENABLE_CREATE_ANY_BUBBLE, FLAG_ENABLE_ENTER_SPLIT_REMOVE_BUBBLE)
    @Test
    fun testOnTransitionReady_noBubbles_doesNotCheckForSplitState() {
        bubbleData.stub {
            on { hasBubbles() } doReturn false
        }
        val info = createTaskTransition(TRANSIT_TO_FRONT, taskId = 1)
        transitionObserver.onTransitionReady(mock(), info, mock(), mock())

        verifyNoInteractions(splitScreenController)
    }

    // Transits that aren't opening.
    enum class TransitNotOpeningTestCase(
        @TransitionType private val changeType: Int,
@@ -264,7 +334,7 @@ class BubblesTransitionObserverTest : ShellTestCase() {

        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
        }
    }