diff options
| author | 2024-11-05 18:53:18 +0000 | |
|---|---|---|
| committer | 2024-11-14 11:21:25 +0000 | |
| commit | 4b92f869cffe6a471f7a7aa765344cb8764b40ef (patch) | |
| tree | 09bc90696948a383ff9567fa3b8719c0e7682e30 | |
| parent | aad007bac5cee46f5260ba5f218a3729643ea62f (diff) | |
[4/n] Implement LetterboxObservable Tests
Implement a Type Safe Builder for testing TransitionObservable and
use it for testing LetterboxObservable.
Flag: com.android.window.flags.app_compat_refactoring
Bug: 370997904
Test: atest WMShellUnitTests:LetterboxTransitionObserverTest
Change-Id: Ib36170c1ca621f37dd717686aa41b4256d417f98
2 files changed, 476 insertions, 0 deletions
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxTransitionObserverTest.kt new file mode 100644 index 000000000000..1ae1c3fc4563 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxTransitionObserverTest.kt @@ -0,0 +1,294 @@ +/* + * 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.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import android.testing.AndroidTestingRunner +import android.view.WindowManager.TRANSIT_CLOSE +import androidx.test.filters.SmallTest +import com.android.window.flags.Flags +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.common.ShellExecutor +import com.android.wm.shell.sysui.ShellInit +import com.android.wm.shell.transition.Transitions +import com.android.wm.shell.util.TransitionObserverInputBuilder +import com.android.wm.shell.util.executeTransitionObserverTest +import java.util.function.Consumer +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito +import org.mockito.kotlin.any +import org.mockito.kotlin.eq +import org.mockito.kotlin.never +import org.mockito.kotlin.times +import org.mockito.kotlin.anyOrNull +import org.mockito.verification.VerificationMode + +/** + * Tests for [LetterboxTransitionObserver]. + * + * Build/Install/Run: + * atest WMShellUnitTests:LetterboxTransitionObserverTest + */ +@RunWith(AndroidTestingRunner::class) +@SmallTest +class LetterboxTransitionObserverTest : ShellTestCase() { + + @get:Rule + val setFlagsRule: SetFlagsRule = SetFlagsRule() + + @Test + @DisableFlags(Flags.FLAG_APP_COMPAT_REFACTORING) + fun `when initialized and flag disabled the observer is not registered`() { + runTestScenario { r -> + executeTransitionObserverTest(observerFactory = r.observerFactory) { + r.invokeShellInit() + r.checkObservableIsRegistered(expected = false) + } + } + } + + @Test + @EnableFlags(Flags.FLAG_APP_COMPAT_REFACTORING) + fun `when initialized and flag enabled the observer is registered`() { + runTestScenario { r -> + executeTransitionObserverTest(observerFactory = r.observerFactory) { + r.invokeShellInit() + r.checkObservableIsRegistered(expected = true) + } + } + } + + @Test + fun `LetterboxController not used without TaskInfos in Change`() { + runTestScenario { r -> + executeTransitionObserverTest(observerFactory = r.observerFactory) { + r.invokeShellInit() + + inputBuilder { + buildTransitionInfo() + addChange(createChange()) + addChange(createChange()) + addChange(createChange()) + } + + validateOutput { + r.creationEventDetected(expected = false) + r.visibilityEventDetected(expected = false) + r.destroyEventDetected(expected = false) + r.boundsEventDetected(expected = false) + } + } + } + } + + @Test + fun `When a topActivity is letterboxed surfaces creation is requested`() { + runTestScenario { r -> + executeTransitionObserverTest(observerFactory = r.observerFactory) { + r.invokeShellInit() + + inputBuilder { + buildTransitionInfo() + r.createTopActivityChange(inputBuilder = this, isLetterboxed = true) + } + + validateOutput { + r.creationEventDetected(expected = true) + r.visibilityEventDetected(expected = true, visible = true) + r.destroyEventDetected(expected = false) + r.boundsEventDetected(expected = true) + } + } + } + } + + @Test + fun `When a topActivity is not letterboxed visibility is updated`() { + runTestScenario { r -> + executeTransitionObserverTest(observerFactory = r.observerFactory) { + r.invokeShellInit() + + inputBuilder { + buildTransitionInfo() + r.createTopActivityChange(inputBuilder = this, isLetterboxed = false) + } + + validateOutput { + r.creationEventDetected(expected = false) + r.visibilityEventDetected(expected = true, visible = false) + r.destroyEventDetected(expected = false) + r.boundsEventDetected(expected = false) + } + } + } + } + + @Test + fun `When closing change letterbox surface destroy is triggered`() { + runTestScenario { r -> + executeTransitionObserverTest(observerFactory = r.observerFactory) { + r.invokeShellInit() + + inputBuilder { + buildTransitionInfo() + r.createClosingChange(inputBuilder = this) + } + + validateOutput { + r.destroyEventDetected(expected = true) + r.creationEventDetected(expected = false) + r.visibilityEventDetected(expected = false, visible = false) + r.boundsEventDetected(expected = false) + } + } + } + } + + /** + * Runs a test scenario providing a Robot. + */ + fun runTestScenario(consumer: Consumer<LetterboxTransitionObserverRobotTest>) { + val robot = LetterboxTransitionObserverRobotTest() + consumer.accept(robot) + } + + class LetterboxTransitionObserverRobotTest { + + companion object { + @JvmStatic + private val DISPLAY_ID = 1 + + @JvmStatic + private val TASK_ID = 20 + } + + private val executor: ShellExecutor + private val shellInit: ShellInit + private val transitions: Transitions + private val letterboxController: LetterboxController + private val letterboxObserver: LetterboxTransitionObserver + + val observerFactory: () -> LetterboxTransitionObserver + + init { + executor = Mockito.mock(ShellExecutor::class.java) + shellInit = ShellInit(executor) + transitions = Mockito.mock(Transitions::class.java) + letterboxController = Mockito.mock(LetterboxController::class.java) + letterboxObserver = + LetterboxTransitionObserver(shellInit, transitions, letterboxController) + observerFactory = { letterboxObserver } + } + + fun invokeShellInit() = shellInit.init() + + fun observer() = letterboxObserver + + fun checkObservableIsRegistered(expected: Boolean) { + Mockito.verify(transitions, expected.asMode()).registerObserver(observer()) + } + + fun creationEventDetected( + expected: Boolean, + displayId: Int = DISPLAY_ID, + taskId: Int = TASK_ID + ) { + Mockito.verify(letterboxController, expected.asMode()).createLetterboxSurface( + toLetterboxKeyMatcher(displayId, taskId), + anyOrNull(), + anyOrNull() + ) + } + + fun visibilityEventDetected( + expected: Boolean, + displayId: Int = DISPLAY_ID, + taskId: Int = TASK_ID, + visible: Boolean? = null + ) { + Mockito.verify(letterboxController, expected.asMode()).updateLetterboxSurfaceVisibility( + toLetterboxKeyMatcher(displayId, taskId), + anyOrNull(), + visible.asMatcher() + ) + } + + fun destroyEventDetected( + expected: Boolean, + displayId: Int = DISPLAY_ID, + taskId: Int = TASK_ID + ) { + Mockito.verify(letterboxController, expected.asMode()).destroyLetterboxSurface( + toLetterboxKeyMatcher(displayId, taskId), + anyOrNull() + ) + } + + fun boundsEventDetected( + expected: Boolean, + displayId: Int = DISPLAY_ID, + taskId: Int = TASK_ID + ) { + Mockito.verify(letterboxController, expected.asMode()).updateLetterboxSurfaceBounds( + toLetterboxKeyMatcher(displayId, taskId), + anyOrNull(), + anyOrNull() + ) + } + + fun createTopActivityChange( + inputBuilder: TransitionObserverInputBuilder, + isLetterboxed: Boolean = true, + displayId: Int = DISPLAY_ID, + taskId: Int = TASK_ID + ) { + inputBuilder.addChange(changeTaskInfo = inputBuilder.createTaskInfo().apply { + appCompatTaskInfo.isTopActivityLetterboxed = isLetterboxed + this.taskId = taskId + this.displayId = displayId + }) + } + + fun createClosingChange( + inputBuilder: TransitionObserverInputBuilder, + displayId: Int = DISPLAY_ID, + taskId: Int = TASK_ID + ) { + inputBuilder.addChange(changeTaskInfo = inputBuilder.createTaskInfo().apply { + this.taskId = taskId + this.displayId = displayId + }, changeMode = TRANSIT_CLOSE) + } + + private fun Boolean.asMode(): VerificationMode = if (this) times(1) else never() + + private fun Boolean?.asMatcher(): Boolean = + if (this != null) eq(this) else any() + + private fun toLetterboxKeyMatcher(displayId: Int, taskId: Int): LetterboxKey { + if (displayId < 0 || taskId < 0) { + return any() + } else { + return eq(LetterboxKey(displayId, taskId)) + } + } + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/util/TransitionObserverTestUtils.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/util/TransitionObserverTestUtils.kt new file mode 100644 index 000000000000..3e26ee0deed0 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/util/TransitionObserverTestUtils.kt @@ -0,0 +1,182 @@ +/* + * 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.util + +import android.app.ActivityManager.RunningTaskInfo +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.content.ComponentName +import android.content.Intent +import android.os.IBinder +import android.view.Display.DEFAULT_DISPLAY +import android.view.SurfaceControl +import android.view.SurfaceControl.Transaction +import android.view.WindowManager.TRANSIT_NONE +import android.view.WindowManager.TransitionFlags +import android.view.WindowManager.TransitionType +import android.window.IWindowContainerToken +import android.window.TransitionInfo +import android.window.TransitionInfo.Change +import android.window.TransitionInfo.ChangeFlags +import android.window.TransitionInfo.FLAG_NONE +import android.window.TransitionInfo.TransitionMode +import android.window.WindowContainerToken +import com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn +import com.android.wm.shell.transition.Transitions.TransitionObserver +import org.mockito.Mockito +import org.mockito.kotlin.mock + +@DslMarker +annotation class TransitionObserverTagMarker + +/** + * Abstraction for all the phases of the [TransitionObserver] test. + */ +interface TransitionObserverTestStep + +/** + * Encapsulates the values for the [TransitionObserver#onTransitionReady] input parameters. + */ +class TransitionObserverTransitionReadyInput( + val transition: IBinder, + val info: TransitionInfo, + val startTransaction: Transaction, + val finishTransaction: Transaction +) + +@TransitionObserverTagMarker +class TransitionObserverTestContext : TransitionObserverTestStep { + + lateinit var transitionObserver: TransitionObserver + lateinit var transitionReadyInput: TransitionObserverTransitionReadyInput + + fun inputBuilder(builderInput: TransitionObserverInputBuilder.() -> Unit) { + val inputFactoryObj = TransitionObserverInputBuilder() + inputFactoryObj.builderInput() + transitionReadyInput = inputFactoryObj.build() + } + + fun validateOutput( + validate: + TransitionObserverResultValidation.() -> Unit + ) { + val validateObj = TransitionObserverResultValidation() + invokeObservable() + validateObj.validate() + } + + fun invokeObservable() { + transitionObserver.onTransitionReady( + transitionReadyInput.transition, + transitionReadyInput.info, + transitionReadyInput.startTransaction, + transitionReadyInput.finishTransaction + ) + } +} + +/** + * Phase responsible for the input parameters for [TransitionObserver]. + */ +class TransitionObserverInputBuilder : TransitionObserverTestStep { + + private val transition = Mockito.mock(IBinder::class.java) + private var transitionInfo: TransitionInfo? = null + private val startTransaction = Mockito.mock(Transaction::class.java) + private val finishTransaction = Mockito.mock(Transaction::class.java) + + fun buildTransitionInfo( + @TransitionType type: Int = TRANSIT_NONE, + @TransitionFlags flags: Int = 0 + ) { + transitionInfo = TransitionInfo(type, flags) + spyOn(transitionInfo) + } + + fun addChange( + token: WindowContainerToken? = mock(), + leash: SurfaceControl = mock(), + @TransitionMode changeMode: Int = TRANSIT_NONE, + parentToken: WindowContainerToken? = null, + changeTaskInfo: RunningTaskInfo? = null, + @ChangeFlags changeFlags: Int = FLAG_NONE + ) = addChange(Change(token, leash).apply { + mode = changeMode + parent = parentToken + taskInfo = changeTaskInfo + flags = changeFlags + }) + + fun createChange( + token: WindowContainerToken? = mock(), + leash: SurfaceControl = mock(), + @TransitionMode changeMode: Int = TRANSIT_NONE, + parentToken: WindowContainerToken? = null, + changeTaskInfo: RunningTaskInfo? = null, + @ChangeFlags changeFlags: Int = FLAG_NONE + ) = Change(token, leash).apply { + mode = changeMode + parent = parentToken + taskInfo = changeTaskInfo + flags = changeFlags + } + + fun addChange(change: Change) { + transitionInfo!!.addChange(change) + } + + fun createTaskInfo(id: Int = 0, windowingMode: Int = WINDOWING_MODE_FREEFORM) = + RunningTaskInfo().apply { + taskId = id + displayId = DEFAULT_DISPLAY + configuration.windowConfiguration.windowingMode = windowingMode + token = WindowContainerToken(Mockito.mock(IWindowContainerToken::class.java)) + baseIntent = Intent().apply { + component = ComponentName("package", "component.name") + } + } + + fun build(): TransitionObserverTransitionReadyInput = TransitionObserverTransitionReadyInput( + transition = transition, + info = transitionInfo!!, + startTransaction = startTransaction, + finishTransaction = finishTransaction + ) +} + +/** + * Phase responsible for the execution of validation methods. + */ +class TransitionObserverResultValidation : TransitionObserverTestStep + +/** + * Allows to run a test about a specific [TransitionObserver] passing the specific + * implementation and input value as parameters for the [TransitionObserver#onTransitionReady] + * method. + * @param observerFactory The Factory for the TransitionObserver + * @param inputFactory The Builder for the onTransitionReady input parameters + * @param init The test code itself. + */ +fun executeTransitionObserverTest( + observerFactory: () -> TransitionObserver, + init: TransitionObserverTestContext.() -> Unit +): TransitionObserverTestContext { + val testContext = TransitionObserverTestContext().apply { + transitionObserver = observerFactory() + } + testContext.init() + return testContext +} |