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

Commit 9472876b authored by Eric Lin's avatar Eric Lin
Browse files

Collapse bubble on non-bubbled activity transition.

The BubblesTransitionObserver is updated to handle activity transitions
involving expanded bubbles. Previously, an activity launched from a
notification into an already expanded bubble's task would incorrectly
cause the bubble to collapse. This change leverages the task ID from
ActivityTransitionInfo (introduced in a preceding change) to determine
if the transitioning activity belongs to a bubble task in the stack. If
the target task is bubbled, the expanded bubble is preserved, thus
resolving the erroneous collapse.

This fix allows the observer to collapse the bubble when an activity
transition targets a different, non-bubbled task, such as an activity
launched from a quick settings tile into a fullscreen task. It also
collapses an expanded bubble if the underlying fullscreen app launches a
new activity. As task ID alone doesn't fully distinguish user or app
intent, this is considered an acceptable trade-off for improved bubble
behavior and the infrequency of such occurrences.

Bug: 390047887
Flag: EXEMPT bug fix
Test: atest WMShellUnitTests:BubblesTransitionObserverTest
Change-Id: I6c5ab588a41ecf096df0e6ea241a0151510570f6
parent 36567a83
Loading
Loading
Loading
Loading
+49 −26
Original line number Diff line number Diff line
@@ -24,6 +24,7 @@ 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;

@@ -53,12 +54,13 @@ 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,
            TaskViewTransitions taskViewTransitions,
            @NonNull TaskViewTransitions taskViewTransitions,
            Lazy<Optional<SplitScreenController>> splitScreenController) {
        mBubbleController = controller;
        mBubbleData = bubbleData;
@@ -100,37 +102,60 @@ public class BubblesTransitionObserver implements Transitions.TransitionObserver
            if (!TransitionUtil.isOpeningType(change.getMode())) {
                continue;
            }
            final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
            // We only handle task transitions.
            if (taskInfo == null || taskInfo.taskId == INVALID_TASK_ID) {
                continue;
            }
            // If the opening task id is the same as the expanded bubble, skip collapsing
            // because it is our bubble that is opening.
            if (taskInfo.taskId == expandedTaskId) {
            // If the opening transition is on a different display, skip collapsing because
            // it does not visually overlap with the bubbles.
            if (change.getEndDisplayId() != bubbleViewDisplayId) {
                continue;
            }
            // If the opening task is on a different display, skip collapsing because the task
            // opening does not visually overlap with the bubbles.
            if (taskInfo.displayId != bubbleViewDisplayId) {

            final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
            final ActivityTransitionInfo activityInfo = change.getActivityTransitionInfo();
            if (taskInfo != null) {  // Task transition.
                if (shouldBypassCollapseForTask(taskInfo.taskId, expandedTaskId)) {
                    continue;
                }
            // If the opening task was launched by another bubble, skip collapsing the existing one
            // since BubbleTransitions will start a new bubble for it

                // If the opening task was launched by another bubble, skip collapsing the
                // existing one since BubbleTransitions will start a new bubble for it.
                if (BubbleAnythingFlagHelper.enableCreateAnyBubble()
                        && mBubbleController.shouldBeAppBubble(taskInfo)) {
                ProtoLog.d(WM_SHELL_BUBBLES_NOISY, "TransitionObserver.onTransitionReady(): "
                    ProtoLog.d(WM_SHELL_BUBBLES_NOISY,
                            "BubblesTransitionObserver.onTransitionReady(): "
                                    + "skipping app bubble for taskId=%d", taskInfo.taskId);
                    continue;
                }
            } else if (activityInfo != null) {  // Activity transition.
                if (shouldBypassCollapseForTask(activityInfo.getTaskId(), expandedTaskId)) {
                    continue;
                }
            } else {  // Invalid transition.
                continue;
            }

            ProtoLog.d(WM_SHELL_BUBBLES_NOISY, "TransitionObserver.onTransitionReady(): "
                    + "collapsing bubble for taskId=%d", taskInfo.taskId);
            ProtoLog.d(WM_SHELL_BUBBLES_NOISY, "BubblesTransitionObserver.onTransitionReady(): "
                    + "collapse the expanded bubble for taskId=%d", expandedTaskId);
            mBubbleData.setExpanded(false);
            return;
        }
    }

    /** Checks if a task should be skipped for bubble collapse based on task ID. */
    private boolean shouldBypassCollapseForTask(int taskId, int expandedTaskId) {
        if (taskId == INVALID_TASK_ID) {
            ProtoLog.w(WM_SHELL_BUBBLES_NOISY, "BubblesTransitionObserver.onTransitionReady(): "
                    + "task id is invalid so skip collapsing");
            return true;
        }
        // If the opening task id is the same as the expanded bubble, skip collapsing
        // because it is our bubble that is opening.
        if (taskId == expandedTaskId) {
            ProtoLog.d(WM_SHELL_BUBBLES_NOISY, "BubblesTransitionObserver.onTransitionReady(): "
                    + "task %d is our bubble so skip collapsing", taskId);
            return true;
        }
        return false;
    }

    private void removeBubbleIfLaunchingToSplit(@NonNull TransitionInfo info) {
        if (mSplitScreenController.get().isEmpty()) return;
        SplitScreenController splitScreenController = mSplitScreenController.get().get();
@@ -141,10 +166,8 @@ public class BubblesTransitionObserver implements Transitions.TransitionObserver
            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);
            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 = BubbleUtilsKt.getExitBubbleTransaction(taskInfo.token,
+74 −20
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ 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
import android.view.WindowManager.TRANSIT_CLOSE
@@ -27,6 +28,7 @@ import android.view.WindowManager.TRANSIT_OPEN
import android.view.WindowManager.TRANSIT_TO_BACK
import android.view.WindowManager.TRANSIT_TO_FRONT
import android.view.WindowManager.TransitionType
import android.window.ActivityTransitionInfo
import android.window.TransitionInfo
import android.window.WindowContainerTransaction
import androidx.test.filters.SmallTest
@@ -40,14 +42,15 @@ 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.ArgumentMatchers.anyInt
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
@@ -57,7 +60,8 @@ import org.mockito.kotlin.verifyNoInteractions
/**
 * Unit tests of [BubblesTransitionObserver].
 *
 * Build/Install/Run: atest WMShellUnitTests:BubblesTransitionObserverTest
 * Build/Install/Run:
 * atest WMShellUnitTests:BubblesTransitionObserverTest
 */
@SmallTest
@RunWith(TestParameterInjector::class)
@@ -76,7 +80,7 @@ class BubblesTransitionObserverTest : ShellTestCase() {
    }
    private val taskViewTransitions = mock<TaskViewTransitions>()
    private val splitScreenController = mock<SplitScreenController> {
        on { isTaskRootOrStageRoot(anyInt()) } doReturn false
        on { isTaskRootOrStageRoot(any()) } doReturn false
    }
    private val transitionObserver =
        BubblesTransitionObserver(
@@ -96,11 +100,37 @@ class BubblesTransitionObserverTest : ShellTestCase() {
    }

    @Test
    fun testOnTransitionReady_openTaskOnAnotherDisplay_doesNotCollapseStack() {
        val taskInfo = createTaskInfo(taskId = 2).apply {
            displayId = 1 // not DEFAULT_DISPLAY
    fun testOnTransitionReady_noneBubbleActivityTransition_collapsesStack() {
        val info = createActivityTransition(TRANSIT_OPEN, taskId = 2)

        transitionObserver.onTransitionReady(mock(), info, mock(), mock())

        verify(bubbleData).setExpanded(false)
    }
        val info = createTaskTransition(TRANSIT_OPEN, taskInfo)

    @Test
    fun testOnTransitionReady_expandedBubbleActivityTransition_doesNotCollapseStack() {
        val info = createActivityTransition(TRANSIT_OPEN, taskId = 1)

        transitionObserver.onTransitionReady(mock(), info, mock(), mock())

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

    @Test
    fun testOnTransitionReady_activityTransitionOnAnotherDisplay_doesNotCollapseStack() {
        val displayId = 1 // not DEFAULT_DISPLAY
        val info = createActivityTransition(TRANSIT_OPEN, taskId = 1, displayId)

        transitionObserver.onTransitionReady(mock(), info, mock(), mock())

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

    @Test
    fun testOnTransitionReady_openTaskOnAnotherDisplay_doesNotCollapseStack() {
        val displayId = 1 // not DEFAULT_DISPLAY
        val info = createTaskTransition(TRANSIT_OPEN, taskId = 2, displayId)

        transitionObserver.onTransitionReady(mock(), info, mock(), mock())

@@ -140,10 +170,8 @@ class BubblesTransitionObserverTest : ShellTestCase() {
    }

    @Test
    fun testOnTransitionReady_noTaskId_skip() {
        val info = createTaskTransition(TRANSIT_OPEN, taskId = INVALID_TASK_ID) // Invalid task id

        transitionObserver.onTransitionReady(mock(), info, mock(), mock())
    fun testOnTransitionReady_noTaskId_skip(@TestParameter tc: InvalidTaskIdTestCase) {
        transitionObserver.onTransitionReady(mock(), tc.info, mock(), mock())

        verify(bubbleData, never()).setExpanded(false)
    }
@@ -267,17 +295,43 @@ class BubblesTransitionObserverTest : ShellTestCase() {
            get() = createTaskTransition(changeType, taskId)
    }

    // Invalid task id.
    enum class InvalidTaskIdTestCase(
        private val transitionCreator: (changeType: Int, taskId: Int) -> TransitionInfo,
    ) {
        ACTIVITY_TRANSITION(transitionCreator = ::createActivityTransition),
        TASK_TRANSITION(transitionCreator = ::createTaskTransition);

        val info: TransitionInfo
            get() = transitionCreator(TRANSIT_OPEN, INVALID_TASK_ID)
    }

    companion object {
        private fun createTaskTransition(@TransitionType changeType: Int, taskId: Int) =
            createTaskTransition(changeType, taskInfo = createTaskInfo(taskId))
        private val COMPONENT = ComponentName("com.example.app", "com.example.app.MainActivity")

        private fun createTaskTransition(
            @TransitionType changeType: Int,
            taskId: Int,
            displayId: Int = DEFAULT_DISPLAY_ID,
        ) = createTaskTransition(changeType, taskInfo = createTaskInfo(taskId), displayId)

        private fun createTaskTransition(
            @TransitionType changeType: Int,
            taskInfo: ActivityManager.RunningTaskInfo?,
        ) = TransitionInfoBuilder(TRANSIT_OPEN).addChange(changeType, taskInfo).build()
            displayId: Int = DEFAULT_DISPLAY_ID,
        ) = TransitionInfoBuilder(TRANSIT_OPEN, displayId = displayId)
            .addChange(changeType, taskInfo)
            .build()

        private fun createActivityTransition(
            @TransitionType changeType: Int,
            taskId: Int,
            displayId: Int = DEFAULT_DISPLAY_ID,
        ) = TransitionInfoBuilder(TRANSIT_OPEN, displayId = displayId)
            .addChange(changeType, ActivityTransitionInfo(COMPONENT, taskId))
            .build()

        private fun createTaskInfo(taskId: Int) =
            ActivityManager.RunningTaskInfo().apply {
        private fun createTaskInfo(taskId: Int) = ActivityManager.RunningTaskInfo().apply {
            this.taskId = taskId
            this.token = MockToken().token()
            this.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
+5 −3
Original line number Diff line number Diff line
@@ -33,11 +33,13 @@ import org.mockito.kotlin.mock
 * @param type the type of the transition. See [WindowManager.TransitionType].
 * @param flags the flags for the transition. See [WindowManager.TransitionFlags].
 * @param asNoOp if true, the root leash will not be added.
 * @param displayId the display ID for the root leash and transition changes.
 */
class TransitionInfoBuilder @JvmOverloads constructor(
    @WindowManager.TransitionType type: Int,
    @WindowManager.TransitionFlags flags: Int = 0,
    asNoOp: Boolean = false,
    private val displayId: Int = DEFAULT_DISPLAY_ID,
) {
    // The underlying TransitionInfo object being built.
    private val info: TransitionInfo = TransitionInfo(type, flags).apply {
@@ -46,7 +48,7 @@ class TransitionInfoBuilder @JvmOverloads constructor(
        }
        // Add a root leash by default, unless asNoOp is true.
        addRootLeash(
            DISPLAY_ID,
            displayId,
            createMockSurface(), /* leash */
            0, /* offsetLeft */
            0, /* offsetTop */
@@ -132,7 +134,7 @@ class TransitionInfoBuilder @JvmOverloads constructor(
     */
    fun addChange(change: TransitionInfo.Change): TransitionInfoBuilder {
        // Set the display ID for the change.
        change.setDisplayId(DISPLAY_ID /* start */, DISPLAY_ID /* end */)
        change.setDisplayId(displayId /* start */, displayId /* end */)
        // Add the change to the internal TransitionInfo object.
        info.addChange(change)
        return this // Return this for fluent builder pattern.
@@ -149,7 +151,7 @@ class TransitionInfoBuilder @JvmOverloads constructor(

    companion object {
        // Default display ID for root leashes and changes.
        const val DISPLAY_ID = 0
        const val DEFAULT_DISPLAY_ID = 0

        // Create a mock SurfaceControl for testing.
        private fun createMockSurface() = mock<SurfaceControl> {