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

Commit b8b1f62b authored by Eric Lin's avatar Eric Lin
Browse files

Fix regression in desktop decor visibility for bubble tasks.

In ag/32735920, bubble task visibility was changed to use task
reordering instead of the hidden property. As part of this, the
`alwaysOnTop` flag is reset to false when a bubble collapses, allowing
the task to be moved to the bottom of the stack.

However, desktop window decor logic uses the `alwaysOnTop` flag to
determine whether to show decor elements like the app handle. This led
to a regression where collapsed bubble tasks (which have `alwaysOnTop`
set to false) incorrectly triggered desktop decor logic.

This change fixes the issue by introducing a check in
`AppHandleAndHeaderVisibilityHelper` to recognize stable bubble tasks
via `BubbleController`, ensuring that collapsed bubble tasks are
excluded from decor rendering decisions even when `alwaysOnTop` is
false.

Bug: 388630258
Bug: 408389476
Flag: com.android.wm.shell.enable_create_any_bubble
Flag: com.android.window.flags.exclude_task_from_recents
Test: atest WMShellUnitTests:DesktopModeWindowDecorViewModelTests
Test: atest WMShellRobolectricTests:BubbleControllerTest
Test: atest WMShellMultivalentTestsOnDevice:BubbleControllerTest
Test: atest systemui-bubble-1-jank-suite \
--request-upload-result \
-- --enable-module-dynamic-download \
--module-arg systemui-bubble-1-jank-suite:strict-include-metric-filter:'perfetto_ft_systemui-missed_app_frames-mean' \
--test-arg com.android.tradefed.testtype.AndroidJUnitTest:class:android.platform.test.scenario.sysui.bubble.ShowMultipleBubblesAndSwitchMicrobenchmark
http://ab/I40600010387932237 (flag disabled, 6.24)
http://ab/I30400010385432590 (flag enabled, 10.36)
Change-Id: I0ce2060bed50ff973f1046f11a9da4c6872f5333

Change-Id: I9c20f3e534f2dc9e5d7a62da83777d2ee2eda5d6
parent 9dca151a
Loading
Loading
Loading
Loading
+46 −2
Original line number Diff line number Diff line
@@ -320,7 +320,51 @@ class BubbleControllerTest(flags: FlagsParameterization) {
        }
    }

    private fun createBubble(key: String): Bubble {
    @Test
    fun hasStableBubbleForTask_whenBubbleIsCollapsed_returnsTrue() {
        val taskId = 777
        val bubble = createBubble("key", taskId)
        getInstrumentation().runOnMainSync {
            bubbleData.notificationEntryUpdated(
                bubble,
                true /* suppressFlyout */,
                true /* showInShade= */,
            )
        }

        assertThat(bubbleController.hasStableBubbleForTask(taskId)).isTrue()
    }

    @Test
    fun hasStableBubbleForTask_whenBubbleInTransition_returnsFalse() {
        val taskId = 777
        val bubble = createBubble("key", taskId).apply { preparingTransition = mock() }
        getInstrumentation().runOnMainSync {
            bubbleData.notificationEntryUpdated(
                bubble,
                true /* suppressFlyout */,
                true /* showInShade= */,
            )
        }

        assertThat(bubbleController.hasStableBubbleForTask(taskId)).isFalse()
    }

    @Test
    fun hasStableBubbleForTask_noBubble_returnsFalse() {
        val bubble = createBubble("key", taskId = 123)
        getInstrumentation().runOnMainSync {
            bubbleData.notificationEntryUpdated(
                bubble,
                true /* suppressFlyout */,
                true /* showInShade= */,
            )
        }

        assertThat(bubbleController.hasStableBubbleForTask(777)).isFalse()
    }

    private fun createBubble(key: String, taskId: Int = 0): Bubble {
        val icon = Icon.createWithResource(context.resources, R.drawable.bubble_ic_overflow_button)
        val shortcutInfo = ShortcutInfo.Builder(context, "fakeId").setIcon(icon).build()
        val bubble =
@@ -330,7 +374,7 @@ class BubbleControllerTest(flags: FlagsParameterization) {
                /* desiredHeight= */ 0,
                Resources.ID_NULL,
                "title",
                /* taskId= */ 0,
                taskId,
                "locus",
                /* isDismissable= */ true,
                directExecutor(),
+6 −0
Original line number Diff line number Diff line
@@ -1374,6 +1374,12 @@ public class BubbleController implements ConfigurationChangeListener,
        return mBubbleData.hasBubbles() || mBubbleData.isShowingOverflow();
    }

    /** Returns whether the given task is a non-transient bubble. */
    public boolean hasStableBubbleForTask(int taskId) {
        final Bubble bubble = mBubbleData.getBubbleInStackWithTaskId(taskId);
        return bubble != null && bubble.getPreparingTransition() == null;
    }

    public boolean isStackExpanded() {
        return mBubbleData.isExpanded();
    }
+3 −2
Original line number Diff line number Diff line
@@ -1168,9 +1168,10 @@ public abstract class WMShellModule {
    static AppHandleAndHeaderVisibilityHelper provideAppHandleAndHeaderVisibilityHelper(
            @NonNull DisplayController displayController,
            @NonNull DesktopModeCompatPolicy desktopModeCompatPolicy,
            @NonNull DesktopState desktopState) {
            @NonNull DesktopState desktopState,
            Optional<BubbleController> bubbleController) {
        return new AppHandleAndHeaderVisibilityHelper(displayController,
                desktopModeCompatPolicy, desktopState);
                desktopModeCompatPolicy, desktopState, bubbleController);
    }

    @WMSingleton
+14 −0
Original line number Diff line number Diff line
@@ -21,12 +21,14 @@ import android.app.WindowConfiguration
import android.view.Display
import android.view.WindowManager
import android.window.DesktopExperienceFlags.ENABLE_BUG_FIXES_FOR_SECONDARY_DISPLAY
import com.android.wm.shell.bubbles.BubbleController
import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.desktopmode.DesktopWallpaperActivity.Companion.isWallpaperTask
import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper
import com.android.wm.shell.shared.desktopmode.DesktopModeCompatPolicy
import com.android.wm.shell.shared.desktopmode.DesktopState
import com.android.wm.shell.splitscreen.SplitScreenController
import java.util.Optional

/**
 * Resolves whether, given a task and its associated display that it is currently on, to show the
@@ -36,6 +38,7 @@ class AppHandleAndHeaderVisibilityHelper (
    private val displayController: DisplayController,
    private val desktopModeCompatPolicy: DesktopModeCompatPolicy,
    private val desktopState: DesktopState,
    private val bubbleController: Optional<BubbleController>,
) {
    var splitScreenController: SplitScreenController? = null

@@ -90,11 +93,22 @@ class AppHandleAndHeaderVisibilityHelper (
                return false
            }
        }

        // Bubble tasks reset alwaysOnTop when reordering a task to the bottom to hide its task view
        // in TaskViewTransitions#setTaskViewVisible, so we need to explicitly check here.
        fun ActivityManager.RunningTaskInfo.isBubble(): Boolean =
            if (BubbleAnythingFlagHelper.enableCreateAnyBubbleWithForceExcludedFromRecents()) {
                bubbleController.map { it.hasStableBubbleForTask(taskId) }.orElse(false)
            } else {
                false
            }

        return desktopState.canEnterDesktopModeOrShowAppHandle
                && !isWallpaperTask(taskInfo)
                && taskInfo.windowingMode != WindowConfiguration.WINDOWING_MODE_PINNED
                && taskInfo.activityType == WindowConfiguration.ACTIVITY_TYPE_STANDARD
                && !taskInfo.configuration.windowConfiguration.isAlwaysOnTop
                && !taskInfo.isBubble()
    }

    private fun allowedForDisplay(display: Display): Boolean {
+64 −0
Original line number Diff line number Diff line
@@ -66,13 +66,16 @@ import com.android.wm.shell.desktopmode.DesktopTasksController
import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition
import com.android.wm.shell.desktopmode.common.ToggleTaskSizeInteraction
import com.android.wm.shell.recents.RecentsTransitionStateListener
import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper
import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource
import com.android.wm.shell.splitscreen.SplitScreenController
import com.android.wm.shell.util.StubTransaction
import com.google.common.truth.Truth.assertThat
import java.util.function.Consumer
import junit.framework.Assert.assertFalse
import junit.framework.Assert.assertTrue
import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.junit.Assume.assumeTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -1292,6 +1295,67 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest
        verify(decoration, times(1)).setIsRecentsTransitionRunning(true)
    }

    @Test
    fun testOnTaskOpening_expandedBubbleTask_skipsWindowDecorationCreation() {
        val taskInfo = createTask(windowingMode = WINDOWING_MODE_MULTI_WINDOW).apply {
            // Bubble task is launched with ActivityOptions#setTaskAlwaysOnTop
            // in BubbleTaskViewListener#onInitialized.
            configuration.windowConfiguration.setAlwaysOnTop(true)
        }
        mockBubbleController.stub {
            on { hasStableBubbleForTask(taskInfo.taskId) } doReturn true
        }

        val isWindowDecorCreated = desktopModeWindowDecorViewModel.onTaskOpening(
            taskInfo,
            SurfaceControl(), /* taskSurface */
            StubTransaction(), /* startT */
            StubTransaction(), /* finishT */
        )

        assertThat(isWindowDecorCreated).isFalse()
    }

    @Test
    fun testOnTaskChanging_collapsedBubbleTask_skipsWindowDecorationCreation() {
        assumeTrue(BubbleAnythingFlagHelper.enableCreateAnyBubbleWithForceExcludedFromRecents())

        val taskInfo = createTask(windowingMode = WINDOWING_MODE_MULTI_WINDOW)
        mockBubbleController.stub {
            on { hasStableBubbleForTask(taskInfo.taskId) } doReturn true
        }

        desktopModeWindowDecorViewModel.onTaskChanging(
            taskInfo,
            SurfaceControl(), /* taskSurface */
            StubTransaction(), /* startT */
            StubTransaction(), /* finishT */
        )

        assertThat(windowDecorByTaskIdSpy.contains(taskInfo.taskId)).isFalse()
    }

    @Test
    fun testOnTaskChanging_convertTaskToBubble_destroysWindowDecoration() {
        assumeTrue(BubbleAnythingFlagHelper.enableCreateAnyBubbleWithForceExcludedFromRecents())

        val taskInfo = createTask(windowingMode = WINDOWING_MODE_MULTI_WINDOW)
        mockBubbleController.stub {
            on { hasStableBubbleForTask(taskInfo.taskId) } doReturn true
        }
        val mockDecoration = mock<DesktopModeWindowDecoration>()
        windowDecorByTaskIdSpy.put(taskInfo.taskId, mockDecoration)

        desktopModeWindowDecorViewModel.onTaskChanging(
            taskInfo,
            SurfaceControl(), /* taskSurface */
            StubTransaction(), /* startT */
            StubTransaction(), /* finishT */
        )

        verify(mockDecoration).close()
    }

    private fun createOpenTaskDecoration(
        @WindowingMode windowingMode: Int,
        taskSurface: SurfaceControl = SurfaceControl(),
Loading