Loading libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskStackListenerTest.kt +94 −2 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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( Loading @@ -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() { Loading @@ -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 { Loading @@ -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) } } libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewTest.kt +67 −26 Original line number Diff line number Diff line Loading @@ -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) Loading @@ -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, ) } } libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskStackListener.kt +73 −3 Original line number Diff line number Diff line Loading @@ -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. Loading @@ -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( Loading @@ -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, Loading @@ -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 } libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskView.kt +5 −1 Original line number Diff line number Diff line Loading @@ -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() } } } } libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java +7 −0 Original line number Diff line number Diff line Loading @@ -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 Loading
libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskStackListenerTest.kt +94 −2 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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( Loading @@ -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() { Loading @@ -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 { Loading @@ -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) } }
libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewTest.kt +67 −26 Original line number Diff line number Diff line Loading @@ -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) Loading @@ -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, ) } }
libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskStackListener.kt +73 −3 Original line number Diff line number Diff line Loading @@ -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. Loading @@ -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( Loading @@ -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, Loading @@ -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 }
libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskView.kt +5 −1 Original line number Diff line number Diff line Loading @@ -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() } } } }
libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java +7 −0 Original line number Diff line number Diff line Loading @@ -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