diff options
8 files changed, 527 insertions, 18 deletions
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiDisplayDragMoveIndicatorController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiDisplayDragMoveIndicatorController.kt new file mode 100644 index 000000000000..7a5bc1383ccf --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiDisplayDragMoveIndicatorController.kt @@ -0,0 +1,124 @@ +/* + * 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.common + +import android.app.ActivityManager.RunningTaskInfo +import android.graphics.RectF +import android.view.SurfaceControl +import com.android.wm.shell.RootTaskDisplayAreaOrganizer +import com.android.wm.shell.shared.annotations.ShellDesktopThread + +/** + * Controller to manage the indicators that show users the current position of the dragged window on + * the new display when performing drag move across displays. + */ +class MultiDisplayDragMoveIndicatorController( + private val displayController: DisplayController, + private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, + private val indicatorSurfaceFactory: MultiDisplayDragMoveIndicatorSurface.Factory, + @ShellDesktopThread private val desktopExecutor: ShellExecutor, +) { + @ShellDesktopThread + private val dragIndicators = + mutableMapOf<Int, MutableMap<Int, MultiDisplayDragMoveIndicatorSurface>>() + + /** + * Called during drag move, which started at [startDisplayId]. Updates the position and + * visibility of the drag move indicators for the [taskInfo] based on [boundsDp] on the + * destination displays ([displayIds]) as the dragged window moves. [transactionSupplier] + * provides a [SurfaceControl.Transaction] for applying changes to the indicator surfaces. + * + * It is executed on the [desktopExecutor] to prevent blocking the main thread and avoid jank, + * as creating and manipulating surfaces can be expensive. + */ + fun onDragMove( + boundsDp: RectF, + startDisplayId: Int, + taskInfo: RunningTaskInfo, + displayIds: Set<Int>, + transactionSupplier: () -> SurfaceControl.Transaction, + ) { + desktopExecutor.execute { + for (displayId in displayIds) { + if (displayId == startDisplayId) { + // No need to render indicators on the original display where the drag started. + continue + } + val displayLayout = displayController.getDisplayLayout(displayId) ?: continue + val shouldBeVisible = + RectF.intersects(RectF(boundsDp), displayLayout.globalBoundsDp()) + if ( + dragIndicators[taskInfo.taskId]?.containsKey(displayId) != true && + !shouldBeVisible + ) { + // Skip this display if: + // - It doesn't have an existing indicator that needs to be updated, AND + // - The latest dragged window bounds don't intersect with this display. + continue + } + + val boundsPx = + MultiDisplayDragMoveBoundsCalculator.convertGlobalDpToLocalPxForRect( + boundsDp, + displayLayout, + ) + + // Get or create the inner map for the current task. + val dragIndicatorsForTask = + dragIndicators.getOrPut(taskInfo.taskId) { mutableMapOf() } + dragIndicatorsForTask[displayId]?.also { existingIndicator -> + val transaction = transactionSupplier() + existingIndicator.relayout(boundsPx, transaction, shouldBeVisible) + transaction.apply() + } ?: run { + val newIndicator = + indicatorSurfaceFactory.create( + taskInfo, + displayController.getDisplay(displayId), + ) + newIndicator.show( + transactionSupplier(), + taskInfo, + rootTaskDisplayAreaOrganizer, + displayId, + boundsPx, + ) + dragIndicatorsForTask[displayId] = newIndicator + } + } + } + } + + /** + * Called when the drag ends. Disposes of the drag move indicator surfaces associated with the + * given [taskId]. [transactionSupplier] provides a [SurfaceControl.Transaction] for applying + * changes to the indicator surfaces. + * + * It is executed on the [desktopExecutor] to ensure that any pending `onDragMove` operations + * have completed before disposing of the surfaces. + */ + fun onDragEnd(taskId: Int, transactionSupplier: () -> SurfaceControl.Transaction) { + desktopExecutor.execute { + dragIndicators.remove(taskId)?.values?.takeIf { it.isNotEmpty() }?.let { indicators -> + val transaction = transactionSupplier() + indicators.forEach { indicator -> + indicator.disposeSurface(transaction) + } + transaction.apply() + } + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiDisplayDragMoveIndicatorSurface.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiDisplayDragMoveIndicatorSurface.kt new file mode 100644 index 000000000000..d05d3b0903d7 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiDisplayDragMoveIndicatorSurface.kt @@ -0,0 +1,151 @@ +/* + * 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.common + +import android.app.ActivityManager.RunningTaskInfo +import android.content.Context +import android.graphics.Color +import android.graphics.Rect +import android.os.Trace +import android.view.Display +import android.view.SurfaceControl +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.ui.graphics.toArgb +import com.android.wm.shell.RootTaskDisplayAreaOrganizer +import com.android.wm.shell.windowdecor.common.DecorThemeUtil +import com.android.wm.shell.windowdecor.common.Theme + +/** + * Represents the indicator surface that visualizes the current position of a dragged window during + * a multi-display drag operation. + * + * This class manages the creation, display, and manipulation of the [SurfaceControl]s that act as a + * visual indicator, providing feedback to the user about the dragged window's location. + */ +class MultiDisplayDragMoveIndicatorSurface( + context: Context, + taskInfo: RunningTaskInfo, + display: Display, + surfaceControlBuilderFactory: Factory.SurfaceControlBuilderFactory, +) { + private var isVisible = false + + // A container surface to host the veil background + private var veilSurface: SurfaceControl? = null + + private val decorThemeUtil = DecorThemeUtil(context) + private val lightColors = dynamicLightColorScheme(context) + private val darkColors = dynamicDarkColorScheme(context) + + init { + Trace.beginSection("DragIndicatorSurface#init") + + val displayId = display.displayId + veilSurface = + surfaceControlBuilderFactory + .create("Drag indicator veil of Task=${taskInfo.taskId} Display=$displayId") + .setColorLayer() + .setCallsite("DragIndicatorSurface#init") + .setHidden(true) + .build() + + // TODO: b/383069173 - Add icon for the surface. + + Trace.endSection() + } + + /** + * Disposes the indicator surface using the provided [transaction]. + */ + fun disposeSurface(transaction: SurfaceControl.Transaction) { + veilSurface?.let { veil -> transaction.remove(veil) } + veilSurface = null + } + + /** + * Shows the indicator surface at [bounds] on the specified display ([displayId]), + * visualizing the drag of the [taskInfo]. The indicator surface is shown using [transaction], + * and the [rootTaskDisplayAreaOrganizer] is used to reparent the surfaces. + */ + fun show( + transaction: SurfaceControl.Transaction, + taskInfo: RunningTaskInfo, + rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, + displayId: Int, + bounds: Rect, + ) { + val backgroundColor = + when (decorThemeUtil.getAppTheme(taskInfo)) { + Theme.LIGHT -> lightColors.surfaceContainer + Theme.DARK -> darkColors.surfaceContainer + } + val veil = veilSurface ?: return + isVisible = true + + rootTaskDisplayAreaOrganizer.reparentToDisplayArea(displayId, veil, transaction) + relayout(bounds, transaction, shouldBeVisible = true) + transaction.show(veil).setColor(veil, Color.valueOf(backgroundColor.toArgb()).components) + transaction.apply() + } + + /** + * Repositions and resizes the indicator surface based on [bounds] using [transaction]. The + * [shouldBeVisible] flag indicates whether the indicator is within the display after relayout. + */ + fun relayout(bounds: Rect, transaction: SurfaceControl.Transaction, shouldBeVisible: Boolean) { + if (!isVisible && !shouldBeVisible) { + // No need to relayout if the surface is already invisible and should not be visible. + return + } + isVisible = shouldBeVisible + val veil = veilSurface ?: return + transaction.setCrop(veil, bounds) + } + + /** + * Factory for creating [MultiDisplayDragMoveIndicatorSurface] instances with the [context]. + */ + class Factory(private val context: Context) { + private val surfaceControlBuilderFactory: SurfaceControlBuilderFactory = + object : SurfaceControlBuilderFactory {} + + /** + * Creates a new [MultiDisplayDragMoveIndicatorSurface] instance to visualize the drag + * operation of the [taskInfo] on the given [display]. + */ + fun create( + taskInfo: RunningTaskInfo, + display: Display, + ) = MultiDisplayDragMoveIndicatorSurface( + context, + taskInfo, + display, + surfaceControlBuilderFactory, + ) + + /** + * Interface for creating [SurfaceControl.Builder] instances. + * + * This provides an abstraction over [SurfaceControl.Builder] creation for testing purposes. + */ + interface SurfaceControlBuilderFactory { + fun create(name: String): SurfaceControl.Builder { + return SurfaceControl.Builder().setName(name) + } + } + } +} 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 2fd8c27d5970..ab9aa550e915 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 @@ -68,6 +68,8 @@ import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.FloatingContentCoordinator; import com.android.wm.shell.common.LaunchAdjacentController; +import com.android.wm.shell.common.MultiDisplayDragMoveIndicatorController; +import com.android.wm.shell.common.MultiDisplayDragMoveIndicatorSurface; import com.android.wm.shell.common.MultiInstanceHelper; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; @@ -989,7 +991,8 @@ public abstract class WMShellModule { WindowDecorTaskResourceLoader taskResourceLoader, RecentsTransitionHandler recentsTransitionHandler, DesktopModeCompatPolicy desktopModeCompatPolicy, - DesktopTilingDecorViewModel desktopTilingDecorViewModel + DesktopTilingDecorViewModel desktopTilingDecorViewModel, + MultiDisplayDragMoveIndicatorController multiDisplayDragMoveIndicatorController ) { if (!DesktopModeStatus.canEnterDesktopModeOrShowAppHandle(context)) { return Optional.empty(); @@ -1006,7 +1009,30 @@ public abstract class WMShellModule { windowDecorCaptionHandleRepository, activityOrientationChangeHandler, focusTransitionObserver, desktopModeEventLogger, desktopModeUiEventLogger, taskResourceLoader, recentsTransitionHandler, desktopModeCompatPolicy, - desktopTilingDecorViewModel)); + desktopTilingDecorViewModel, + multiDisplayDragMoveIndicatorController)); + } + + @WMSingleton + @Provides + static MultiDisplayDragMoveIndicatorController + providesMultiDisplayDragMoveIndicatorController( + DisplayController displayController, + RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, + MultiDisplayDragMoveIndicatorSurface.Factory + multiDisplayDragMoveIndicatorSurfaceFactory, + @ShellDesktopThread ShellExecutor desktopExecutor + ) { + return new MultiDisplayDragMoveIndicatorController( + displayController, rootTaskDisplayAreaOrganizer, + multiDisplayDragMoveIndicatorSurfaceFactory, desktopExecutor); + } + + @WMSingleton + @Provides + static MultiDisplayDragMoveIndicatorSurface.Factory + providesMultiDisplayDragMoveIndicatorSurfaceFactory(Context context) { + return new MultiDisplayDragMoveIndicatorSurface.Factory(context); } @WMSingleton diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index 5a6ea214e561..cf139a008164 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -103,6 +103,7 @@ import com.android.wm.shell.common.DisplayChangeController; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.MultiDisplayDragMoveIndicatorController; import com.android.wm.shell.common.MultiInstanceHelper; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; @@ -258,6 +259,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, private final RecentsTransitionHandler mRecentsTransitionHandler; private final DesktopModeCompatPolicy mDesktopModeCompatPolicy; private final DesktopTilingDecorViewModel mDesktopTilingDecorViewModel; + private final MultiDisplayDragMoveIndicatorController mMultiDisplayDragMoveIndicatorController; public DesktopModeWindowDecorViewModel( Context context, @@ -296,7 +298,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, WindowDecorTaskResourceLoader taskResourceLoader, RecentsTransitionHandler recentsTransitionHandler, DesktopModeCompatPolicy desktopModeCompatPolicy, - DesktopTilingDecorViewModel desktopTilingDecorViewModel) { + DesktopTilingDecorViewModel desktopTilingDecorViewModel, + MultiDisplayDragMoveIndicatorController multiDisplayDragMoveIndicatorController) { this( context, shellExecutor, @@ -340,7 +343,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, taskResourceLoader, recentsTransitionHandler, desktopModeCompatPolicy, - desktopTilingDecorViewModel); + desktopTilingDecorViewModel, + multiDisplayDragMoveIndicatorController); } @VisibleForTesting @@ -387,7 +391,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, WindowDecorTaskResourceLoader taskResourceLoader, RecentsTransitionHandler recentsTransitionHandler, DesktopModeCompatPolicy desktopModeCompatPolicy, - DesktopTilingDecorViewModel desktopTilingDecorViewModel) { + DesktopTilingDecorViewModel desktopTilingDecorViewModel, + MultiDisplayDragMoveIndicatorController multiDisplayDragMoveIndicatorController) { mContext = context; mMainExecutor = shellExecutor; mMainHandler = mainHandler; @@ -460,6 +465,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, mDesktopModeCompatPolicy = desktopModeCompatPolicy; mDesktopTilingDecorViewModel = desktopTilingDecorViewModel; mDesktopTasksController.setSnapEventHandler(this); + mMultiDisplayDragMoveIndicatorController = multiDisplayDragMoveIndicatorController; shellInit.addInitCallback(this::onInit, this); } @@ -1759,7 +1765,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, mTransitions, mInteractionJankMonitor, mTransactionFactory, - mMainHandler); + mMainHandler, + mMultiDisplayDragMoveIndicatorController); windowDecoration.setTaskDragResizer(taskPositioner); final DesktopModeTouchEventListener touchEventListener = @@ -2056,7 +2063,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, Transitions transitions, InteractionJankMonitor interactionJankMonitor, Supplier<SurfaceControl.Transaction> transactionFactory, - Handler handler) { + Handler handler, + MultiDisplayDragMoveIndicatorController multiDisplayDragMoveIndicatorController) { final TaskPositioner taskPositioner = DesktopModeStatus.isVeiledResizeEnabled() // TODO(b/383632995): Update when the flag is launched. ? (Flags.enableConnectedDisplaysWindowDrag() @@ -2067,7 +2075,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, dragEventListener, transitions, interactionJankMonitor, - handler) + handler, + multiDisplayDragMoveIndicatorController) : new VeiledResizeTaskPositioner( taskOrganizer, windowDecoration, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositioner.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositioner.kt index bb20292a51d4..c6cb62d153ac 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositioner.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositioner.kt @@ -17,6 +17,7 @@ package com.android.wm.shell.windowdecor import android.graphics.PointF import android.graphics.Rect +import android.hardware.display.DisplayTopology import android.os.Handler import android.os.IBinder import android.os.Looper @@ -32,10 +33,10 @@ import com.android.internal.jank.InteractionJankMonitor import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.MultiDisplayDragMoveBoundsCalculator +import com.android.wm.shell.common.MultiDisplayDragMoveIndicatorController import com.android.wm.shell.shared.annotations.ShellMainThread import com.android.wm.shell.transition.Transitions import java.util.concurrent.TimeUnit -import java.util.function.Supplier /** * A task positioner that also takes into account resizing a @@ -49,11 +50,12 @@ class MultiDisplayVeiledResizeTaskPositioner( private val desktopWindowDecoration: DesktopModeWindowDecoration, private val displayController: DisplayController, dragEventListener: DragPositioningCallbackUtility.DragEventListener, - private val transactionSupplier: Supplier<SurfaceControl.Transaction>, + private val transactionSupplier: () -> SurfaceControl.Transaction, private val transitions: Transitions, private val interactionJankMonitor: InteractionJankMonitor, @ShellMainThread private val handler: Handler, -) : TaskPositioner, Transitions.TransitionHandler { + private val multiDisplayDragMoveIndicatorController: MultiDisplayDragMoveIndicatorController, +) : TaskPositioner, Transitions.TransitionHandler, DisplayController.OnDisplaysChangedListener { private val dragEventListeners = mutableListOf<DragPositioningCallbackUtility.DragEventListener>() private val stableBounds = Rect() @@ -71,6 +73,7 @@ class MultiDisplayVeiledResizeTaskPositioner( private var isResizingOrAnimatingResize = false @Surface.Rotation private var rotation = 0 private var startDisplayId = 0 + private val displayIds = mutableSetOf<Int>() constructor( taskOrganizer: ShellTaskOrganizer, @@ -80,19 +83,22 @@ class MultiDisplayVeiledResizeTaskPositioner( transitions: Transitions, interactionJankMonitor: InteractionJankMonitor, @ShellMainThread handler: Handler, + multiDisplayDragMoveIndicatorController: MultiDisplayDragMoveIndicatorController, ) : this( taskOrganizer, windowDecoration, displayController, dragEventListener, - Supplier<SurfaceControl.Transaction> { SurfaceControl.Transaction() }, + { SurfaceControl.Transaction() }, transitions, interactionJankMonitor, handler, + multiDisplayDragMoveIndicatorController, ) init { dragEventListeners.add(dragEventListener) + displayController.addDisplayWindowListener(this) } override fun onDragPositioningStart(ctrlType: Int, displayId: Int, x: Float, y: Float): Rect { @@ -164,7 +170,7 @@ class MultiDisplayVeiledResizeTaskPositioner( createLongTimeoutJankConfigBuilder(Cuj.CUJ_DESKTOP_MODE_DRAG_WINDOW) ) - val t = transactionSupplier.get() + val t = transactionSupplier() val startDisplayLayout = displayController.getDisplayLayout(startDisplayId) val currentDisplayLayout = displayController.getDisplayLayout(displayId) @@ -196,7 +202,13 @@ class MultiDisplayVeiledResizeTaskPositioner( ) ) - // TODO(b/383069173): Render drag indicator(s) + multiDisplayDragMoveIndicatorController.onDragMove( + boundsDp, + startDisplayId, + desktopWindowDecoration.mTaskInfo, + displayIds, + transactionSupplier, + ) t.setPosition( desktopWindowDecoration.leash, @@ -267,7 +279,10 @@ class MultiDisplayVeiledResizeTaskPositioner( ) ) - // TODO(b/383069173): Clear drag indicator(s) + multiDisplayDragMoveIndicatorController.onDragEnd( + desktopWindowDecoration.mTaskInfo.taskId, + transactionSupplier, + ) } interactionJankMonitor.end(Cuj.CUJ_DESKTOP_MODE_DRAG_WINDOW) @@ -348,6 +363,14 @@ class MultiDisplayVeiledResizeTaskPositioner( dragEventListeners.remove(dragEventListener) } + override fun onTopologyChanged(topology: DisplayTopology) { + // TODO: b/383069173 - Cancel window drag when topology changes happen during drag. + + displayIds.clear() + val displayBounds = topology.getAbsoluteBounds() + displayIds.addAll(List(displayBounds.size()) { displayBounds.keyAt(it) }) + } + companion object { // Timeout used for resize and drag CUJs, this is longer than the default timeout to avoid // timing out in the middle of a resize or drag action. diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/MultiDisplayDragMoveIndicatorControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/MultiDisplayDragMoveIndicatorControllerTest.kt new file mode 100644 index 000000000000..abd238847519 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/MultiDisplayDragMoveIndicatorControllerTest.kt @@ -0,0 +1,168 @@ +/* + * 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.common + +import android.app.ActivityManager.RunningTaskInfo +import android.content.res.Configuration +import android.graphics.Rect +import android.graphics.RectF +import android.testing.TestableResources +import android.view.Display +import android.view.SurfaceControl +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.wm.shell.RootTaskDisplayAreaOrganizer +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.TestShellExecutor +import java.util.function.Supplier +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.kotlin.any +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.times +import org.mockito.kotlin.whenever + +/** + * Tests for [MultiDisplayDragMoveIndicatorController]. + * + * Build/Install/Run: atest WMShellUnitTests:MultiDisplayDragMoveIndicatorControllerTest + */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class MultiDisplayDragMoveIndicatorControllerTest : ShellTestCase() { + private val displayController = mock<DisplayController>() + private val rootTaskDisplayAreaOrganizer = mock<RootTaskDisplayAreaOrganizer>() + private val indicatorSurfaceFactory = mock<MultiDisplayDragMoveIndicatorSurface.Factory>() + private val indicatorSurface0 = mock<MultiDisplayDragMoveIndicatorSurface>() + private val indicatorSurface1 = mock<MultiDisplayDragMoveIndicatorSurface>() + private val transaction = mock<SurfaceControl.Transaction>() + private val transactionSupplier = mock<Supplier<SurfaceControl.Transaction>>() + private val taskInfo = mock<RunningTaskInfo>() + private val display0 = mock<Display>() + private val display1 = mock<Display>() + + private lateinit var resources: TestableResources + private val executor = TestShellExecutor() + + private lateinit var controller: MultiDisplayDragMoveIndicatorController + + @Before + fun setUp() { + resources = mContext.getOrCreateTestableResources() + val resourceConfiguration = Configuration() + resourceConfiguration.uiMode = 0 + resources.overrideConfiguration(resourceConfiguration) + + controller = + MultiDisplayDragMoveIndicatorController( + displayController, + rootTaskDisplayAreaOrganizer, + indicatorSurfaceFactory, + executor, + ) + + val spyDisplayLayout0 = + MultiDisplayTestUtil.createSpyDisplayLayout( + MultiDisplayTestUtil.DISPLAY_GLOBAL_BOUNDS_0, + MultiDisplayTestUtil.DISPLAY_DPI_0, + resources.resources, + ) + val spyDisplayLayout1 = + MultiDisplayTestUtil.createSpyDisplayLayout( + MultiDisplayTestUtil.DISPLAY_GLOBAL_BOUNDS_1, + MultiDisplayTestUtil.DISPLAY_DPI_1, + resources.resources, + ) + + taskInfo.taskId = TASK_ID + whenever(displayController.getDisplayLayout(0)).thenReturn(spyDisplayLayout0) + whenever(displayController.getDisplayLayout(1)).thenReturn(spyDisplayLayout1) + whenever(displayController.getDisplay(0)).thenReturn(display0) + whenever(displayController.getDisplay(1)).thenReturn(display1) + whenever(indicatorSurfaceFactory.create(taskInfo, display0)).thenReturn(indicatorSurface0) + whenever(indicatorSurfaceFactory.create(taskInfo, display1)).thenReturn(indicatorSurface1) + whenever(transactionSupplier.get()).thenReturn(transaction) + } + + @Test + fun onDrag_boundsNotIntersectWithDisplay_noIndicator() { + controller.onDragMove( + RectF(2000f, 2000f, 2100f, 2200f), // not intersect with any display + startDisplayId = 0, + taskInfo, + displayIds = setOf(0, 1), + ) { transaction } + executor.flushAll() + + verify(indicatorSurfaceFactory, never()).create(any(), any()) + } + + @Test + fun onDrag_boundsIntersectWithStartDisplay_noIndicator() { + controller.onDragMove( + RectF(100f, 100f, 200f, 200f), // intersect with display 0 + startDisplayId = 0, + taskInfo, + displayIds = setOf(0, 1), + ) { transaction } + executor.flushAll() + + verify(indicatorSurfaceFactory, never()).create(any(), any()) + } + + @Test + fun onDrag_boundsIntersectWithNonStartDisplay_showAndDisposeIndicator() { + controller.onDragMove( + RectF(100f, -100f, 200f, 200f), // intersect with display 0 and 1 + startDisplayId = 0, + taskInfo, + displayIds = setOf(0, 1), + ) { transaction } + executor.flushAll() + + verify(indicatorSurfaceFactory, times(1)).create(taskInfo, display1) + verify(indicatorSurface1, times(1)) + .show(transaction, taskInfo, rootTaskDisplayAreaOrganizer, 1, Rect(0, 1800, 200, 2400)) + + controller.onDragMove( + RectF(2000f, 2000f, 2100f, 2200f), // not intersect with display 1 + startDisplayId = 0, + taskInfo, + displayIds = setOf(0, 1) + ) { transaction } + while (executor.callbacks.isNotEmpty()) { + executor.flushAll() + } + + verify(indicatorSurface1, times(1)) + .relayout(any(), eq(transaction), shouldBeVisible = eq(false)) + + controller.onDragEnd(TASK_ID, { transaction }) + while (executor.callbacks.isNotEmpty()) { + executor.flushAll() + } + + verify(indicatorSurface1, times(1)).disposeSurface(transaction) + } + + companion object { + private const val TASK_ID = 10 + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt index 8cccdb2b6120..81dfaed56b6f 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt @@ -52,6 +52,7 @@ import com.android.wm.shell.common.DisplayChangeController import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayInsetsController import com.android.wm.shell.common.DisplayLayout +import com.android.wm.shell.common.MultiDisplayDragMoveIndicatorController import com.android.wm.shell.common.MultiInstanceHelper import com.android.wm.shell.common.SyncTransactionQueue import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler @@ -138,6 +139,8 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() { protected val mockFreeformTaskTransitionStarter = mock<FreeformTaskTransitionStarter>() protected val mockActivityOrientationChangeHandler = mock<DesktopActivityOrientationChangeHandler>() + protected val mockMultiDisplayDragMoveIndicatorController = + mock<MultiDisplayDragMoveIndicatorController>() protected val mockInputManager = mock<InputManager>() private val mockTaskPositionerFactory = mock<DesktopModeWindowDecorViewModel.TaskPositionerFactory>() @@ -229,6 +232,7 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() { mockRecentsTransitionHandler, desktopModeCompatPolicy, mockTilingWindowDecoration, + mockMultiDisplayDragMoveIndicatorController, ) desktopModeWindowDecorViewModel.setSplitScreenController(mockSplitScreenController) whenever(mockDisplayController.getDisplayLayout(any())).thenReturn(mockDisplayLayout) @@ -243,6 +247,7 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() { any(), any(), any(), + any(), any() ) ) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositionerTest.kt index 937938df82c8..a6b077037b86 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositionerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositionerTest.kt @@ -41,6 +41,7 @@ import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.ShellTestCase import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayLayout +import com.android.wm.shell.common.MultiDisplayDragMoveIndicatorController import com.android.wm.shell.common.MultiDisplayTestUtil import com.android.wm.shell.transition.Transitions import com.android.wm.shell.transition.Transitions.TransitionFinishCallback @@ -62,8 +63,8 @@ import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.times import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` as whenever import org.mockito.Mockito.`when` +import org.mockito.Mockito.`when` as whenever import org.mockito.MockitoAnnotations /** @@ -93,7 +94,8 @@ class MultiDisplayVeiledResizeTaskPositionerTest : ShellTestCase() { @Mock private lateinit var mockTransitions: Transitions @Mock private lateinit var mockInteractionJankMonitor: InteractionJankMonitor @Mock private lateinit var mockSurfaceControl: SurfaceControl - + @Mock private lateinit var mockMultiDisplayDragMoveIndicatorController: + MultiDisplayDragMoveIndicatorController private lateinit var resources: TestableResources private lateinit var spyDisplayLayout0: DisplayLayout private lateinit var spyDisplayLayout1: DisplayLayout @@ -170,10 +172,11 @@ class MultiDisplayVeiledResizeTaskPositionerTest : ShellTestCase() { mockDesktopWindowDecoration, mockDisplayController, mockDragEventListener, - mockTransactionFactory, + { mockTransaction }, mockTransitions, mockInteractionJankMonitor, mainHandler, + mockMultiDisplayDragMoveIndicatorController, ) } |