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

Commit 4bb57ee2 authored by Eric Lin's avatar Eric Lin Committed by Android (Google) Code Review
Browse files

Merge "Enable bubble'd app task to fullscreen transition." into main

parents 44cdf168 132e558b
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