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

Commit fa48c3c2 authored by Ats Jenk's avatar Ats Jenk
Browse files

Convert bubbled tasks to fullscreen after restart

In ShellCrashHandler add a check for tasks retaining bubble properties
after WMShell restarts.
If we find such tasks, exit then from bubbles. As after a restart we
lose information about these tasks in WMShell and they no longer have a
TaskView.

Create a NoOpTransitionHandler for cases where we do not want to animate
the transition at all, but just finish it immediately.

Bug: 425496840
Test: atest WMShellUnitTests
Flag: com.android.wm.shell.enable_shell_restart_bubble_cleanup
Change-Id: I6347c957796bd72ad3f5d502bc5f5ac302e0a8a1
parent 1dfaa3ec
Loading
Loading
Loading
Loading
+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
+44 −7
Original line number Diff line number Diff line
@@ -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 {
@@ -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()) {
@@ -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())
        }
    }
}
+4 −2
Original line number Diff line number Diff line
@@ -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
+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
    }
}
+46 −14
Original line number Diff line number Diff line
@@ -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
@@ -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
@@ -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) {
@@ -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