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

Commit c9e9b20a authored by Ats Jenk's avatar Ats Jenk Committed by Android (Google) Code Review
Browse files

Merge "Remove a task from bubbles if it is moving to split" into main

parents a1338be5 21c00578
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.
@@ -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) {

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