Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 5b9a78f0 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Render drag move indicator" into main

parents aea90982 5246e9e7
Loading
Loading
Loading
Loading
+124 −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.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()
            }
        }
    }
}
+151 −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.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)
            }
        }
    }
}
+28 −2
Original line number Diff line number Diff line
@@ -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
+15 −6
Original line number Diff line number Diff line
@@ -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,
+30 −7
Original line number Diff line number Diff line
@@ -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.
Loading