Loading libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/lifecycle/SkipLetterboxLifecycleEventFactory.kt 0 → 100644 +44 −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.window.TransitionInfo import com.android.wm.shell.dagger.WMSingleton import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer import javax.inject.Inject /** * [LetterboxLifecycleEventFactory] implementation that ignore a Change * if not related to Letterboxing. */ @WMSingleton class SkipLetterboxLifecycleEventFactory @Inject constructor( private val desksOrganizer: DesksOrganizer ) : LetterboxLifecycleEventFactory { // A Desktop Windowing transition should be ignored because not related to Letterboxing. This // prevents any operations on the Letterbox Surfaces (e.g. resize) which can cause unwanted // behaviour (e.g. Adding Letterbox Surfaces on the wrong Task surface). // TODO(b/421188466): Improve heuristics for Activities dealing with Camera. override fun canHandle(change: TransitionInfo.Change): Boolean = desksOrganizer.isDeskChange(change) // Although this LetterboxLifecycleEventFactory is able to handle the specific Change // it returns an empty LetterboxLifecycleEvent to basically ignore the Change. override fun createLifecycleEvent(change: TransitionInfo.Change): LetterboxLifecycleEvent? = null } libs/WindowManager/Shell/src/com/android/wm/shell/dagger/LetterboxModule.java +9 −1 Original line number Diff line number Diff line Loading @@ -26,6 +26,7 @@ import com.android.wm.shell.compatui.letterbox.lifecycle.LetterboxLifecycleContr import com.android.wm.shell.compatui.letterbox.lifecycle.LetterboxLifecycleControllerImpl; import com.android.wm.shell.compatui.letterbox.lifecycle.LetterboxLifecycleEventFactory; import com.android.wm.shell.compatui.letterbox.lifecycle.MultiLetterboxLifecycleEventFactory; import com.android.wm.shell.compatui.letterbox.lifecycle.SkipLetterboxLifecycleEventFactory; import com.android.wm.shell.compatui.letterbox.lifecycle.TaskInfoLetterboxLifecycleEventFactory; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; Loading Loading @@ -55,8 +56,15 @@ public abstract class LetterboxModule { @WMSingleton @Provides static LetterboxLifecycleEventFactory provideLetterboxLifecycleEventFactory() { static LetterboxLifecycleEventFactory provideLetterboxLifecycleEventFactory( @NonNull SkipLetterboxLifecycleEventFactory skipLetterboxLifecycleEventFactory ) { // The order of the LetterboxLifecycleEventFactory implementation matters because the // first that can handle a Change will be chosen for the LetterboxLifecycleEvent creation. return new MultiLetterboxLifecycleEventFactory(List.of( // Filters out transitions not related to Letterboxing. skipLetterboxLifecycleEventFactory, // Creates a LetterboxLifecycleEvent in case of Task transitions. new TaskInfoLetterboxLifecycleEventFactory() )); } Loading libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/lifecycle/SkipLetterboxLifecycleEventFactoryTest.kt 0 → 100644 +102 −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.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.wm.shell.ShellTestCase import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer import com.android.wm.shell.util.testLetterboxLifecycleEventFactory 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.mock /** * Tests for [SkipLetterboxLifecycleEventFactory]. * * Build/Install/Run: * atest WMShellUnitTests:SkipLetterboxLifecycleEventFactoryTest */ @RunWith(AndroidTestingRunner::class) @SmallTest class SkipLetterboxLifecycleEventFactoryTest : ShellTestCase() { @Test fun `Factory is active when Change is a DesksOrganizer change`() { runTestScenario { r -> testLetterboxLifecycleEventFactory(r.getLetterboxLifecycleEventFactory()) { inputChange { // Empty Change } r.configureDesksOrganizer(isDeskChange = true) validateCanHandle { canHandle -> assert(canHandle) } validateCreateLifecycleEvent { event -> assert(event == null) } } } } @Test fun `Factory is skipped when Change is NOT a DesksOrganizer change`() { runTestScenario { r -> testLetterboxLifecycleEventFactory(r.getLetterboxLifecycleEventFactory()) { inputChange { // Empty Change } r.configureDesksOrganizer(isDeskChange = false) validateCanHandle { canHandle -> assert(!canHandle) } validateCreateLifecycleEvent { event -> assert(event != null) assert(event?.type == LetterboxLifecycleEventType.NONE) } } } } /** * Runs a test scenario providing a Robot. */ fun runTestScenario(consumer: Consumer<DisableLetterboxLifecycleEventFactoryRobotTest>) { val robot = DisableLetterboxLifecycleEventFactoryRobotTest() consumer.accept(robot) } /** * Robot contextual to [TaskInfoLetterboxLifecycleEventFactory]. */ class DisableLetterboxLifecycleEventFactoryRobotTest { private val desksOrganizer: DesksOrganizer = mock<DesksOrganizer>() fun configureDesksOrganizer(isDeskChange: Boolean) { doReturn(isDeskChange).`when`(desksOrganizer).isDeskChange(any()) } fun getLetterboxLifecycleEventFactory(): () -> LetterboxLifecycleEventFactory = { SkipLetterboxLifecycleEventFactory(desksOrganizer) } } } Loading
libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/lifecycle/SkipLetterboxLifecycleEventFactory.kt 0 → 100644 +44 −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.window.TransitionInfo import com.android.wm.shell.dagger.WMSingleton import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer import javax.inject.Inject /** * [LetterboxLifecycleEventFactory] implementation that ignore a Change * if not related to Letterboxing. */ @WMSingleton class SkipLetterboxLifecycleEventFactory @Inject constructor( private val desksOrganizer: DesksOrganizer ) : LetterboxLifecycleEventFactory { // A Desktop Windowing transition should be ignored because not related to Letterboxing. This // prevents any operations on the Letterbox Surfaces (e.g. resize) which can cause unwanted // behaviour (e.g. Adding Letterbox Surfaces on the wrong Task surface). // TODO(b/421188466): Improve heuristics for Activities dealing with Camera. override fun canHandle(change: TransitionInfo.Change): Boolean = desksOrganizer.isDeskChange(change) // Although this LetterboxLifecycleEventFactory is able to handle the specific Change // it returns an empty LetterboxLifecycleEvent to basically ignore the Change. override fun createLifecycleEvent(change: TransitionInfo.Change): LetterboxLifecycleEvent? = null }
libs/WindowManager/Shell/src/com/android/wm/shell/dagger/LetterboxModule.java +9 −1 Original line number Diff line number Diff line Loading @@ -26,6 +26,7 @@ import com.android.wm.shell.compatui.letterbox.lifecycle.LetterboxLifecycleContr import com.android.wm.shell.compatui.letterbox.lifecycle.LetterboxLifecycleControllerImpl; import com.android.wm.shell.compatui.letterbox.lifecycle.LetterboxLifecycleEventFactory; import com.android.wm.shell.compatui.letterbox.lifecycle.MultiLetterboxLifecycleEventFactory; import com.android.wm.shell.compatui.letterbox.lifecycle.SkipLetterboxLifecycleEventFactory; import com.android.wm.shell.compatui.letterbox.lifecycle.TaskInfoLetterboxLifecycleEventFactory; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; Loading Loading @@ -55,8 +56,15 @@ public abstract class LetterboxModule { @WMSingleton @Provides static LetterboxLifecycleEventFactory provideLetterboxLifecycleEventFactory() { static LetterboxLifecycleEventFactory provideLetterboxLifecycleEventFactory( @NonNull SkipLetterboxLifecycleEventFactory skipLetterboxLifecycleEventFactory ) { // The order of the LetterboxLifecycleEventFactory implementation matters because the // first that can handle a Change will be chosen for the LetterboxLifecycleEvent creation. return new MultiLetterboxLifecycleEventFactory(List.of( // Filters out transitions not related to Letterboxing. skipLetterboxLifecycleEventFactory, // Creates a LetterboxLifecycleEvent in case of Task transitions. new TaskInfoLetterboxLifecycleEventFactory() )); } Loading
libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/lifecycle/SkipLetterboxLifecycleEventFactoryTest.kt 0 → 100644 +102 −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.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.wm.shell.ShellTestCase import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer import com.android.wm.shell.util.testLetterboxLifecycleEventFactory 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.mock /** * Tests for [SkipLetterboxLifecycleEventFactory]. * * Build/Install/Run: * atest WMShellUnitTests:SkipLetterboxLifecycleEventFactoryTest */ @RunWith(AndroidTestingRunner::class) @SmallTest class SkipLetterboxLifecycleEventFactoryTest : ShellTestCase() { @Test fun `Factory is active when Change is a DesksOrganizer change`() { runTestScenario { r -> testLetterboxLifecycleEventFactory(r.getLetterboxLifecycleEventFactory()) { inputChange { // Empty Change } r.configureDesksOrganizer(isDeskChange = true) validateCanHandle { canHandle -> assert(canHandle) } validateCreateLifecycleEvent { event -> assert(event == null) } } } } @Test fun `Factory is skipped when Change is NOT a DesksOrganizer change`() { runTestScenario { r -> testLetterboxLifecycleEventFactory(r.getLetterboxLifecycleEventFactory()) { inputChange { // Empty Change } r.configureDesksOrganizer(isDeskChange = false) validateCanHandle { canHandle -> assert(!canHandle) } validateCreateLifecycleEvent { event -> assert(event != null) assert(event?.type == LetterboxLifecycleEventType.NONE) } } } } /** * Runs a test scenario providing a Robot. */ fun runTestScenario(consumer: Consumer<DisableLetterboxLifecycleEventFactoryRobotTest>) { val robot = DisableLetterboxLifecycleEventFactoryRobotTest() consumer.accept(robot) } /** * Robot contextual to [TaskInfoLetterboxLifecycleEventFactory]. */ class DisableLetterboxLifecycleEventFactoryRobotTest { private val desksOrganizer: DesksOrganizer = mock<DesksOrganizer>() fun configureDesksOrganizer(isDeskChange: Boolean) { doReturn(isDeskChange).`when`(desksOrganizer).isDeskChange(any()) } fun getLetterboxLifecycleEventFactory(): () -> LetterboxLifecycleEventFactory = { SkipLetterboxLifecycleEventFactory(desksOrganizer) } } }