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

Commit 5acc4065 authored by Massimo Carli's avatar Massimo Carli
Browse files

[36/n] Implement LetterboxLifecycleController

We decouple the logic related to TransitionObserver to the one
related to the lifecycle of the letterbox surfaces in Shell.
This will make easier to integrate the same logic with other
entry points (e.g. Listeners)

Flag: com.android.window.flags.app_compat_refactoring
Bug: 409951586
Test: atest WMShellUnitTests:LetterboxLifecycleControllerImplTest
Test: atest WMShellUnitTests:LetterboxLifecycleEventTest

Change-Id: Id0bfef10b7f549f112f8266573c62413cee25c8b
parent 2752ad14
Loading
Loading
Loading
Loading
+39 −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.compatui.letterbox.lifecycle

import android.view.SurfaceControl

/**
 * Abstract a component that handles the lifecycle of the letterbox surfaces given
 * information encapsulated in a [LetterboxLifecycleEvent].
 */
interface LetterboxLifecycleController {

    /**
     * Describes how [LetterboxLifecycleEvent]s interact with the Letterbox surfaces lifecycle.
     * <p/>
     * @param event The [LetterboxLifecycleEvent] To handle.
     * @param startTransaction The initial [Transaction].
     * @param finishTransaction The final [Transaction].
     */
    fun onLetterboxLifecycleEvent(
        event: LetterboxLifecycleEvent,
        startTransaction: SurfaceControl.Transaction,
        finishTransaction: SurfaceControl.Transaction
    )
}
 No newline at end of file
+76 −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.compatui.letterbox.lifecycle

import android.view.SurfaceControl
import com.android.wm.shell.common.transition.TransitionStateHolder
import com.android.wm.shell.compatui.letterbox.LetterboxController
import com.android.wm.shell.compatui.letterbox.LetterboxControllerStrategy

/**
 * [LetterboxLifecycleController] default implementation.
 */
class LetterboxLifecycleControllerImpl(
    private val letterboxController: LetterboxController,
    private val transitionStateHolder: TransitionStateHolder,
    private val letterboxModeStrategy: LetterboxControllerStrategy
) : LetterboxLifecycleController {

    override fun onLetterboxLifecycleEvent(
        event: LetterboxLifecycleEvent,
        startTransaction: SurfaceControl.Transaction,
        finishTransaction: SurfaceControl.Transaction
    ) {
        val key = event.letterboxKey()
        with(letterboxController) {
            when (event.type) {
                LetterboxLifecycleEventType.CLOSE -> {
                    if (!transitionStateHolder.isRecentsTransitionRunning()) {
                        // For the other types of close we need to check recents.
                        destroyLetterboxSurface(key, finishTransaction)
                    }
                }
                else -> {
                    if (event.letterboxBounds != null) {
                        // In this case the top Activity is letterboxed.
                        letterboxModeStrategy.configureLetterboxMode()
                        event.letterboxActivityLeash?.let { leash ->
                            createLetterboxSurface(
                                key,
                                startTransaction,
                                leash,
                                event.letterboxActivityToken
                            )
                        }
                        updateLetterboxSurfaceBounds(
                            key,
                            startTransaction,
                            event.taskBounds,
                            event.letterboxBounds
                        )
                    } else {
                        updateLetterboxSurfaceVisibility(
                            key,
                            startTransaction,
                            visible = false
                        )
                    }
                }
            }
        }
    }
}
 No newline at end of file
+87 −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.compatui.letterbox.lifecycle

import android.graphics.Rect
import android.view.SurfaceControl
import android.window.TransitionInfo.Change
import android.window.WindowContainerToken
import com.android.wm.shell.compatui.letterbox.LetterboxKey
import com.android.wm.shell.compatui.letterbox.lifecycle.LetterboxLifecycleEventType.CLOSE
import com.android.wm.shell.compatui.letterbox.lifecycle.LetterboxLifecycleEventType.NONE
import com.android.wm.shell.compatui.letterbox.lifecycle.LetterboxLifecycleEventType.OPEN
import com.android.wm.shell.shared.TransitionUtil.isClosingType
import com.android.wm.shell.shared.TransitionUtil.isOpeningType

enum class LetterboxLifecycleEventType {
    NONE,
    OPEN,
    CLOSE
}

/**
 * Encapsulate all the information required by a [LetterboxLifecycleController]
 */
data class LetterboxLifecycleEvent(
    val type: LetterboxLifecycleEventType = NONE,
    val taskId: Int,
    val displayId: Int,
    val taskBounds: Rect,
    val letterboxBounds: Rect? = null,
    val letterboxActivityToken: WindowContainerToken? = null,
    val letterboxActivityLeash: SurfaceControl? = null,
)

/**
 * Extract the [LetterboxKey] from the [LetterboxLifecycleEvent].
 */
fun LetterboxLifecycleEvent.letterboxKey(): LetterboxKey =
    LetterboxKey(displayId = displayId, taskId = taskId)


/**
 * Creates a [LetterboxLifecycleEvent] from the information in a [Change].
 */
fun Change.toLetterboxLifecycleEvent(): LetterboxLifecycleEvent {
    val taskBounds = Rect(
        endRelOffset.x,
        endRelOffset.y,
        endAbsBounds.width(),
        endAbsBounds.height()
    )

    val type = when {
        isClosingType(mode) -> CLOSE
        isOpeningType(mode) -> OPEN
        else -> NONE
    }

    val isLetterboxed = taskInfo?.appCompatTaskInfo?.isTopActivityLetterboxed ?: false
    // Letterbox bounds are null when the activity is not letterboxed.
    val letterboxBounds =
        if (isLetterboxed) taskInfo?.appCompatTaskInfo?.topActivityLetterboxBounds else null

    return LetterboxLifecycleEvent(
        type = type,
        displayId = taskInfo?.displayId ?: -1,
        taskId = taskInfo?.taskId ?: -1,
        taskBounds = taskBounds,
        letterboxBounds = letterboxBounds,
        letterboxActivityToken = taskInfo?.token,
        letterboxActivityLeash = leash
    )
}
+276 −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.compatui.letterbox.lifecycle

import android.graphics.Rect
import android.testing.AndroidTestingRunner
import android.view.SurfaceControl
import android.view.SurfaceControl.Transaction
import android.window.WindowContainerToken
import androidx.test.filters.SmallTest
import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.common.transition.TransitionStateHolder
import com.android.wm.shell.compatui.letterbox.LetterboxController
import com.android.wm.shell.compatui.letterbox.LetterboxControllerStrategy
import com.android.wm.shell.compatui.letterbox.LetterboxKey
import com.android.wm.shell.compatui.letterbox.asMode
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import java.util.function.Consumer

/**
 * Tests for [LetterboxLifecycleControllerImpl].
 *
 * Build/Install/Run:
 *  atest WMShellUnitTests:LetterboxLifecycleControllerImplTest
 */
@RunWith(AndroidTestingRunner::class)
@SmallTest
class LetterboxLifecycleControllerImplTest : ShellTestCase() {

    @Test
    fun `this is my test`() {
        runTestScenario { r ->
            r.invokeLifecycleControllerWith(
                r.createLifecycleEvent()
            )
        }
    }

    @Test
    fun `Letterbox surfaces is destroyed when CLOSE and isRecentsTransitionRunning is false`() {
        runTestScenario { r ->
            r.configureIsRecentsTransitionRunning(running = false)
            r.invokeLifecycleControllerWith(
                r.createLifecycleEvent(
                    type = LetterboxLifecycleEventType.CLOSE
                )
            )
            r.verifyDestroyLetterboxSurface(expected = true)
        }
    }

    @Test
    fun `Letterbox surfaces is NOT destroyed when CLOSE and isRecentsTransitionRunning is true`() {
        runTestScenario { r ->
            r.configureIsRecentsTransitionRunning(running = true)
            r.invokeLifecycleControllerWith(
                r.createLifecycleEvent(
                    type = LetterboxLifecycleEventType.CLOSE
                )
            )
            r.verifyDestroyLetterboxSurface(expected = false)
        }
    }

    @Test
    fun `Letterbox is hidden with OPEN Transition but not letterboxed`() {
        runTestScenario { r ->
            r.invokeLifecycleControllerWith(
                r.createLifecycleEvent(
                    type = LetterboxLifecycleEventType.OPEN
                )
            )
            r.verifyUpdateLetterboxSurfaceVisibility(expected = true)
        }
    }

    @Test
    fun `Surface created with OPEN Transition and letterboxed with leash`() {
        runTestScenario { r ->
            r.invokeLifecycleControllerWith(
                r.createLifecycleEvent(
                    type = LetterboxLifecycleEventType.OPEN,
                    letterboxBounds = Rect(500, 0, 800, 1800)
                )
            )
            r.verifyCreateLetterboxSurface(expected = true)
        }
    }

    @Test
    fun `Surface NOT created with OPEN Transition and letterboxed with NO leash`() {
        runTestScenario { r ->
            r.invokeLifecycleControllerWith(
                r.createLifecycleEvent(
                    type = LetterboxLifecycleEventType.OPEN,
                    letterboxBounds = Rect(500, 0, 800, 1800),
                    letterboxActivityLeash = null
                )
            )
            r.verifyCreateLetterboxSurface(expected = false)
        }
    }

    @Test
    fun `Surface Bounds updated with OPEN Transition and letterboxed`() {
        runTestScenario { r ->
            r.invokeLifecycleControllerWith(
                r.createLifecycleEvent(
                    type = LetterboxLifecycleEventType.OPEN,
                    letterboxBounds = Rect(500, 0, 800, 1800),
                    letterboxActivityLeash = null
                )
            )
            r.verifyUpdateLetterboxSurfaceBounds(
                expected = true,
                letterboxBounds = Rect(500, 0, 800, 1800)
            )
        }
    }

    /**
     * Runs a test scenario providing a Robot.
     */
    fun runTestScenario(consumer: Consumer<LetterboxLifecycleControllerImplRobotTest>) {
        val robot = LetterboxLifecycleControllerImplRobotTest()
        consumer.accept(robot)
    }

    class LetterboxLifecycleControllerImplRobotTest {

        private val lifecycleController: LetterboxLifecycleControllerImpl
        private val letterboxController: LetterboxController
        private val transitionStateHolder: TransitionStateHolder
        private val letterboxModeStrategy: LetterboxControllerStrategy
        private val startTransaction: Transaction
        private val finishTransaction: Transaction
        private val token: WindowContainerToken
        private val leash: SurfaceControl

        companion object {
            @JvmStatic
            private val DISPLAY_ID = 1

            @JvmStatic
            private val TASK_ID = 20

            @JvmStatic
            private val TASK_BOUNDS = Rect(0, 0, 2800, 1400)
        }

        init {
            letterboxController = mock<LetterboxController>()
            transitionStateHolder = mock<TransitionStateHolder>()
            letterboxModeStrategy = mock<LetterboxControllerStrategy>()
            startTransaction = mock<Transaction>()
            finishTransaction = mock<Transaction>()
            token = mock<WindowContainerToken>()
            leash = mock<SurfaceControl>()
            lifecycleController = LetterboxLifecycleControllerImpl(
                letterboxController,
                transitionStateHolder,
                letterboxModeStrategy
            )
        }

        fun createLifecycleEvent(
            type: LetterboxLifecycleEventType = LetterboxLifecycleEventType.NONE,
            displayId: Int = DISPLAY_ID,
            taskId: Int = TASK_ID,
            taskBounds: Rect = TASK_BOUNDS,
            letterboxBounds: Rect? = null,
            letterboxActivityToken: WindowContainerToken = token,
            letterboxActivityLeash: SurfaceControl? = leash
        ): LetterboxLifecycleEvent = LetterboxLifecycleEvent(
            type = type,
            displayId = displayId,
            taskId = taskId,
            taskBounds = taskBounds,
            letterboxBounds = letterboxBounds,
            letterboxActivityToken = letterboxActivityToken,
            letterboxActivityLeash = letterboxActivityLeash
        )

        fun configureIsRecentsTransitionRunning(running: Boolean) {
            doReturn(running).`when`(transitionStateHolder).isRecentsTransitionRunning()
        }

        fun invokeLifecycleControllerWith(event: LetterboxLifecycleEvent) {
            lifecycleController.onLetterboxLifecycleEvent(
                event,
                startTransaction,
                finishTransaction
            )
        }

        fun verifyDestroyLetterboxSurface(
            expected: Boolean,
            displayId: Int = DISPLAY_ID,
            taskId: Int = TASK_ID
        ) {
            verify(
                letterboxController,
                expected.asMode()
            ).destroyLetterboxSurface(eq(LetterboxKey(displayId, taskId)), eq(finishTransaction))
        }

        fun verifyCreateLetterboxSurface(
            expected: Boolean,
            displayId: Int = DISPLAY_ID,
            taskId: Int = TASK_ID
        ) {
            verify(
                letterboxController,
                expected.asMode()
            ).createLetterboxSurface(
                eq(LetterboxKey(displayId, taskId)),
                eq(startTransaction),
                eq(leash),
                eq(token)
            )
        }

        fun verifyUpdateLetterboxSurfaceVisibility(
            expected: Boolean,
            displayId: Int = DISPLAY_ID,
            taskId: Int = TASK_ID
        ) {
            verify(
                letterboxController,
                expected.asMode()
            ).updateLetterboxSurfaceVisibility(
                eq(LetterboxKey(displayId, taskId)),
                eq(startTransaction),
                eq(false)
            )
        }

        fun verifyUpdateLetterboxSurfaceBounds(
            expected: Boolean,
            displayId: Int = DISPLAY_ID,
            taskId: Int = TASK_ID,
            taskBounds: Rect = TASK_BOUNDS,
            letterboxBounds: Rect,

            ) {
            verify(
                letterboxController,
                expected.asMode()
            ).updateLetterboxSurfaceBounds(
                eq(LetterboxKey(displayId, taskId)),
                eq(startTransaction),
                eq(taskBounds),
                eq(letterboxBounds)
            )
        }
    }
}
 No newline at end of file
+264 −0

File added.

Preview size limit exceeded, changes collapsed.