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

Commit 132e558b authored by Eric Lin's avatar Eric Lin
Browse files

Enable bubble'd app task to fullscreen transition.

This change adds support for transitioning a bubble task to fullscreen
when launched from the launcher. It resets task force excluded from
recents and unregisters the task instead of task removal.

Bug: 388630258
Flag: com.android.wm.shell.enable_create_any_bubble
Flag: com.android.window.flags.exclude_task_from_recents
Test: atest WMShellUnitTests:TaskViewTest
Test: atest WMShellRobolectricTests:BubbleTaskStackListenerTest
Test: atest WMShellRobolectricTests:BubbleTaskViewTest
Test: atest WMShellMultivalentTestsOnDevice:BubbleTaskStackListenerTest
Test: atest WMShellMultivalentTestsOnDevice:BubbleTaskViewTest
Change-Id: Iff5e960f91407e1efbe4050e5c5a65f9113d7e0f
parent fd24d1ec
Loading
Loading
Loading
Loading
+94 −2
Original line number Diff line number Diff line
@@ -17,12 +17,26 @@
package com.android.wm.shell.bubbles

import android.app.ActivityManager
import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
import android.os.IBinder
import android.platform.test.flag.junit.SetFlagsRule
import android.window.IWindowContainerToken
import android.window.WindowContainerToken
import android.window.WindowContainerTransaction
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.protolog.ProtoLog
import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper
import com.android.wm.shell.taskview.TaskView
import com.android.wm.shell.taskview.TaskViewTaskController
import com.google.common.truth.Truth.assertThat
import org.junit.Assume.assumeTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.stub
@@ -32,7 +46,19 @@ import org.mockito.kotlin.verify
@SmallTest
@RunWith(AndroidJUnit4::class)
class BubbleTaskStackListenerTest {
    private val bubble = mock<Bubble>()

    @get:Rule
    val setFlagsRule = SetFlagsRule()

    private val mockTaskViewTaskController = mock<TaskViewTaskController> {
        on { taskOrganizer } doReturn mock<ShellTaskOrganizer>()
    }
    private val mockTaskView = mock<TaskView> {
        on { controller } doReturn mockTaskViewTaskController
    }
    private val bubble = mock<Bubble> {
        on { taskView } doReturn mockTaskView
    }
    private val bubbleController = mock<BubbleController>()
    private val bubbleData = mock<BubbleData>()
    private val bubbleTaskStackListener = BubbleTaskStackListener(
@@ -40,7 +66,13 @@ class BubbleTaskStackListenerTest {
        bubbleData,
    )
    private val bubbleTaskId = 123
    private val task = ActivityManager.RunningTaskInfo().apply { taskId = bubbleTaskId }
    private val bubbleTaskToken = WindowContainerToken(mock<IWindowContainerToken> {
        on { asBinder() } doReturn mock<IBinder>()
    })
    private val task = ActivityManager.RunningTaskInfo().apply {
        taskId = bubbleTaskId
        token = bubbleTaskToken
    }

    @Before
    fun setUp() {
@@ -64,6 +96,36 @@ class BubbleTaskStackListenerTest {
        verify(bubbleData).setSelectedBubbleAndExpandStack(bubble)
    }

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

        task.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
        bubbleData.stub {
            on { getBubbleInStackWithTaskId(bubbleTaskId) } doReturn bubble
        }

        bubbleTaskStackListener.onActivityRestartAttempt(
            task,
            homeTaskVisible = false,
            clearedTask = false,
            wasVisible = false,
        )

        val taskViewTaskController = bubble.taskView.controller
        val taskOrganizer = taskViewTaskController.taskOrganizer
        val wct = argumentCaptor<WindowContainerTransaction>().let { wctCaptor ->
            verify(taskOrganizer).applyTransaction(wctCaptor.capture())
            wctCaptor.lastValue
        }
        assertThat(wct.changes).hasSize(1)
        val chg = wct.changes.get(bubbleTaskToken.asBinder())
        assertThat(chg).isNotNull()
        assertThat(chg!!.forceExcludedFromRecents).isFalse()
        verify(taskOrganizer).setInterceptBackPressedOnTaskRoot(task.token, false /* intercept */)
        verify(taskViewTaskController).notifyTaskRemovalStarted(task)
    }

    @Test
    fun onActivityRestartAttempt_overflowAppBubbleRestart_promotesFromOverflow() {
        bubbleData.stub {
@@ -80,4 +142,34 @@ class BubbleTaskStackListenerTest {
        verify(bubbleController).promoteBubbleFromOverflow(bubble)
        verify(bubbleData).setExpanded(true)
    }

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

        task.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
        bubbleData.stub {
            on { getOverflowBubbleWithTaskId(bubbleTaskId) } doReturn bubble
        }

        bubbleTaskStackListener.onActivityRestartAttempt(
            task,
            homeTaskVisible = false,
            clearedTask = false,
            wasVisible = false,
        )

        val taskViewTaskController = bubble.taskView.controller
        val taskOrganizer = taskViewTaskController.taskOrganizer
        val wct = argumentCaptor<WindowContainerTransaction>().let { wctCaptor ->
            verify(taskOrganizer).applyTransaction(wctCaptor.capture())
            wctCaptor.lastValue
        }
        assertThat(wct.changes).hasSize(1)
        val chg = wct.changes.get(bubbleTaskToken.asBinder())
        assertThat(chg).isNotNull()
        assertThat(chg!!.forceExcludedFromRecents).isFalse()
        verify(taskOrganizer).setInterceptBackPressedOnTaskRoot(task.token, false /* intercept */)
        verify(taskViewTaskController).notifyTaskRemovalStarted(task)
    }
}
+67 −26
Original line number Diff line number Diff line
@@ -16,47 +16,48 @@

package com.android.wm.shell.bubbles

import android.app.ActivityManager
import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
import android.content.ComponentName
import android.content.Context
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.FlagsParameterization
import android.platform.test.flag.junit.SetFlagsRule
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.wm.shell.Flags
import com.android.window.flags.Flags.FLAG_EXCLUDE_TASK_FROM_RECENTS
import com.android.wm.shell.Flags.FLAG_ENABLE_CREATE_ANY_BUBBLE
import com.android.wm.shell.Flags.FLAG_ENABLE_TASK_VIEW_CONTROLLER_CLEANUP
import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper
import com.android.wm.shell.taskview.TaskView

import com.google.common.truth.Truth.assertThat
import com.google.common.util.concurrent.MoreExecutors.directExecutor
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.doReturn
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

@SmallTest
@RunWith(AndroidJUnit4::class)
class BubbleTaskViewTest {
@RunWith(ParameterizedAndroidJunit4::class)
class BubbleTaskViewTest(flags: FlagsParameterization) {

    @get:Rule
    val setFlagsRule = SetFlagsRule()
    val setFlagsRule = SetFlagsRule(flags)

    private lateinit var bubbleTaskView: BubbleTaskView
    private val context = ApplicationProvider.getApplicationContext<Context>()
    private lateinit var taskView: TaskView

    @Before
    fun setUp() {
        taskView = mock()
        bubbleTaskView = BubbleTaskView(taskView, directExecutor())
    }
    private val componentName = ComponentName(context, "TestClass")
    private val taskView = mock<TaskView>()
    private val bubbleTaskView = BubbleTaskView(taskView, directExecutor())

    @Test
    fun onTaskCreated_updatesState() {
        val componentName = ComponentName(context, "TestClass")
        bubbleTaskView.listener.onTaskCreated(123, componentName)

        assertThat(bubbleTaskView.taskId).isEqualTo(123)
@@ -76,44 +77,84 @@ class BubbleTaskViewTest {
        }
        bubbleTaskView.delegateListener = delegateListener

        val componentName = ComponentName(context, "TestClass")
        bubbleTaskView.listener.onTaskCreated(123, componentName)
        bubbleTaskView.listener.onTaskCreated(123 /* taskId */, componentName)

        assertThat(actualTaskId).isEqualTo(123)
        assertThat(actualComponentName).isEqualTo(componentName)
    }

    @DisableFlags(Flags.FLAG_ENABLE_TASK_VIEW_CONTROLLER_CLEANUP)
    @Test
    @DisableFlags(FLAG_ENABLE_TASK_VIEW_CONTROLLER_CLEANUP)
    fun cleanup_flagOff_invalidTaskId_doesNotRemoveTask() {
        bubbleTaskView.cleanup()
        verify(taskView, never()).removeTask()
    }

    @EnableFlags(Flags.FLAG_ENABLE_TASK_VIEW_CONTROLLER_CLEANUP)
    @Test
    @EnableFlags(FLAG_ENABLE_TASK_VIEW_CONTROLLER_CLEANUP)
    fun cleanup_flagOn_invalidTaskId_removesTask() {
        bubbleTaskView.cleanup()
        verify(taskView).removeTask()
    }

    @DisableFlags(Flags.FLAG_ENABLE_TASK_VIEW_CONTROLLER_CLEANUP)
    @Test
    @DisableFlags(FLAG_ENABLE_TASK_VIEW_CONTROLLER_CLEANUP)
    fun cleanup_flagOff_validTaskId_removesTask() {
        val componentName = ComponentName(context, "TestClass")
        bubbleTaskView.listener.onTaskCreated(123, componentName)
        bubbleTaskView.listener.onTaskCreated(123 /* taskId */, componentName)

        bubbleTaskView.cleanup()

        verify(taskView).removeTask()
    }

    @EnableFlags(Flags.FLAG_ENABLE_TASK_VIEW_CONTROLLER_CLEANUP)
    @Test
    @EnableFlags(FLAG_ENABLE_TASK_VIEW_CONTROLLER_CLEANUP)
    fun cleanup_flagOn_validTaskId_removesTask() {
        val componentName = ComponentName(context, "TestClass")
        bubbleTaskView.listener.onTaskCreated(123, componentName)
        bubbleTaskView.listener.onTaskCreated(123 /* taskId */, componentName)

        bubbleTaskView.cleanup()

        verify(taskView).removeTask()
    }

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

        bubbleTaskView.cleanup()

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

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

        bubbleTaskView.cleanup()

        if (BubbleAnythingFlagHelper.enableCreateAnyBubbleWithForceExcludedFromRecents()) {
            verify(taskView).unregisterTask()
            verify(taskView, never()).removeTask()
        } else {
            verify(taskView, never()).unregisterTask()
            verify(taskView).removeTask()
        }
    }

    companion object {
        @JvmStatic
        @Parameters(name = "{0}")
        fun getParams() = FlagsParameterization.allCombinationsOf(
            FLAG_ENABLE_CREATE_ANY_BUBBLE,
            FLAG_ENABLE_TASK_VIEW_CONTROLLER_CLEANUP,
            FLAG_EXCLUDE_TASK_FROM_RECENTS,
        )
    }
}
+73 −3
Original line number Diff line number Diff line
@@ -14,18 +14,27 @@
 * limitations under the License.
 */

// Exports bubble task utilities (e.g., `isBubbleToFullscreen`) for Java interop.
@file:JvmName("BubbleTaskUtils")

package com.android.wm.shell.bubbles

import android.app.ActivityManager
import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
import android.window.WindowContainerTransaction
import com.android.internal.protolog.ProtoLog
import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.common.TaskStackListenerCallback
import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BUBBLES
import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper
import com.android.wm.shell.taskview.TaskViewTaskController

/**
 * Listens for task stack changes and handles bubble interactions when activities are restarted.
 *
 * This class monitors task stack events to determine how bubbles should behave when their
 * associated activities are restarted. It handles scenarios where bubbles should be expanded.
 * associated activities are restarted. It handles scenarios where bubbles should be expanded
 * or moved to fullscreen based on the task's windowing mode.
 *
 * @property bubbleController The [BubbleController] to manage bubble promotions and expansions.
 * @property bubbleData The [BubbleData] to access and update bubble information.
@@ -43,14 +52,22 @@ class BubbleTaskStackListener(
    ) {
        val taskId = task.taskId
        bubbleData.getBubbleInStackWithTaskId(taskId)?.let { bubble ->
            if (isBubbleToFullscreen(task)) {
                moveCollapsedInStackBubbleToFullscreen(bubble, task)
            } else {
                selectAndExpandInStackBubble(bubble, task)
            }
            return@onActivityRestartAttempt
        }

        bubbleData.getOverflowBubbleWithTaskId(taskId)?.let { bubble ->
            if (isBubbleToFullscreen(task)) {
                moveCollapsedOverflowBubbleToFullscreen(bubble, task)
            } else {
                selectAndExpandOverflowBubble(bubble, task)
            }
        }
    }

    /** Selects and expands a bubble that is currently in the stack. */
    private fun selectAndExpandInStackBubble(
@@ -66,6 +83,21 @@ class BubbleTaskStackListener(
        bubbleData.setSelectedBubbleAndExpandStack(bubble)
    }

    /** Moves a collapsed bubble that is currently in the stack to fullscreen. */
    private fun moveCollapsedInStackBubbleToFullscreen(
        bubble: Bubble,
        task: ActivityManager.RunningTaskInfo,
    ) {
        ProtoLog.d(
            WM_SHELL_BUBBLES,
            "moveCollapsedInStackBubbleToFullscreen - taskId=%d " +
                    "moving matching bubble=%s to fullscreen",
            task.taskId,
            bubble.key
        )
        collapsedBubbleToFullscreenInternal(bubble, task)
    }

    /** Selects and expands a bubble that is currently in the overflow. */
    private fun selectAndExpandOverflowBubble(
        bubble: Bubble,
@@ -80,4 +112,42 @@ class BubbleTaskStackListener(
        bubbleController.promoteBubbleFromOverflow(bubble)
        bubbleData.setExpanded(true)
    }

    /** Moves a collapsed overflow bubble to fullscreen. */
    private fun moveCollapsedOverflowBubbleToFullscreen(
        bubble: Bubble,
        task: ActivityManager.RunningTaskInfo,
    ) {
        ProtoLog.d(
            WM_SHELL_BUBBLES,
            "moveCollapsedOverflowBubbleToFullscreen - taskId=%d " +
                    "moving matching overflow bubble=%s to fullscreen",
            task.taskId,
            bubble.key,
        )
        collapsedBubbleToFullscreenInternal(bubble, task)
    }

    /** Internal function to move a collapsed bubble to fullscreen task. */
    private fun collapsedBubbleToFullscreenInternal(
        bubble: Bubble,
        task: ActivityManager.RunningTaskInfo,
    ) {
        val taskViewTaskController: TaskViewTaskController = bubble.taskView.controller
        val taskOrganizer: ShellTaskOrganizer = taskViewTaskController.taskOrganizer

        val wct = WindowContainerTransaction()
        wct.setTaskForceExcludedFromRecents(task.token, false /* forceExcluded */)
        taskOrganizer.applyTransaction(wct)

        taskOrganizer.setInterceptBackPressedOnTaskRoot(task.token, false /* intercept */)

        taskViewTaskController.notifyTaskRemovalStarted(task)
    }
}

/** Determines if a bubble task is moving to fullscreen based on its windowing mode. */
fun isBubbleToFullscreen(task: ActivityManager.RunningTaskInfo?): Boolean {
    return BubbleAnythingFlagHelper.enableCreateAnyBubbleWithForceExcludedFromRecents()
            && task?.windowingMode == WINDOWING_MODE_FULLSCREEN
}
+5 −1
Original line number Diff line number Diff line
@@ -100,7 +100,11 @@ class BubbleTaskView(val taskView: TaskView, executor: Executor) {
     */
    fun cleanup() {
        if (Flags.enableTaskViewControllerCleanup() || taskId != INVALID_TASK_ID) {
            if (isBubbleToFullscreen(taskView.taskInfo)) {
                taskView.unregisterTask()
            } else {
                taskView.removeTask()
            }
        }
    }
}
+7 −0
Original line number Diff line number Diff line
@@ -241,6 +241,13 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback,
        mTaskViewController.removeTaskView(mTaskViewTaskController, null /* token */);
    }

    /**
     * Call to unregister the task from the controller.
     */
    public void unregisterTask() {
        mTaskViewController.unregisterTaskView(mTaskViewTaskController);
    }

    /**
     * Release this container if it is initialized.
     */
Loading