Loading libs/WindowManager/Shell/src/com/android/wm/shell/crashhandling/OWNERS +1 −0 Original line number Diff line number Diff line # WM shell sub-module crash handling owners atsjenk@google.com uysalorhan@google.com No newline at end of file libs/WindowManager/Shell/src/com/android/wm/shell/crashhandling/ShellCrashHandler.kt +44 −7 Original line number Diff line number Diff line Loading @@ -18,20 +18,30 @@ package com.android.wm.shell.crashhandling import android.app.WindowConfiguration import android.view.Display.DEFAULT_DISPLAY import android.view.WindowManager import android.window.DesktopExperienceFlags import android.window.WindowContainerTransaction import com.android.wm.shell.Flags import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.bubbles.BubbleController import com.android.wm.shell.bubbles.util.BubbleUtils import com.android.wm.shell.common.HomeIntentProvider import com.android.wm.shell.shared.desktopmode.DesktopState import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.NoOpTransitionHandler import com.android.wm.shell.transition.Transitions import java.util.Optional /** [ShellCrashHandler] for shell to use when it's being initialized. Currently it only restores * the home task to top. **/ /** * [ShellCrashHandler] for shell to use when it's being initialized. Currently it only restores the * home task to top. */ class ShellCrashHandler( private val shellTaskOrganizer: ShellTaskOrganizer, private val transitions: Transitions, private val homeIntentProvider: HomeIntentProvider, private val desktopState: DesktopState, private val bubbleController: Optional<BubbleController>, shellInit: ShellInit, ) { init { Loading @@ -44,8 +54,10 @@ class ShellCrashHandler( private fun handleCrashIfNeeded() { // For now only handle crashes when desktop mode is enabled on the device. if (desktopState.canEnterDesktopMode && !DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { if ( desktopState.canEnterDesktopMode && !DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue ) { var freeformTaskExists = false // If there are running tasks at init, WMShell has crashed but WMCore is still alive. for (task in shellTaskOrganizer.getRunningTasks()) { Loading @@ -61,14 +73,39 @@ class ShellCrashHandler( } } } if (Flags.enableShellRestartBubbleCleanup()) { bubbleController.ifPresent { handleBubbleTaskCleanup(it) } } } private fun addLaunchHomePendingIntent( wct: WindowContainerTransaction, displayId: Int wct: WindowContainerTransaction, displayId: Int, ): WindowContainerTransaction { // TODO: b/400462917 - Check that crashes are also handled correctly on HSUM devices. We // might need to pass the [userId] here to launch the correct home. homeIntentProvider.addLaunchHomePendingIntent(wct, displayId) return wct } /** * Cleans up any existing bubble tasks by removing bubble specific overrides. * After cleanup, the device will be transitioned to the home screen. */ private fun handleBubbleTaskCleanup(bc: BubbleController) { val wct = WindowContainerTransaction() for (task in shellTaskOrganizer.getRunningTasks()) { if (bc.shouldBeAppBubble(task)) { val exitWct = BubbleUtils.getExitBubbleTransaction(task.token, /* captionInsetsOwner= */ null) wct.merge(exitWct, /* transfer= */ true) } } if (!wct.isEmpty) { // Make sure we end up on the home screen addLaunchHomePendingIntent(wct, DEFAULT_DISPLAY) transitions.startTransition(WindowManager.TRANSIT_CHANGE, wct, NoOpTransitionHandler()) } } } libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +4 −2 Original line number Diff line number Diff line Loading @@ -2062,11 +2062,13 @@ public abstract class WMShellModule { @Provides static ShellCrashHandler provideShellCrashHandler( ShellTaskOrganizer shellTaskOrganizer, Transitions transitions, HomeIntentProvider homeIntentProvider, DesktopState desktopState, Optional<BubbleController> bubbleController, ShellInit shellInit) { return new ShellCrashHandler(shellTaskOrganizer, homeIntentProvider, desktopState, shellInit); return new ShellCrashHandler(shellTaskOrganizer, transitions, homeIntentProvider, desktopState, bubbleController, shellInit); } @WMSingleton Loading libs/WindowManager/Shell/src/com/android/wm/shell/transition/NoOpTransitionHandler.kt 0 → 100644 +49 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.wm.shell.transition import android.os.IBinder import android.view.SurfaceControl import android.window.TransitionInfo import android.window.TransitionRequestInfo import android.window.WindowContainerTransaction /** * A [Transitions.TransitionHandler] that does nothing. * * Will report to handle the transition, but will not start any animations. Will immediately call * finish callback when animation starts. */ class NoOpTransitionHandler : Transitions.TransitionHandler { override fun handleRequest( transition: IBinder, request: TransitionRequestInfo, ): WindowContainerTransaction? { return WindowContainerTransaction() } override fun startAnimation( transition: IBinder, info: TransitionInfo, startTransaction: SurfaceControl.Transaction, finishTransaction: SurfaceControl.Transaction, finishCallback: Transitions.TransitionFinishCallback, ): Boolean { finishCallback.onTransitionFinished(null) return true } } libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/crashhandling/ShellCrashHandlerTest.kt +46 −14 Original line number Diff line number Diff line Loading @@ -19,30 +19,38 @@ package com.android.wm.shell.crashhandling import android.app.ActivityManager.RunningTaskInfo import android.app.PendingIntent import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW import android.content.ComponentName import android.content.Context import android.content.Intent import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.view.Display.DEFAULT_DISPLAY import android.window.IWindowContainerToken import android.window.WindowContainerToken import android.view.WindowManager.TRANSIT_CHANGE import android.window.WindowContainerTransaction import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_PENDING_INTENT import com.android.modules.utils.testing.ExtendedMockitoRule import com.android.window.flags.Flags import com.android.wm.shell.MockToken import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.ShellTestCase import com.android.wm.shell.bubbles.BubbleController import com.android.wm.shell.bubbles.util.BubbleTestUtils.verifyExitBubbleTransaction import com.android.wm.shell.common.HomeIntentProvider import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.shared.desktopmode.FakeDesktopState import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.NoOpTransitionHandler import com.android.wm.shell.transition.Transitions import com.google.common.truth.Truth.assertThat import java.util.Optional import kotlin.test.Test import org.junit.Before import org.junit.Rule import org.mockito.ArgumentCaptor import org.mockito.Mockito import org.mockito.kotlin.any import org.mockito.kotlin.eq import org.mockito.kotlin.isA import org.mockito.kotlin.mock import org.mockito.kotlin.spy import org.mockito.kotlin.verify Loading @@ -52,20 +60,19 @@ class ShellCrashHandlerTest : ShellTestCase() { @JvmField @Rule val extendedMockitoRule = ExtendedMockitoRule.Builder(this) .mockStatic(PendingIntent::class.java) .build()!! ExtendedMockitoRule.Builder(this).mockStatic(PendingIntent::class.java).build()!! private val testExecutor = mock<ShellExecutor>() private val context = mock<Context>() private val shellTaskOrganizer = mock<ShellTaskOrganizer>() private val desktopState = FakeDesktopState() private val transitions = mock<Transitions>() private val bubbleController = mock<BubbleController>() private lateinit var homeIntentProvider: HomeIntentProvider private lateinit var crashHandler: ShellCrashHandler private lateinit var shellInit: ShellInit @Before fun setup() { desktopState.canEnterDesktopMode = true Loading @@ -74,23 +81,48 @@ class ShellCrashHandlerTest : ShellTestCase() { shellInit = spy(ShellInit(testExecutor)) homeIntentProvider = HomeIntentProvider(context) crashHandler = ShellCrashHandler(shellTaskOrganizer, homeIntentProvider, desktopState, shellInit) crashHandler = ShellCrashHandler( shellTaskOrganizer, transitions, homeIntentProvider, desktopState, Optional.of(bubbleController), shellInit, ) } @Test @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun init_freeformTaskExists_sendsHomeIntent() { val wctCaptor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) whenever(shellTaskOrganizer.getRunningTasks()).thenReturn(arrayListOf(createTaskInfo(1))) val task = createTaskInfo(1) whenever(shellTaskOrganizer.getRunningTasks()).thenReturn(arrayListOf(task)) shellInit.init() verify(shellTaskOrganizer).applyTransaction( wctCaptor.capture() ) verify(shellTaskOrganizer).applyTransaction(wctCaptor.capture()) wctCaptor.value.assertPendingIntentAt(0, launchHomeIntent(DEFAULT_DISPLAY)) } @Test @EnableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_SHELL_RESTART_BUBBLE_CLEANUP) fun init_bubbleTaskExists_convertsToUndefined() { val bubbleTask = createTaskInfo(1, windowingMode = WINDOWING_MODE_MULTI_WINDOW) whenever(shellTaskOrganizer.getRunningTasks()).thenReturn(arrayListOf(bubbleTask)) whenever(bubbleController.shouldBeAppBubble(bubbleTask)).thenReturn(true) shellInit.init() val wctCaptor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) verify(transitions) .startTransition(eq(TRANSIT_CHANGE), wctCaptor.capture(), isA<NoOpTransitionHandler>()) val wct = wctCaptor.value verifyExitBubbleTransaction(wct, bubbleTask.token.asBinder()) wct.assertPendingIntentAt(wct.hierarchyOps.lastIndex, launchHomeIntent(DEFAULT_DISPLAY)) } private fun launchHomeIntent(displayId: Int): Intent { return Intent(Intent.ACTION_MAIN).apply { if (displayId != DEFAULT_DISPLAY) { Loading @@ -106,7 +138,7 @@ class ShellCrashHandlerTest : ShellTestCase() { taskId = id displayId = DEFAULT_DISPLAY configuration.windowConfiguration.windowingMode = windowingMode token = WindowContainerToken(Mockito.mock(IWindowContainerToken::class.java)) token = MockToken.token() baseIntent = Intent().apply { component = ComponentName("package", "component.name") } } Loading Loading
libs/WindowManager/Shell/src/com/android/wm/shell/crashhandling/OWNERS +1 −0 Original line number Diff line number Diff line # WM shell sub-module crash handling owners atsjenk@google.com uysalorhan@google.com No newline at end of file
libs/WindowManager/Shell/src/com/android/wm/shell/crashhandling/ShellCrashHandler.kt +44 −7 Original line number Diff line number Diff line Loading @@ -18,20 +18,30 @@ package com.android.wm.shell.crashhandling import android.app.WindowConfiguration import android.view.Display.DEFAULT_DISPLAY import android.view.WindowManager import android.window.DesktopExperienceFlags import android.window.WindowContainerTransaction import com.android.wm.shell.Flags import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.bubbles.BubbleController import com.android.wm.shell.bubbles.util.BubbleUtils import com.android.wm.shell.common.HomeIntentProvider import com.android.wm.shell.shared.desktopmode.DesktopState import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.NoOpTransitionHandler import com.android.wm.shell.transition.Transitions import java.util.Optional /** [ShellCrashHandler] for shell to use when it's being initialized. Currently it only restores * the home task to top. **/ /** * [ShellCrashHandler] for shell to use when it's being initialized. Currently it only restores the * home task to top. */ class ShellCrashHandler( private val shellTaskOrganizer: ShellTaskOrganizer, private val transitions: Transitions, private val homeIntentProvider: HomeIntentProvider, private val desktopState: DesktopState, private val bubbleController: Optional<BubbleController>, shellInit: ShellInit, ) { init { Loading @@ -44,8 +54,10 @@ class ShellCrashHandler( private fun handleCrashIfNeeded() { // For now only handle crashes when desktop mode is enabled on the device. if (desktopState.canEnterDesktopMode && !DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { if ( desktopState.canEnterDesktopMode && !DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue ) { var freeformTaskExists = false // If there are running tasks at init, WMShell has crashed but WMCore is still alive. for (task in shellTaskOrganizer.getRunningTasks()) { Loading @@ -61,14 +73,39 @@ class ShellCrashHandler( } } } if (Flags.enableShellRestartBubbleCleanup()) { bubbleController.ifPresent { handleBubbleTaskCleanup(it) } } } private fun addLaunchHomePendingIntent( wct: WindowContainerTransaction, displayId: Int wct: WindowContainerTransaction, displayId: Int, ): WindowContainerTransaction { // TODO: b/400462917 - Check that crashes are also handled correctly on HSUM devices. We // might need to pass the [userId] here to launch the correct home. homeIntentProvider.addLaunchHomePendingIntent(wct, displayId) return wct } /** * Cleans up any existing bubble tasks by removing bubble specific overrides. * After cleanup, the device will be transitioned to the home screen. */ private fun handleBubbleTaskCleanup(bc: BubbleController) { val wct = WindowContainerTransaction() for (task in shellTaskOrganizer.getRunningTasks()) { if (bc.shouldBeAppBubble(task)) { val exitWct = BubbleUtils.getExitBubbleTransaction(task.token, /* captionInsetsOwner= */ null) wct.merge(exitWct, /* transfer= */ true) } } if (!wct.isEmpty) { // Make sure we end up on the home screen addLaunchHomePendingIntent(wct, DEFAULT_DISPLAY) transitions.startTransition(WindowManager.TRANSIT_CHANGE, wct, NoOpTransitionHandler()) } } }
libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +4 −2 Original line number Diff line number Diff line Loading @@ -2062,11 +2062,13 @@ public abstract class WMShellModule { @Provides static ShellCrashHandler provideShellCrashHandler( ShellTaskOrganizer shellTaskOrganizer, Transitions transitions, HomeIntentProvider homeIntentProvider, DesktopState desktopState, Optional<BubbleController> bubbleController, ShellInit shellInit) { return new ShellCrashHandler(shellTaskOrganizer, homeIntentProvider, desktopState, shellInit); return new ShellCrashHandler(shellTaskOrganizer, transitions, homeIntentProvider, desktopState, bubbleController, shellInit); } @WMSingleton Loading
libs/WindowManager/Shell/src/com/android/wm/shell/transition/NoOpTransitionHandler.kt 0 → 100644 +49 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.wm.shell.transition import android.os.IBinder import android.view.SurfaceControl import android.window.TransitionInfo import android.window.TransitionRequestInfo import android.window.WindowContainerTransaction /** * A [Transitions.TransitionHandler] that does nothing. * * Will report to handle the transition, but will not start any animations. Will immediately call * finish callback when animation starts. */ class NoOpTransitionHandler : Transitions.TransitionHandler { override fun handleRequest( transition: IBinder, request: TransitionRequestInfo, ): WindowContainerTransaction? { return WindowContainerTransaction() } override fun startAnimation( transition: IBinder, info: TransitionInfo, startTransaction: SurfaceControl.Transaction, finishTransaction: SurfaceControl.Transaction, finishCallback: Transitions.TransitionFinishCallback, ): Boolean { finishCallback.onTransitionFinished(null) return true } }
libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/crashhandling/ShellCrashHandlerTest.kt +46 −14 Original line number Diff line number Diff line Loading @@ -19,30 +19,38 @@ package com.android.wm.shell.crashhandling import android.app.ActivityManager.RunningTaskInfo import android.app.PendingIntent import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW import android.content.ComponentName import android.content.Context import android.content.Intent import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.view.Display.DEFAULT_DISPLAY import android.window.IWindowContainerToken import android.window.WindowContainerToken import android.view.WindowManager.TRANSIT_CHANGE import android.window.WindowContainerTransaction import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_PENDING_INTENT import com.android.modules.utils.testing.ExtendedMockitoRule import com.android.window.flags.Flags import com.android.wm.shell.MockToken import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.ShellTestCase import com.android.wm.shell.bubbles.BubbleController import com.android.wm.shell.bubbles.util.BubbleTestUtils.verifyExitBubbleTransaction import com.android.wm.shell.common.HomeIntentProvider import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.shared.desktopmode.FakeDesktopState import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.NoOpTransitionHandler import com.android.wm.shell.transition.Transitions import com.google.common.truth.Truth.assertThat import java.util.Optional import kotlin.test.Test import org.junit.Before import org.junit.Rule import org.mockito.ArgumentCaptor import org.mockito.Mockito import org.mockito.kotlin.any import org.mockito.kotlin.eq import org.mockito.kotlin.isA import org.mockito.kotlin.mock import org.mockito.kotlin.spy import org.mockito.kotlin.verify Loading @@ -52,20 +60,19 @@ class ShellCrashHandlerTest : ShellTestCase() { @JvmField @Rule val extendedMockitoRule = ExtendedMockitoRule.Builder(this) .mockStatic(PendingIntent::class.java) .build()!! ExtendedMockitoRule.Builder(this).mockStatic(PendingIntent::class.java).build()!! private val testExecutor = mock<ShellExecutor>() private val context = mock<Context>() private val shellTaskOrganizer = mock<ShellTaskOrganizer>() private val desktopState = FakeDesktopState() private val transitions = mock<Transitions>() private val bubbleController = mock<BubbleController>() private lateinit var homeIntentProvider: HomeIntentProvider private lateinit var crashHandler: ShellCrashHandler private lateinit var shellInit: ShellInit @Before fun setup() { desktopState.canEnterDesktopMode = true Loading @@ -74,23 +81,48 @@ class ShellCrashHandlerTest : ShellTestCase() { shellInit = spy(ShellInit(testExecutor)) homeIntentProvider = HomeIntentProvider(context) crashHandler = ShellCrashHandler(shellTaskOrganizer, homeIntentProvider, desktopState, shellInit) crashHandler = ShellCrashHandler( shellTaskOrganizer, transitions, homeIntentProvider, desktopState, Optional.of(bubbleController), shellInit, ) } @Test @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun init_freeformTaskExists_sendsHomeIntent() { val wctCaptor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) whenever(shellTaskOrganizer.getRunningTasks()).thenReturn(arrayListOf(createTaskInfo(1))) val task = createTaskInfo(1) whenever(shellTaskOrganizer.getRunningTasks()).thenReturn(arrayListOf(task)) shellInit.init() verify(shellTaskOrganizer).applyTransaction( wctCaptor.capture() ) verify(shellTaskOrganizer).applyTransaction(wctCaptor.capture()) wctCaptor.value.assertPendingIntentAt(0, launchHomeIntent(DEFAULT_DISPLAY)) } @Test @EnableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_SHELL_RESTART_BUBBLE_CLEANUP) fun init_bubbleTaskExists_convertsToUndefined() { val bubbleTask = createTaskInfo(1, windowingMode = WINDOWING_MODE_MULTI_WINDOW) whenever(shellTaskOrganizer.getRunningTasks()).thenReturn(arrayListOf(bubbleTask)) whenever(bubbleController.shouldBeAppBubble(bubbleTask)).thenReturn(true) shellInit.init() val wctCaptor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) verify(transitions) .startTransition(eq(TRANSIT_CHANGE), wctCaptor.capture(), isA<NoOpTransitionHandler>()) val wct = wctCaptor.value verifyExitBubbleTransaction(wct, bubbleTask.token.asBinder()) wct.assertPendingIntentAt(wct.hierarchyOps.lastIndex, launchHomeIntent(DEFAULT_DISPLAY)) } private fun launchHomeIntent(displayId: Int): Intent { return Intent(Intent.ACTION_MAIN).apply { if (displayId != DEFAULT_DISPLAY) { Loading @@ -106,7 +138,7 @@ class ShellCrashHandlerTest : ShellTestCase() { taskId = id displayId = DEFAULT_DISPLAY configuration.windowConfiguration.windowingMode = windowingMode token = WindowContainerToken(Mockito.mock(IWindowContainerToken::class.java)) token = MockToken.token() baseIntent = Intent().apply { component = ComponentName("package", "component.name") } } Loading