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

Commit a4bde1d7 authored by Ats Jenk's avatar Ats Jenk Committed by Android (Google) Code Review
Browse files

Merge "Convert bubbled tasks to fullscreen after restart" into main

parents 3560481e fa48c3c2
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