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

Commit 3922d5a4 authored by Massimo Carli's avatar Massimo Carli Committed by Android (Google) Code Review
Browse files

Merge "[75/n] Implement RoundedCornersLetterboxController" into main

parents 4bec9bee df036f2b
Loading
Loading
Loading
Loading
+99 −0
Original line number Original line 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.roundedcorners

import android.graphics.Rect
import android.view.SurfaceControl
import android.window.WindowContainerToken
import com.android.internal.protolog.ProtoLog
import com.android.wm.shell.common.ShellExecutor
import com.android.wm.shell.compatui.letterbox.LetterboxController
import com.android.wm.shell.compatui.letterbox.LetterboxKey
import com.android.wm.shell.compatui.letterbox.LetterboxUtils.Maps.runOnItem
import com.android.wm.shell.compatui.letterbox.state.LetterboxTaskInfoRepository
import com.android.wm.shell.dagger.WMSingleton
import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_APP_COMPAT
import com.android.wm.shell.shared.annotations.ShellMainThread
import javax.inject.Inject

/** [LetterboxController] implementation to handle rounded corners for Letterbox in Shell. */
@WMSingleton
class RoundedCornersLetterboxController
@Inject
constructor(
    @ShellMainThread private val animExecutor: ShellExecutor,
    private val roundedCornersSurfaceBuilder: RoundedCornersSurfaceBuilder,
    private val taskRepository: LetterboxTaskInfoRepository,
) : LetterboxController {

    companion object {
        @JvmStatic private val TAG = "RoundedCornersLetterboxController"
    }

    private val roundedCornersMap = mutableMapOf<Int, RoundedCornersSurface>()

    override fun createLetterboxSurface(
        key: LetterboxKey,
        transaction: SurfaceControl.Transaction,
        parentLeash: SurfaceControl,
        token: WindowContainerToken?,
    ) {
        roundedCornersMap.runOnItem(
            key.taskId,
            onMissed = { k, m ->
                taskRepository.find(key.taskId)?.let { item ->
                    m[k] = roundedCornersSurfaceBuilder.create(item.configuration, parentLeash)
                }
            },
        )
    }

    override fun destroyLetterboxSurface(
        key: LetterboxKey,
        transaction: SurfaceControl.Transaction,
    ) {
        roundedCornersMap.runOnItem(key.taskId, onFound = { item -> item.release() })
        roundedCornersMap.remove(key.taskId)
    }

    override fun updateLetterboxSurfaceVisibility(
        key: LetterboxKey,
        transaction: SurfaceControl.Transaction,
        visible: Boolean,
    ) {
        roundedCornersMap.runOnItem(
            key.taskId,
            onFound = { item -> item.setCornersVisibility(animExecutor, visible, immediate = true) },
        )
    }

    override fun updateLetterboxSurfaceBounds(
        key: LetterboxKey,
        transaction: SurfaceControl.Transaction,
        taskBounds: Rect,
        activityBounds: Rect,
    ) {
        roundedCornersMap.runOnItem(
            key.taskId,
            onFound = { item -> item.updateSurfaceBounds(activityBounds) },
        )
    }

    override fun dump() {
        ProtoLog.v(WM_SHELL_APP_COMPAT, "%s: %s", TAG, "$roundedCornersMap")
    }
}
+2 −3
Original line number Original line Diff line number Diff line
@@ -36,7 +36,6 @@ import android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
import android.view.WindowlessWindowManager
import android.view.WindowlessWindowManager
import android.widget.FrameLayout
import android.widget.FrameLayout
import android.window.TaskConstants
import android.window.TaskConstants
import android.window.TaskConstants.TASK_CHILD_SHELL_LAYER_LETTERBOX_ROUNDED_CORNERS
import com.android.wm.shell.R
import com.android.wm.shell.R
import com.android.wm.shell.common.ShellExecutor
import com.android.wm.shell.common.ShellExecutor
import com.android.wm.shell.common.SyncTransactionQueue
import com.android.wm.shell.common.SyncTransactionQueue
@@ -56,7 +55,7 @@ class RoundedCornersSurface(
    private val cornersFactory: LetterboxRoundedCornersDrawableFactory,
    private val cornersFactory: LetterboxRoundedCornersDrawableFactory,
    private val letterboxConfiguration: LetterboxConfiguration,
    private val letterboxConfiguration: LetterboxConfiguration,
    private val surfaceBuilderSupplier: SurfaceBuilderSupplier,
    private val surfaceBuilderSupplier: SurfaceBuilderSupplier,
) : WindowlessWindowManager(config, parentSurface, /* hostInputToken */ null) {
) : WindowlessWindowManager(config, parentSurface, null) {


    private var viewHost: SurfaceControlViewHost? = null
    private var viewHost: SurfaceControlViewHost? = null


@@ -90,7 +89,7 @@ class RoundedCornersSurface(
            surfaceBuilderSupplier
            surfaceBuilderSupplier
                .get()
                .get()
                .setContainerLayer()
                .setContainerLayer()
                .setName(className + "Leash")
                .setName("LetterboxRoundedCornersParentSurface")
                .setHidden(false)
                .setHidden(false)
                .setParent(parentSurface)
                .setParent(parentSurface)
                .setCallsite("$className#attachToParentSurface")
                .setCallsite("$className#attachToParentSurface")
+50 −0
Original line number Original line 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.roundedcorners

import android.content.Context
import android.content.res.Configuration
import android.view.SurfaceControl
import com.android.wm.shell.common.SyncTransactionQueue
import com.android.wm.shell.common.suppliers.SurfaceBuilderSupplier
import com.android.wm.shell.compatui.letterbox.LetterboxConfiguration
import com.android.wm.shell.dagger.WMSingleton
import javax.inject.Inject

/** Component responsible for the actual creation of the RoundedCorners surfaces. */
@WMSingleton
class RoundedCornersSurfaceBuilder
@Inject
constructor(
    private val ctx: Context,
    private val syncQueue: SyncTransactionQueue,
    private val surfaceBuilderSupplier: SurfaceBuilderSupplier,
    private val roundedCornersFactory: LetterboxRoundedCornersDrawableFactory,
    private val letterboxConfiguration: LetterboxConfiguration,
) {

    fun create(conf: Configuration, parentLeash: SurfaceControl): RoundedCornersSurface =
        RoundedCornersSurface(
            ctx,
            conf,
            syncQueue,
            parentLeash,
            roundedCornersFactory,
            letterboxConfiguration,
            surfaceBuilderSupplier,
        )
}
+1 −1
Original line number Original line Diff line number Diff line
@@ -43,7 +43,7 @@ abstract class LetterboxControllerRobotTest {
        val ANOTHER_TASK_ID = 10
        val ANOTHER_TASK_ID = 10


        @JvmStatic
        @JvmStatic
        private val TOKEN = mock<WindowContainerToken>()
        val TOKEN = mock<WindowContainerToken>()
    }
    }


    lateinit var letterboxController: LetterboxController
    lateinit var letterboxController: LetterboxController
+206 −0
Original line number Original line 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.roundedcorners

import android.content.res.Configuration
import android.graphics.Rect
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.TestShellExecutor
import com.android.wm.shell.compatui.letterbox.LetterboxController
import com.android.wm.shell.compatui.letterbox.LetterboxControllerRobotTest
import com.android.wm.shell.compatui.letterbox.LetterboxControllerRobotTest.Companion.ANOTHER_TASK_ID
import com.android.wm.shell.compatui.letterbox.state.LetterboxTaskInfoRepository
import com.android.wm.shell.compatui.letterbox.state.LetterboxTaskInfoState
import java.util.function.Consumer
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.any
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.times
import org.mockito.kotlin.verify

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

    @Test
    fun `Rounded corners are NOT created if configuration is missing from the repository`() {
        runTestScenario { r ->
            r.sendCreateSurfaceRequest()

            r.checkSurfaceBuilderInvoked(times = 0)
        }
    }

    @Test
    fun `Rounded corners are created if configuration is missing from the repository`() {
        runTestScenario { r ->
            r.configureRepository()
            r.sendCreateSurfaceRequest()

            r.checkSurfaceBuilderInvoked(times = 1)
        }
    }

    @Test
    fun `When creation is requested multiple times rounded corners are created once`() {
        runTestScenario { r ->
            r.configureRepository()
            r.sendCreateSurfaceRequest()
            r.sendCreateSurfaceRequest()
            r.sendCreateSurfaceRequest()
            r.sendCreateSurfaceRequest()

            r.checkSurfaceBuilderInvoked(times = 1)
        }
    }

    @Test
    fun `A different surface is created for every key`() {
        runTestScenario { r ->
            r.configureRepository()
            r.configureRepository(taskId = ANOTHER_TASK_ID)
            r.sendCreateSurfaceRequest()
            r.sendCreateSurfaceRequest()
            r.sendCreateSurfaceRequest(taskId = ANOTHER_TASK_ID)
            r.sendCreateSurfaceRequest(taskId = ANOTHER_TASK_ID)

            r.checkSurfaceBuilderInvoked(times = 2)
        }
    }

    @Test
    fun `Created surface is removed once`() {
        runTestScenario { r ->
            r.configureRepository()
            r.sendCreateSurfaceRequest()
            r.checkSurfaceBuilderInvoked()

            r.sendDestroySurfaceRequest()
            r.sendDestroySurfaceRequest()
            r.sendDestroySurfaceRequest()

            r.checkRoundedCornersSurfaceReleased()
        }
    }

    @Test
    fun `Only existing surfaces receive visibility update`() {
        runTestScenario { r ->
            r.configureRepository()
            r.configureRepository(taskId = ANOTHER_TASK_ID)
            r.sendCreateSurfaceRequest()
            r.sendUpdateSurfaceVisibilityRequest(visible = true)
            r.sendUpdateSurfaceVisibilityRequest(visible = true, taskId = ANOTHER_TASK_ID)

            r.checkRoundedCornersVisibilityUpdated(times = 1, expectedVisibility = true)
        }
    }

    @Test
    fun `Only existing surfaces receive taskBounds update`() {
        runTestScenario { r ->
            r.configureRepository()
            r.sendUpdateSurfaceBoundsRequest(
                taskBounds = Rect(0, 0, 2000, 1000),
                activityBounds = Rect(500, 0, 1500, 1000)
            )

            r.checkSurfacePositionUpdated(times = 0)
            r.checkSurfaceSizeUpdated(times = 0)

            r.resetTransitionTest()

            r.sendCreateSurfaceRequest()
            r.sendUpdateSurfaceBoundsRequest(
                taskBounds = Rect(0, 0, 2000, 1000),
                activityBounds = Rect(500, 0, 1500, 1000)
            )
        }
    }

    /**
     * Runs a test scenario providing a Robot.
     */
    fun runTestScenario(consumer: Consumer<RoundedCornersControllerRobotTest>) {
        consumer.accept(RoundedCornersControllerRobotTest().apply { initController() })
    }

    class RoundedCornersControllerRobotTest : LetterboxControllerRobotTest() {

        private val letterboxRepository: LetterboxTaskInfoRepository
        private val roundedCornersSurfaceBuilder: RoundedCornersSurfaceBuilder
        private val roundedCornersSurface: RoundedCornersSurface
        private val executor: TestShellExecutor
        private val testConfiguration: Configuration

        init {
            testConfiguration = Configuration()
            executor = TestShellExecutor()
            letterboxRepository = LetterboxTaskInfoRepository()
            roundedCornersSurface = mock<RoundedCornersSurface>()
            roundedCornersSurfaceBuilder = mock<RoundedCornersSurfaceBuilder>()
            doReturn(roundedCornersSurface).`when`(roundedCornersSurfaceBuilder).create(
                any(),
                any()
            )
        }

        override fun buildController(): LetterboxController =
            RoundedCornersLetterboxController(
                executor,
                roundedCornersSurfaceBuilder,
                letterboxRepository
            )

        fun configureRepository(taskId: Int = TASK_ID) {
            letterboxRepository.insert(
                taskId,
                LetterboxTaskInfoState(TOKEN, parentLeash, configuration = testConfiguration)
            )
        }

        fun checkSurfaceBuilderInvoked(times: Int = 1) {
            verify(roundedCornersSurfaceBuilder, times(times)).create(
                eq(testConfiguration),
                eq(parentLeash)
            )
        }

        fun checkRoundedCornersVisibilityUpdated(times: Int = 1, expectedVisibility: Boolean) {
            verify(roundedCornersSurface, times(times)).setCornersVisibility(
                any(),
                eq(expectedVisibility),
                eq(true)
            )
        }

        fun checkRoundedCornersSurfaceReleased(times: Int = 1) {
            verify(roundedCornersSurface, times(times)).release()
        }
    }
}