diff options
7 files changed, 282 insertions, 199 deletions
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxController.kt index 0ac7aff306a0..523e2f5cf7dc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxController.kt @@ -18,133 +18,51 @@ package com.android.wm.shell.compatui.letterbox import android.graphics.Rect import android.view.SurfaceControl -import com.android.internal.protolog.ProtoLog -import com.android.wm.shell.dagger.WMSingleton -import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_APP_COMPAT -import javax.inject.Inject +import android.view.SurfaceControl.Transaction /** - * Component responsible for handling the lifecycle of the letterbox surfaces. + * Abstracts the component responsible to handle a single or multiple letterbox surfaces for a + * specific [Change]. */ -@WMSingleton -class LetterboxController @Inject constructor( - private val letterboxConfiguration: LetterboxConfiguration -) { - - companion object { - /* - * Letterbox surfaces need to stay below the activity layer which is 0. - */ - // TODO(b/378673153): Consider adding this to [TaskConstants]. - @JvmStatic - private val TASK_CHILD_LAYER_LETTERBOX_BACKGROUND = -1000 - @JvmStatic - private val TAG = "LetterboxController" - } - - private val letterboxMap = mutableMapOf<LetterboxKey, LetterboxItem>() +interface LetterboxController { /** * Creates a Letterbox Surface for a given displayId/taskId if it doesn't exist. */ fun createLetterboxSurface( key: LetterboxKey, - startTransaction: SurfaceControl.Transaction, + transaction: Transaction, parentLeash: SurfaceControl - ) { - letterboxMap.runOnItem(key, onMissed = { k, m -> - m[k] = LetterboxItem( - SurfaceControl.Builder() - .setName("ShellLetterboxSurface-$key") - .setHidden(true) - .setColorLayer() - .setParent(parentLeash) - .setCallsite("LetterboxController-createLetterboxSurface") - .build().apply { - startTransaction.setLayer( - this, - TASK_CHILD_LAYER_LETTERBOX_BACKGROUND - ).setColorSpaceAgnostic(this, true) - .setColor(this, letterboxConfiguration.getBackgroundColorRgbArray()) - } - ) - }) - } + ) /** * Invoked to destroy the surfaces for a letterbox session for given displayId/taskId. */ fun destroyLetterboxSurface( key: LetterboxKey, - startTransaction: SurfaceControl.Transaction - ) { - letterboxMap.runOnItem(key, onFound = { item -> - item.fullWindowSurface?.run { - startTransaction.remove(this) - } - }) - letterboxMap.remove(key) - } + transaction: Transaction + ) /** * Invoked to show/hide the letterbox surfaces for given displayId/taskId. */ fun updateLetterboxSurfaceVisibility( key: LetterboxKey, - startTransaction: SurfaceControl.Transaction, - visible: Boolean = true - ) { - letterboxMap.runOnItem(key, onFound = { item -> - item.fullWindowSurface?.run { - startTransaction.setVisibility(this, visible) - } - }) - } + transaction: Transaction, + visible: Boolean + ) /** * Updates the bounds for the letterbox surfaces for given displayId/taskId. */ fun updateLetterboxSurfaceBounds( key: LetterboxKey, - startTransaction: SurfaceControl.Transaction, - bounds: Rect - ) { - letterboxMap.runOnItem(key, onFound = { item -> - item.fullWindowSurface?.run { - startTransaction.moveAndCrop(this, bounds) - } - }) - } + transaction: Transaction, + taskBounds: Rect + ) - /* - * Executes [onFound] on the [LetterboxItem] if present or [onMissed] if not present. + /** + * Utility method to dump the current state. */ - private fun MutableMap<LetterboxKey, LetterboxItem>.runOnItem( - key: LetterboxKey, - onFound: (LetterboxItem) -> Unit = { _ -> }, - onMissed: ( - LetterboxKey, - MutableMap<LetterboxKey, LetterboxItem> - ) -> Unit = { _, _ -> } - ) { - this[key]?.let { - return onFound(it) - } - return onMissed(key, this) - } - - fun dump() { - ProtoLog.v(WM_SHELL_APP_COMPAT, "%s: %s", TAG, "${letterboxMap.keys}") - } - - private fun SurfaceControl.Transaction.moveAndCrop( - surface: SurfaceControl, - rect: Rect - ): SurfaceControl.Transaction = - setPosition(surface, rect.left.toFloat(), rect.top.toFloat()) - .setWindowCrop( - surface, - rect.width(), - rect.height() - ) + fun dump() } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxData.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxData.kt index 98fd2472f1e4..adb034cc4787 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxData.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxData.kt @@ -16,10 +16,5 @@ package com.android.wm.shell.compatui.letterbox -import android.view.SurfaceControl - // The key to use for identify the letterbox sessions. -data class LetterboxKey(val displayId: Int, val taskId: Int) - -// Encapsulate the objects for the specific letterbox session. -data class LetterboxItem(val fullWindowSurface: SurfaceControl?)
\ No newline at end of file +data class LetterboxKey(val displayId: Int, val taskId: Int)
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxTransitionObserver.kt index 67429bdd112b..b50716ad07a3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxTransitionObserver.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxTransitionObserver.kt @@ -43,12 +43,7 @@ class LetterboxTransitionObserver( init { if (appCompatRefactoring()) { - ProtoLog.v( - WM_SHELL_APP_COMPAT, - "%s: %s", - TAG, - "Initializing LetterboxTransitionObserver" - ) + logV("Initializing LetterboxTransitionObserver") shellInit.addInitCallback({ transitions.registerObserver(this) }, this) @@ -69,38 +64,45 @@ class LetterboxTransitionObserver( for (change in info.changes) { change.taskInfo?.let { ti -> val key = LetterboxKey(ti.displayId, ti.taskId) - if (isClosingType(change.mode)) { - letterboxController.destroyLetterboxSurface( - key, - startTransaction - ) - } else { - val isTopActivityLetterboxed = ti.appCompatTaskInfo.isTopActivityLetterboxed - if (isTopActivityLetterboxed) { - letterboxController.createLetterboxSurface( + val taskBounds = Rect( + change.endRelOffset.x, + change.endRelOffset.y, + change.endAbsBounds.width(), + change.endAbsBounds.height() + ) + with(letterboxController) { + if (isClosingType(change.mode)) { + destroyLetterboxSurface( key, - startTransaction, - change.leash + startTransaction ) - letterboxController.updateLetterboxSurfaceBounds( + } else { + val isTopActivityLetterboxed = ti.appCompatTaskInfo.isTopActivityLetterboxed + if (isTopActivityLetterboxed) { + createLetterboxSurface( + key, + startTransaction, + change.leash + ) + updateLetterboxSurfaceBounds( + key, + startTransaction, + taskBounds + ) + } + updateLetterboxSurfaceVisibility( key, startTransaction, - Rect( - change.endRelOffset.x, - change.endRelOffset.y, - change.endAbsBounds.width(), - change.endAbsBounds.height() - ) + isTopActivityLetterboxed ) } - letterboxController.updateLetterboxSurfaceVisibility( - key, - startTransaction, - isTopActivityLetterboxed - ) + dump() } - letterboxController.dump() } } } + + private fun logV(msg: String) { + ProtoLog.v(WM_SHELL_APP_COMPAT, "%s: %s", TAG, msg) + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/SingleSurfaceLetterboxController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/SingleSurfaceLetterboxController.kt new file mode 100644 index 000000000000..d918b87be799 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/SingleSurfaceLetterboxController.kt @@ -0,0 +1,151 @@ +/* + * 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.view.SurfaceControl +import android.view.SurfaceControl.Transaction +import com.android.internal.protolog.ProtoLog +import com.android.wm.shell.dagger.WMSingleton +import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_APP_COMPAT +import javax.inject.Inject + +/** + * Component responsible for handling the lifecycle of a single letterbox surface. + */ +@WMSingleton +class SingleSurfaceLetterboxController @Inject constructor( + private val letterboxConfiguration: LetterboxConfiguration +) : LetterboxController { + + companion object { + /* + * Letterbox surfaces need to stay below the activity layer which is 0. + */ + // TODO(b/378673153): Consider adding this to [TaskConstants]. + @JvmStatic + private val TASK_CHILD_LAYER_LETTERBOX_BACKGROUND = -1000 + + @JvmStatic + private val TAG = "LetterboxController" + } + + private val letterboxMap = mutableMapOf<LetterboxKey, SurfaceControl>() + + /** + * Creates a Letterbox Surface for a given displayId/taskId if it doesn't exist. + */ + override fun createLetterboxSurface( + key: LetterboxKey, + transaction: Transaction, + parentLeash: SurfaceControl + ) { + letterboxMap.runOnItem(key, onMissed = { k, m -> + m[k] = + SurfaceControl.Builder() + .setName("ShellLetterboxSurface-$key") + .setHidden(true) + .setColorLayer() + .setParent(parentLeash) + .setCallsite("LetterboxController-createLetterboxSurface") + .build().apply { + transaction.setLayer( + this, + TASK_CHILD_LAYER_LETTERBOX_BACKGROUND + ).setColorSpaceAgnostic(this, true) + .setColor(this, letterboxConfiguration.getBackgroundColorRgbArray()) + } + }) + } + + /** + * Invoked to destroy the surfaces for a letterbox session for given displayId/taskId. + */ + override fun destroyLetterboxSurface( + key: LetterboxKey, + transaction: Transaction + ) { + letterboxMap.runOnItem(key, onFound = { item -> + item.run { + transaction.remove(this) + } + }) + letterboxMap.remove(key) + } + + /** + * Invoked to show/hide the letterbox surfaces for given displayId/taskId. + */ + override fun updateLetterboxSurfaceVisibility( + key: LetterboxKey, + transaction: Transaction, + visible: Boolean + ) { + letterboxMap.runOnItem(key, onFound = { item -> + item.run { + transaction.setVisibility(this, visible) + } + }) + } + + /** + * Updates the bounds for the letterbox surfaces for given displayId/taskId. + */ + override fun updateLetterboxSurfaceBounds( + key: LetterboxKey, + transaction: Transaction, + taskBounds: Rect + ) { + letterboxMap.runOnItem(key, onFound = { item -> + item.run { + transaction.moveAndCrop(this, taskBounds) + } + }) + } + + override fun dump() { + ProtoLog.v(WM_SHELL_APP_COMPAT, "%s: %s", TAG, "${letterboxMap.keys}") + } + + /* + * Executes [onFound] on the [SurfaceControl] if present or [onMissed] if not present. + */ + private fun MutableMap<LetterboxKey, SurfaceControl>.runOnItem( + key: LetterboxKey, + onFound: (SurfaceControl) -> Unit = { _ -> }, + onMissed: ( + LetterboxKey, + MutableMap<LetterboxKey, SurfaceControl> + ) -> Unit = { _, _ -> } + ) { + this[key]?.let { + return onFound(it) + } + return onMissed(key, this) + } + + private fun Transaction.moveAndCrop( + surface: SurfaceControl, + rect: Rect + ): Transaction = + setPosition(surface, rect.left.toFloat(), rect.top.toFloat()) + .setWindowCrop( + surface, + rect.width(), + rect.height() + ) +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index 0f636588476a..975773fe2532 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -71,6 +71,7 @@ import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.compatui.letterbox.LetterboxCommandHandler; import com.android.wm.shell.compatui.letterbox.LetterboxController; import com.android.wm.shell.compatui.letterbox.LetterboxTransitionObserver; +import com.android.wm.shell.compatui.letterbox.SingleSurfaceLetterboxController; import com.android.wm.shell.dagger.back.ShellBackAnimationModule; import com.android.wm.shell.dagger.pip.PipModule; import com.android.wm.shell.desktopmode.CloseDesktopTaskTransitionHandler; @@ -1316,4 +1317,9 @@ public abstract class WMShellModule { ) { return new LetterboxTransitionObserver(shellInit, transitions, letterboxController); } + + @WMSingleton + @Binds + abstract LetterboxController bindsLetterboxController( + SingleSurfaceLetterboxController letterboxController); } 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 index 1ae1c3fc4563..9c6afcb8be63 100644 --- 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 @@ -16,10 +16,13 @@ package com.android.wm.shell.compatui.letterbox +import android.graphics.Point +import android.graphics.Rect 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.SurfaceControl import android.view.WindowManager.TRANSIT_CLOSE import androidx.test.filters.SmallTest import com.android.window.flags.Flags @@ -33,12 +36,12 @@ 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.mock import org.mockito.kotlin.never import org.mockito.kotlin.times -import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.verify import org.mockito.verification.VerificationMode /** @@ -93,7 +96,7 @@ class LetterboxTransitionObserverTest : ShellTestCase() { r.creationEventDetected(expected = false) r.visibilityEventDetected(expected = false) r.destroyEventDetected(expected = false) - r.boundsEventDetected(expected = false) + r.updateSurfaceBoundsEventDetected(expected = false) } } } @@ -107,14 +110,23 @@ class LetterboxTransitionObserverTest : ShellTestCase() { inputBuilder { buildTransitionInfo() - r.createTopActivityChange(inputBuilder = this, isLetterboxed = true) + r.createTopActivityChange( + inputBuilder = this, + isLetterboxed = true, + taskPosition = Point(20, 30), + taskWidth = 200, + taskHeight = 300 + ) } validateOutput { r.creationEventDetected(expected = true) r.visibilityEventDetected(expected = true, visible = true) r.destroyEventDetected(expected = false) - r.boundsEventDetected(expected = true) + r.updateSurfaceBoundsEventDetected( + expected = true, + taskBounds = Rect(20, 30, 200, 300) + ) } } } @@ -135,7 +147,7 @@ class LetterboxTransitionObserverTest : ShellTestCase() { r.creationEventDetected(expected = false) r.visibilityEventDetected(expected = true, visible = false) r.destroyEventDetected(expected = false) - r.boundsEventDetected(expected = false) + r.updateSurfaceBoundsEventDetected(expected = false) } } } @@ -156,7 +168,7 @@ class LetterboxTransitionObserverTest : ShellTestCase() { r.destroyEventDetected(expected = true) r.creationEventDetected(expected = false) r.visibilityEventDetected(expected = false, visible = false) - r.boundsEventDetected(expected = false) + r.updateSurfaceBoundsEventDetected(expected = false) } } } @@ -189,10 +201,10 @@ class LetterboxTransitionObserverTest : ShellTestCase() { val observerFactory: () -> LetterboxTransitionObserver init { - executor = Mockito.mock(ShellExecutor::class.java) + executor = mock<ShellExecutor>() shellInit = ShellInit(executor) - transitions = Mockito.mock(Transitions::class.java) - letterboxController = Mockito.mock(LetterboxController::class.java) + transitions = mock<Transitions>() + letterboxController = mock<LetterboxController>() letterboxObserver = LetterboxTransitionObserver(shellInit, transitions, letterboxController) observerFactory = { letterboxObserver } @@ -203,67 +215,78 @@ class LetterboxTransitionObserverTest : ShellTestCase() { fun observer() = letterboxObserver fun checkObservableIsRegistered(expected: Boolean) { - Mockito.verify(transitions, expected.asMode()).registerObserver(observer()) + 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() - ) - } + ) = verify( + letterboxController, + expected.asMode() + ).createLetterboxSurface( + eq(LetterboxKey(displayId, taskId)), + any<SurfaceControl.Transaction>(), + any<SurfaceControl>() + ) fun visibilityEventDetected( expected: Boolean, + visible: Boolean = true, displayId: Int = DISPLAY_ID, - taskId: Int = TASK_ID, - visible: Boolean? = null - ) { - Mockito.verify(letterboxController, expected.asMode()).updateLetterboxSurfaceVisibility( - toLetterboxKeyMatcher(displayId, taskId), - anyOrNull(), - visible.asMatcher() - ) - } + taskId: Int = TASK_ID + ) = verify(letterboxController, expected.asMode()).updateLetterboxSurfaceVisibility( + eq(LetterboxKey(displayId, taskId)), + any<SurfaceControl.Transaction>(), + eq(visible) + ) fun destroyEventDetected( expected: Boolean, displayId: Int = DISPLAY_ID, taskId: Int = TASK_ID - ) { - Mockito.verify(letterboxController, expected.asMode()).destroyLetterboxSurface( - toLetterboxKeyMatcher(displayId, taskId), - anyOrNull() - ) - } - - fun boundsEventDetected( + ) = verify( + letterboxController, + expected.asMode() + ).destroyLetterboxSurface( + eq(LetterboxKey(displayId, taskId)), + any<SurfaceControl.Transaction>() + ) + + fun updateSurfaceBoundsEventDetected( expected: Boolean, displayId: Int = DISPLAY_ID, - taskId: Int = TASK_ID - ) { - Mockito.verify(letterboxController, expected.asMode()).updateLetterboxSurfaceBounds( - toLetterboxKeyMatcher(displayId, taskId), - anyOrNull(), - anyOrNull() - ) - } + taskId: Int = TASK_ID, + taskBounds: Rect = Rect() + ) = verify( + letterboxController, + expected.asMode() + ).updateLetterboxSurfaceBounds( + eq(LetterboxKey(displayId, taskId)), + any<SurfaceControl.Transaction>(), + eq(taskBounds) + ) fun createTopActivityChange( inputBuilder: TransitionObserverInputBuilder, isLetterboxed: Boolean = true, displayId: Int = DISPLAY_ID, - taskId: Int = TASK_ID + taskId: Int = TASK_ID, + taskPosition: Point = Point(), + taskWidth: Int = 0, + taskHeight: Int = 0 ) { - inputBuilder.addChange(changeTaskInfo = inputBuilder.createTaskInfo().apply { - appCompatTaskInfo.isTopActivityLetterboxed = isLetterboxed - this.taskId = taskId - this.displayId = displayId + inputBuilder.addChange(inputBuilder.createChange( + changeTaskInfo = inputBuilder.createTaskInfo().apply { + appCompatTaskInfo.isTopActivityLetterboxed = isLetterboxed + this.taskId = taskId + this.displayId = displayId + } + ).apply { + endRelOffset.x = taskPosition.x + endRelOffset.y = taskPosition.y + endAbsBounds.set(Rect(0, 0, taskWidth, taskHeight)) }) } @@ -279,16 +302,5 @@ class LetterboxTransitionObserverTest : ShellTestCase() { } 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 index 3e26ee0deed0..0e15668a05a7 100644 --- 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 @@ -36,7 +36,6 @@ 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 @@ -93,10 +92,10 @@ class TransitionObserverTestContext : TransitionObserverTestStep { */ class TransitionObserverInputBuilder : TransitionObserverTestStep { - private val transition = Mockito.mock(IBinder::class.java) + private val transition = mock<IBinder>() private var transitionInfo: TransitionInfo? = null - private val startTransaction = Mockito.mock(Transaction::class.java) - private val finishTransaction = Mockito.mock(Transaction::class.java) + private val startTransaction = mock<Transaction>() + private val finishTransaction = mock<Transaction>() fun buildTransitionInfo( @TransitionType type: Int = TRANSIT_NONE, @@ -143,7 +142,7 @@ class TransitionObserverInputBuilder : TransitionObserverTestStep { taskId = id displayId = DEFAULT_DISPLAY configuration.windowConfiguration.windowingMode = windowingMode - token = WindowContainerToken(Mockito.mock(IWindowContainerToken::class.java)) + token = WindowContainerToken(mock<IWindowContainerToken>()) baseIntent = Intent().apply { component = ComponentName("package", "component.name") } |