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

Commit 62e92de7 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Fix bubble task relaunch into split screen transition." into main

parents 27470e7f 153f7afc
Loading
Loading
Loading
Loading
+86 −12
Original line number Diff line number Diff line
@@ -51,6 +51,7 @@ import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import com.android.internal.logging.testing.UiEventLoggerFake
import com.android.internal.protolog.ProtoLog
import com.android.internal.statusbar.IStatusBarService
import com.android.window.flags.Flags.FLAG_ROOT_TASK_FOR_BUBBLE
import com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_BAR
import com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_BAR_TO_FLOATING_TRANSITION
import com.android.wm.shell.Flags.FLAG_ENABLE_CREATE_ANY_BUBBLE
@@ -76,6 +77,7 @@ import com.android.wm.shell.draganddrop.DragAndDropController
import com.android.wm.shell.shared.TransactionPool
import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper
import com.android.wm.shell.shared.bubbles.DeviceConfig
import com.android.wm.shell.splitscreen.SplitScreenController
import com.android.wm.shell.sysui.ShellCommandHandler
import com.android.wm.shell.sysui.ShellController
import com.android.wm.shell.sysui.ShellInit
@@ -88,6 +90,8 @@ import com.android.wm.shell.transition.Transitions.TransitionHandler
import com.android.wm.shell.unfold.ShellUnfoldProgressProvider
import com.google.common.truth.Truth.assertThat
import com.google.common.util.concurrent.MoreExecutors.directExecutor
import java.util.Optional
import java.util.concurrent.Executor
import org.junit.After
import org.junit.Assume.assumeTrue
import org.junit.Before
@@ -102,11 +106,10 @@ import org.mockito.kotlin.eq
import org.mockito.kotlin.isA
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.stub
import org.mockito.kotlin.verify
import platform.test.runner.parameterized.ParameterizedAndroidJunit4
import platform.test.runner.parameterized.Parameters
import java.util.Optional
import java.util.concurrent.Executor

/** Tests for [BubbleController].
 *
@@ -123,14 +126,16 @@ class BubbleControllerTest(flags: FlagsParameterization) {

    private val context = ApplicationProvider.getApplicationContext<Context>()
    private val uiEventLoggerFake = UiEventLoggerFake()
    private val bubbleAppInfoProvider = FakeBubbleAppInfoProvider()
    private val unfoldProgressProvider = FakeShellUnfoldProgressProvider()
    private val displayImeController = mock<DisplayImeController>()
    private val displayInsetsController = mock<DisplayInsetsController>()
    private val userManager = mock<UserManager>()
    private val splitScreenController = mock<SplitScreenController>()
    private val taskStackListener = mock<TaskStackListenerImpl>()
    private val transitions = mock<Transitions>()
    private val taskViewTransitions = mock<TaskViewTransitions>()
    private val bubbleAppInfoProvider = FakeBubbleAppInfoProvider()
    private val unfoldProgressProvider = FakeShellUnfoldProgressProvider()
    private val userManager = mock<UserManager>()
    private val windowManager = mock<WindowManager>()

    private lateinit var bubbleController: BubbleController
    private lateinit var bubblePositioner: BubblePositioner
@@ -142,7 +147,6 @@ class BubbleControllerTest(flags: FlagsParameterization) {
    private lateinit var displayController: DisplayController
    private lateinit var imeListener: ImeListener
    private lateinit var bubbleTransitions: BubbleTransitions
    private lateinit var shellTaskOrganizer: ShellTaskOrganizer

    private var isStayAwakeOnFold = false

@@ -175,7 +179,7 @@ class BubbleControllerTest(flags: FlagsParameterization) {
        val realWindowManager = context.getSystemService<WindowManager>()!!
        val realDefaultDisplay = realWindowManager.defaultDisplay
        // Tests don't have permission to add our window to windowManager, so we mock it :(
        val windowManager = mock<WindowManager> {
        windowManager.stub {
            // But we do want the metrics from the real one
            on { currentWindowMetrics } doReturn realWindowManager.currentWindowMetrics
            on { defaultDisplay } doReturn realDefaultDisplay
@@ -196,7 +200,7 @@ class BubbleControllerTest(flags: FlagsParameterization) {
                bgExecutor,
            )

        shellTaskOrganizer =
        val shellTaskOrganizer =
            ShellTaskOrganizer(
                mock<ShellInit>(),
                ShellCommandHandler(),
@@ -222,14 +226,12 @@ class BubbleControllerTest(flags: FlagsParameterization) {
            createBubbleController(
                bubbleData,
                windowManager,
                shellTaskOrganizer,
                bubbleLogger,
                bubblePositioner,
                mainExecutor,
                bgExecutor,
            )
        bubbleController.asBubbles().setSysuiProxy(mock<SysuiProxy>())
        // Flush so that proxy gets set
        mainExecutor.flushAll()

        val insetsChangedListenerCaptor = argumentCaptor<ImeListener>()
        verify(displayInsetsController)
@@ -458,6 +460,50 @@ class BubbleControllerTest(flags: FlagsParameterization) {
        assertThat(bubbleController.hasStableBubbleForTask(777)).isFalse()
    }

    @EnableFlags(FLAG_ROOT_TASK_FOR_BUBBLE)
    @Test
    fun shouldBeAppBubble_parentTaskMatchesBubbleRootTask_returnsTrue() {
        val bubbleController = createBubbleControllerWithRootTask(bubbleRootTaskId = 777)
        val taskInfo = ActivityManager.RunningTaskInfo().apply { parentTaskId = 777 }

        assertThat(bubbleController.shouldBeAppBubble(taskInfo)).isTrue()
    }

    @EnableFlags(FLAG_ROOT_TASK_FOR_BUBBLE)
    @Test
    fun shouldBeAppBubble_parentTaskDoesNotMatchesBubbleRootTask_returnsFalse() {
        val bubbleController = createBubbleControllerWithRootTask(bubbleRootTaskId = 123)
        val taskInfo = ActivityManager.RunningTaskInfo().apply { parentTaskId = 456 }

        assertThat(bubbleController.shouldBeAppBubble(taskInfo)).isFalse()
    }

    @DisableFlags(FLAG_ROOT_TASK_FOR_BUBBLE)
    @Test
    fun shouldBeAppBubble_taskIsSplitting_returnsFalse() {
        val sideStageRootTask = 5
        splitScreenController.stub {
            on { isTaskRootOrStageRoot(sideStageRootTask) } doReturn true
        }
        val taskInfo = ActivityManager.RunningTaskInfo().apply {
            // Task is running in split-screen mode.
            parentTaskId = sideStageRootTask
            // Even though the task was previously marked as an app bubble,
            // it should not be considered a bubble when in split-screen mode.
            isAppBubble = true
        }

        assertThat(bubbleController.shouldBeAppBubble(taskInfo)).isFalse()
    }

    @DisableFlags(FLAG_ROOT_TASK_FOR_BUBBLE)
    @Test
    fun shouldBeAppBubble_isAppBubbleNotSplitting_returnsTrue() {
        val taskInfo = ActivityManager.RunningTaskInfo().apply { isAppBubble = true }

        assertThat(bubbleController.shouldBeAppBubble(taskInfo)).isTrue()
    }

    @EnableFlags(FLAG_ENABLE_BUBBLE_BAR)
    @Test
    fun expandStackAndSelectBubbleForExistingTransition_reusesExistingBubble() {
@@ -839,6 +885,7 @@ class BubbleControllerTest(flags: FlagsParameterization) {
    private fun createBubbleController(
        bubbleData: BubbleData,
        windowManager: WindowManager,
        shellTaskOrganizer: ShellTaskOrganizer,
        bubbleLogger: BubbleLogger,
        bubblePositioner: BubblePositioner,
        mainExecutor: TestShellExecutor,
@@ -901,13 +948,40 @@ class BubbleControllerTest(flags: FlagsParameterization) {
                resizeChecker,
                HomeIntentProvider(context),
                bubbleAppInfoProvider,
                { Optional.empty() },
                { Optional.of(splitScreenController) },
                Optional.of(unfoldProgressProvider),
                { isStayAwakeOnFold },
            )
        bubbleController.setInflateSynchronously(true)
        bubbleController.onInit()

        bubbleController.asBubbles().setSysuiProxy(mock<SysuiProxy>())
        // Flush so that proxy gets set
        mainExecutor.flushAll()

        return bubbleController
    }

    private fun createBubbleControllerWithRootTask(bubbleRootTaskId: Int): BubbleController {
        val shellTaskOrganizer = mock<ShellTaskOrganizer>()
        val bubbleController = createBubbleController(
            bubbleData,
            windowManager,
            shellTaskOrganizer,
            bubbleLogger,
            bubblePositioner,
            mainExecutor,
            bgExecutor,
        )

        val rootTaskListener = argumentCaptor<ShellTaskOrganizer.TaskListener>().let { captor ->
            verify(shellTaskOrganizer).createRootTask(any(), captor.capture())
            captor.lastValue
        }

        val bubbleRootTask = ActivityManager.RunningTaskInfo().apply { taskId = bubbleRootTaskId }
        rootTaskListener.onTaskAppeared(bubbleRootTask, null /* leash */)

        return bubbleController
    }

+30 −12
Original line number Diff line number Diff line
@@ -26,9 +26,11 @@ import androidx.test.core.app.ApplicationProvider
import androidx.test.filters.SmallTest
import com.android.wm.shell.Flags.FLAG_ENABLE_CREATE_ANY_BUBBLE
import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper
import com.android.wm.shell.splitscreen.SplitScreenController
import com.android.wm.shell.taskview.TaskView
import com.google.common.truth.Truth.assertThat
import com.google.common.util.concurrent.MoreExecutors.directExecutor
import java.util.Optional
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -49,8 +51,17 @@ class BubbleTaskViewTest(flags: FlagsParameterization) {

    private val context = ApplicationProvider.getApplicationContext<Context>()
    private val componentName = ComponentName(context, "TestClass")
    private val taskView = mock<TaskView>()
    private val bubbleTaskView = BubbleTaskView(taskView, directExecutor())
    private val runningTaskInfo = ActivityManager.RunningTaskInfo()
    private val splitScreenController = mock<SplitScreenController>()
    private val taskView = mock<TaskView> {
        on { taskInfo } doReturn runningTaskInfo
    }
    private val bubbleTaskView =
        BubbleTaskView(
            taskView,
            executor = directExecutor(),
            splitScreenController = { Optional.of(splitScreenController) },
        )

    @Test
    fun onTaskCreated_updatesState() {
@@ -80,38 +91,45 @@ class BubbleTaskViewTest(flags: FlagsParameterization) {
    }

    @Test
    fun cleanup_invalidTaskId_removesTask() {
    fun cleanup_noTaskCreated_removesTask() {
        bubbleTaskView.cleanup()

        verify(taskView, never()).unregisterTask()
        verify(taskView).removeTask()
    }

    @Test
    fun cleanup_validTaskId_removesTask() {
    fun cleanup_regularBubbleTask_removesTask() {
        bubbleTaskView.listener.onTaskCreated(123 /* taskId */, componentName)

        bubbleTaskView.cleanup()

        verify(taskView, never()).unregisterTask()
        verify(taskView).removeTask()
    }

    @Test
    fun cleanup_noneFullscreenTask_removesTask() {
    fun cleanup_taskTransitioningToSplitScreen_unregistersTask() {
        val sideStageRootTask = 5
        splitScreenController.stub {
            on { isTaskRootOrStageRoot(sideStageRootTask) } doReturn true
        }
        runningTaskInfo.apply {
            parentTaskId = sideStageRootTask // Task is running in split-screen mode.
        }
        bubbleTaskView.listener.onTaskCreated(123 /* taskId */, componentName)

        bubbleTaskView.cleanup()

        verify(taskView, never()).unregisterTask()
        verify(taskView).removeTask()
        verify(taskView).unregisterTask()
        verify(taskView, never()).removeTask()
    }

    @Test
    fun cleanup_fullscreenTask_removesOrUnregistersTask() {
        val fullScreenTaskInfo = ActivityManager.RunningTaskInfo().apply {
    fun cleanup_taskTransitioningToFullscreen_removesOrUnregistersTask() {
        runningTaskInfo.apply {
            configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
        }
        taskView.stub {
            on { taskInfo } doReturn fullScreenTaskInfo
        }
        bubbleTaskView.listener.onTaskCreated(123 /* taskId */, componentName)

        bubbleTaskView.cleanup()
+15 −1
Original line number Diff line number Diff line
@@ -36,6 +36,7 @@ import static com.android.wm.shell.bubbles.Bubbles.DISMISS_NO_LONGER_BUBBLE;
import static com.android.wm.shell.bubbles.Bubbles.DISMISS_PACKAGE_REMOVED;
import static com.android.wm.shell.bubbles.Bubbles.DISMISS_SHORTCUT_REMOVED;
import static com.android.wm.shell.bubbles.Bubbles.DISMISS_USER_CHANGED;
import static com.android.wm.shell.bubbles.util.BubbleUtils.isBubbleToSplit;
import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BUBBLES;
import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BUBBLES_NOISY;
import static com.android.wm.shell.transition.Transitions.TRANSIT_BUBBLE_CONVERT_FLOATING_TO_BAR;
@@ -422,7 +423,7 @@ public class BubbleController implements ConfigurationChangeListener,
                        context, organizer, mTaskViewController, syncQueue);
                TaskView taskView = new TaskView(context, mTaskViewController,
                        taskViewTaskController);
                return new BubbleTaskView(taskView, mainExecutor);
                return new BubbleTaskView(taskView, mainExecutor, splitScreenController);
            }
        };
        mExpandedViewManager = BubbleExpandedViewManager.fromBubbleController(this);
@@ -1470,6 +1471,19 @@ public class BubbleController implements ConfigurationChangeListener,
            return mAppBubbleRootTaskInfo != null
                    && taskInfo.parentTaskId == mAppBubbleRootTaskInfo.taskId;
        }

        // Skip treating the task as an app bubble if it's transitioning from bubble to split.
        // In BubblesTransitionObserver#removeBubbleIfLaunchingToSplit, a WCT is applied to set
        // LaunchNextToBubble=false. Then TaskViewTaskController#notifyTaskRemovalStarted is called,
        // which triggers this check. However, the isAppBubble flag is only updated during the next
        // Task#fillTaskInfo by the WM core, so the flag we are currently processing is still true.
        // Later, TaskViewTransitions#onExternalDone unblocks the animation. Without this check,
        // DefaultMixedHandler could misinterpret the OPEN change as a bubble-enter transition,
        // incorrectly re-creating the bubble instead of completing the split-screen transition.
        if (isBubbleToSplit(taskInfo, mSplitScreenController)) {
            return false;
        }

        return taskInfo.isAppBubble;
    }

+11 −2
Original line number Diff line number Diff line
@@ -21,7 +21,11 @@ import android.app.ActivityTaskManager.INVALID_TASK_ID
import android.content.ComponentName
import androidx.annotation.VisibleForTesting
import com.android.wm.shell.bubbles.util.BubbleUtils.isBubbleToFullscreen
import com.android.wm.shell.bubbles.util.BubbleUtils.isBubbleToSplit
import com.android.wm.shell.splitscreen.SplitScreenController
import com.android.wm.shell.taskview.TaskView
import dagger.Lazy
import java.util.Optional
import java.util.concurrent.Executor

/**
@@ -29,7 +33,12 @@ import java.util.concurrent.Executor
 *
 * [delegateListener] allows callers to change listeners after a task has been created.
 */
class BubbleTaskView(val taskView: TaskView, executor: Executor) {
class BubbleTaskView @JvmOverloads constructor(
    val taskView: TaskView,
    executor: Executor,
    private val splitScreenController: Lazy<Optional<SplitScreenController>> =
        Lazy { Optional.empty() },
) {

    /** Whether the task is already created. */
    var isCreated = false
@@ -109,7 +118,7 @@ class BubbleTaskView(val taskView: TaskView, executor: Executor) {
     */
    fun cleanup() {
        val task = taskView.taskInfo
        if (task.isBubbleToFullscreen()) {
        if (task.isBubbleToFullscreen() || task.isBubbleToSplit(splitScreenController)) {
            taskView.unregisterTask()
        } else {
            taskView.removeTask()