diff options
5 files changed, 453 insertions, 4 deletions
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxControllerRobotTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxControllerRobotTest.kt new file mode 100644 index 000000000000..f9f01bcd54f3 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxControllerRobotTest.kt @@ -0,0 +1,142 @@ +/* + * Copyright 2024 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 + +import android.content.Context +import android.graphics.Rect +import android.view.SurfaceControl +import com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn +import com.android.wm.shell.compatui.letterbox.LetterboxMatchers.asAnyMode +import org.mockito.kotlin.any +import org.mockito.kotlin.clearInvocations +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.times +import org.mockito.kotlin.verify + +/** + * Robot to test [LetterboxController] implementations. + */ +open class LetterboxControllerRobotTest( + ctx: Context, + controllerBuilder: (LetterboxSurfaceBuilder) -> LetterboxController +) { + + companion object { + @JvmStatic + private val DISPLAY_ID = 1 + + @JvmStatic + private val TASK_ID = 20 + } + + private val letterboxConfiguration: LetterboxConfiguration + private val surfaceBuilder: LetterboxSurfaceBuilder + private val letterboxController: LetterboxController + private val transaction: SurfaceControl.Transaction + private val parentLeash: SurfaceControl + + init { + letterboxConfiguration = LetterboxConfiguration(ctx) + surfaceBuilder = LetterboxSurfaceBuilder(letterboxConfiguration) + letterboxController = controllerBuilder(surfaceBuilder) + transaction = getTransactionMock() + parentLeash = mock<SurfaceControl>() + spyOn(surfaceBuilder) + } + + fun sendCreateSurfaceRequest( + displayId: Int = DISPLAY_ID, + taskId: Int = TASK_ID + ) = letterboxController.createLetterboxSurface( + key = LetterboxKey(displayId, taskId), + transaction = transaction, + parentLeash = parentLeash + ) + + fun sendDestroySurfaceRequest( + displayId: Int = DISPLAY_ID, + taskId: Int = TASK_ID + ) = letterboxController.destroyLetterboxSurface( + key = LetterboxKey(displayId, taskId), + transaction = transaction + ) + + fun sendUpdateSurfaceVisibilityRequest( + displayId: Int = DISPLAY_ID, + taskId: Int = TASK_ID, + visible: Boolean + ) = letterboxController.updateLetterboxSurfaceVisibility( + key = LetterboxKey(displayId, taskId), + transaction = transaction, + visible = visible + ) + + fun sendUpdateSurfaceBoundsRequest( + displayId: Int = DISPLAY_ID, + taskId: Int = TASK_ID, + taskBounds: Rect, + activityBounds: Rect + ) = letterboxController.updateLetterboxSurfaceBounds( + key = LetterboxKey(displayId, taskId), + transaction = transaction, + taskBounds = taskBounds, + activityBounds = activityBounds + ) + + fun checkSurfaceBuilderInvoked(times: Int = 1, name: String = "", callSite: String = "") { + verify(surfaceBuilder, times(times)).createSurface( + eq(transaction), + eq(parentLeash), + name.asAnyMode(), + callSite.asAnyMode(), + any() + ) + } + + fun checkTransactionRemovedInvoked(times: Int = 1) { + verify(transaction, times(times)).remove(any()) + } + + fun checkVisibilityUpdated(times: Int = 1, expectedVisibility: Boolean) { + verify(transaction, times(times)).setVisibility(any(), eq(expectedVisibility)) + } + + fun checkSurfacePositionUpdated( + times: Int = 1, + expectedX: Float = -1f, + expectedY: Float = -1f + ) { + verify(transaction, times(times)).setPosition( + any(), + expectedX.asAnyMode(), + expectedY.asAnyMode() + ) + } + + fun checkSurfaceSizeUpdated(times: Int = 1, expectedWidth: Int = -1, expectedHeight: Int = -1) { + verify(transaction, times(times)).setWindowCrop( + any(), + expectedWidth.asAnyMode(), + expectedHeight.asAnyMode() + ) + } + + fun resetTransitionTest() { + clearInvocations(transaction) + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxSurfaceBuilderTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxSurfaceBuilderTest.kt index 70341b6646c2..c37913e47cca 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxSurfaceBuilderTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxSurfaceBuilderTest.kt @@ -28,7 +28,6 @@ import org.junit.runner.RunWith import org.mockito.ArgumentMatchers import org.mockito.Mockito.verify import org.mockito.kotlin.anyOrNull -import org.mockito.kotlin.doReturn import org.mockito.kotlin.eq import org.mockito.kotlin.times @@ -85,9 +84,7 @@ class LetterboxSurfaceBuilderTest : ShellTestCase() { init { letterboxConfiguration = LetterboxConfiguration(ctx) letterboxSurfaceBuilder = LetterboxSurfaceBuilder(letterboxConfiguration) - tx = org.mockito.kotlin.mock<SurfaceControl.Transaction>() - doReturn(tx).`when`(tx).setLayer(anyOrNull(), anyOrNull()) - doReturn(tx).`when`(tx).setColorSpaceAgnostic(anyOrNull(), anyOrNull()) + tx = getTransactionMock() parentLeash = org.mockito.kotlin.mock<SurfaceControl>() surfaceBuilder = SurfaceControl.Builder() spyOn(surfaceBuilder) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxTestUtils.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxTestUtils.kt index 29ea09aee61d..2c06dfda7917 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxTestUtils.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxTestUtils.kt @@ -16,9 +16,36 @@ package com.android.wm.shell.compatui.letterbox +import android.view.SurfaceControl +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.times import org.mockito.verification.VerificationMode +/** + * @return A [SurfaceControl.Transaction] mock supporting chaining for some operations. Please + * add other operations if needed. + */ +fun getTransactionMock(): SurfaceControl.Transaction = mock<SurfaceControl.Transaction>().apply { + doReturn(this).`when`(this).setLayer(anyOrNull(), anyOrNull()) + doReturn(this).`when`(this).setColorSpaceAgnostic(anyOrNull(), anyOrNull()) + doReturn(this).`when`(this).setPosition(anyOrNull(), any(), any()) + doReturn(this).`when`(this).setWindowCrop(anyOrNull(), any(), any()) +} + // Utility to make verification mode depending on a [Boolean]. fun Boolean.asMode(): VerificationMode = if (this) times(1) else never() + +// Utility matchers to use for the main types as Mockito [VerificationMode]. +object LetterboxMatchers { + fun Int.asAnyMode() = asAnyMode { this < 0 } + fun Float.asAnyMode() = asAnyMode { this < 0f } + fun String.asAnyMode() = asAnyMode { this.isEmpty() } +} + +private inline fun <reified T : Any> T.asAnyMode(condition: () -> Boolean) = + (if (condition()) any() else eq(this)) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/MultiSurfaceLetterboxControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/MultiSurfaceLetterboxControllerTest.kt new file mode 100644 index 000000000000..295d4edf206b --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/MultiSurfaceLetterboxControllerTest.kt @@ -0,0 +1,155 @@ +/* + * Copyright 2024 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 + +import android.graphics.Rect +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.wm.shell.ShellTestCase +import java.util.function.Consumer +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Tests for [MultiSurfaceLetterboxController]. + * + * Build/Install/Run: + * atest WMShellUnitTests:MultiSurfaceLetterboxControllerTest + */ +@RunWith(AndroidTestingRunner::class) +@SmallTest +class MultiSurfaceLetterboxControllerTest : ShellTestCase() { + + @Test + fun `When creation is requested the surfaces are created if not present`() { + runTestScenario { r -> + r.sendCreateSurfaceRequest() + + r.checkSurfaceBuilderInvoked(times = 4) + } + } + + @Test + fun `When creation is requested multiple times the surfaces are created once`() { + runTestScenario { r -> + r.sendCreateSurfaceRequest() + r.sendCreateSurfaceRequest() + r.sendCreateSurfaceRequest() + r.sendCreateSurfaceRequest() + + r.checkSurfaceBuilderInvoked(times = 4) + } + } + + @Test + fun `Different surfaces are created for every key`() { + runTestScenario { r -> + r.sendCreateSurfaceRequest() + r.sendCreateSurfaceRequest() + r.sendCreateSurfaceRequest(displayId = 2) + r.sendCreateSurfaceRequest(displayId = 2, taskId = 2) + r.sendCreateSurfaceRequest(displayId = 2) + r.sendCreateSurfaceRequest(displayId = 2, taskId = 2) + + r.checkSurfaceBuilderInvoked(times = 12) + } + } + + @Test + fun `Created surface are removed once`() { + runTestScenario { r -> + r.sendCreateSurfaceRequest() + r.checkSurfaceBuilderInvoked(times = 4) + + r.sendDestroySurfaceRequest() + r.sendDestroySurfaceRequest() + r.sendDestroySurfaceRequest() + + r.checkTransactionRemovedInvoked(times = 4) + } + } + + @Test + fun `Only existing surfaces receive visibility update`() { + runTestScenario { r -> + r.sendCreateSurfaceRequest() + r.sendUpdateSurfaceVisibilityRequest(visible = true) + r.sendUpdateSurfaceVisibilityRequest(visible = true, displayId = 20) + + r.checkVisibilityUpdated(times = 4, expectedVisibility = true) + } + } + + @Test + fun `Only existing surfaces receive taskBounds update`() { + runTestScenario { r -> + r.sendUpdateSurfaceBoundsRequest( + taskBounds = Rect(0, 0, 2000, 1000), + activityBounds = Rect(500, 0, 1500, 1000) + ) + + r.checkSurfacePositionUpdated(times = 0) + r.checkSurfaceSizeUpdated(times = 0) + + r.sendCreateSurfaceRequest() + + // Pillarbox. + r.sendUpdateSurfaceBoundsRequest( + taskBounds = Rect(0, 0, 2000, 1000), + activityBounds = Rect(500, 0, 1500, 1000) + ) + // The Left and Top surfaces. + r.checkSurfacePositionUpdated(times = 2, expectedX = 0f, expectedY = 0f) + // The Right surface. + r.checkSurfacePositionUpdated(times = 1, expectedX = 1500f, expectedY = 0f) + // The Bottom surface. + r.checkSurfacePositionUpdated(times = 1, expectedX = 0f, expectedY = 1000f) + // Left and Right surface. + r.checkSurfaceSizeUpdated(times = 2, expectedWidth = 500, expectedHeight = 1000) + // Top and Button. + r.checkSurfaceSizeUpdated(times = 2, expectedWidth = 2000, expectedHeight = 0) + + r.resetTransitionTest() + + // Letterbox. + r.sendUpdateSurfaceBoundsRequest( + taskBounds = Rect(0, 0, 1000, 2000), + activityBounds = Rect(0, 500, 1000, 1500) + ) + // Top and Left surfaces. + r.checkSurfacePositionUpdated(times = 2, expectedX = 0f, expectedY = 0f) + // Bottom surface. + r.checkSurfacePositionUpdated(times = 1, expectedX = 0f, expectedY = 1500f) + // Right surface. + r.checkSurfacePositionUpdated(times = 1, expectedX = 1000f, expectedY = 0f) + + // Left and Right surfaces. + r.checkSurfaceSizeUpdated(times = 2, expectedWidth = 0, expectedHeight = 2000) + // Top and Button surfaces, + r.checkSurfaceSizeUpdated(times = 2, expectedWidth = 1000, expectedHeight = 500) + } + } + + /** + * Runs a test scenario providing a Robot. + */ + fun runTestScenario(consumer: Consumer<LetterboxControllerRobotTest>) { + val robot = + LetterboxControllerRobotTest(mContext, { sb -> MultiSurfaceLetterboxController(sb) }) + consumer.accept(robot) + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/SingleSurfaceLetterboxControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/SingleSurfaceLetterboxControllerTest.kt new file mode 100644 index 000000000000..125e700bcd42 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/SingleSurfaceLetterboxControllerTest.kt @@ -0,0 +1,128 @@ +/* + * Copyright 2024 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 + +import android.graphics.Rect +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.wm.shell.ShellTestCase +import java.util.function.Consumer +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Tests for [SingleSurfaceLetterboxController]. + * + * Build/Install/Run: + * atest WMShellUnitTests:SingleSurfaceLetterboxControllerTest + */ +@RunWith(AndroidTestingRunner::class) +@SmallTest +class SingleSurfaceLetterboxControllerTest : ShellTestCase() { + + @Test + fun `When creation is requested the surface is created if not present`() { + runTestScenario { r -> + r.sendCreateSurfaceRequest() + + r.checkSurfaceBuilderInvoked() + } + } + + @Test + fun `When creation is requested multiple times the surface is created once`() { + runTestScenario { r -> + 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.sendCreateSurfaceRequest() + r.sendCreateSurfaceRequest() + r.sendCreateSurfaceRequest(displayId = 2) + r.sendCreateSurfaceRequest(displayId = 2, taskId = 2) + r.sendCreateSurfaceRequest(displayId = 2) + r.sendCreateSurfaceRequest(displayId = 2, taskId = 2) + + r.checkSurfaceBuilderInvoked(times = 3) + } + } + + @Test + fun `Created surface is removed once`() { + runTestScenario { r -> + r.sendCreateSurfaceRequest() + r.checkSurfaceBuilderInvoked() + + r.sendDestroySurfaceRequest() + r.sendDestroySurfaceRequest() + r.sendDestroySurfaceRequest() + + r.checkTransactionRemovedInvoked() + } + } + + @Test + fun `Only existing surfaces receive visibility update`() { + runTestScenario { r -> + r.sendCreateSurfaceRequest() + r.sendUpdateSurfaceVisibilityRequest(visible = true) + r.sendUpdateSurfaceVisibilityRequest(visible = true, displayId = 20) + + r.checkVisibilityUpdated(expectedVisibility = true) + } + } + + @Test + fun `Only existing surfaces receive taskBounds update`() { + runTestScenario { r -> + 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) + ) + r.checkSurfacePositionUpdated(times = 1, expectedX = 0f, expectedY = 0f) + r.checkSurfaceSizeUpdated(times = 1, expectedWidth = 2000, expectedHeight = 1000) + } + } + + /** + * Runs a test scenario providing a Robot. + */ + fun runTestScenario(consumer: Consumer<LetterboxControllerRobotTest>) { + val robot = + LetterboxControllerRobotTest(mContext, { sb -> SingleSurfaceLetterboxController(sb) }) + consumer.accept(robot) + } +} |