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

Commit cbedd782 authored by Qijing Yao's avatar Qijing Yao
Browse files

Delay desktop mode visual indicator update for across display drag

When dragging a task near the edge of a display towards an adjacent
display, the visual indicator could show and disappear rapidly. This
could be visually jarring before the user fully committed to a
cross-display drag or snapping action.

This commit introduces `VisualIndicatorUpdateScheduler` to manage
updates for the desktop mode visual indicator onDragPositioningMove,
which decides whether to update the indicator immediately or delay the
update based on the indicator type and topology.

Bug: 389868684
Test: atest VisualIndicatorUpdateSchedulerTest DesktopTasksControllerTest
Test: go/cd-smoke
Flag: com.android.window.flags.enable_connected_displays_window_drag
Change-Id: I98a98de0728178871767a88f5f1d624da6a6d3c2
parent 7a79cec7
Loading
Loading
Loading
Loading
+16 −2
Original line number Diff line number Diff line
@@ -114,6 +114,7 @@ import com.android.wm.shell.desktopmode.OverviewToDesktopTransitionObserver;
import com.android.wm.shell.desktopmode.ReturnToDragStartAnimator;
import com.android.wm.shell.desktopmode.SpringDragToDesktopTransitionHandler;
import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler;
import com.android.wm.shell.desktopmode.VisualIndicatorUpdateScheduler;
import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository;
import com.android.wm.shell.desktopmode.compatui.SystemModalsTransitionHandler;
import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider;
@@ -834,7 +835,8 @@ public abstract class WMShellModule {
            DesktopModeMoveToDisplayTransitionHandler moveToDisplayTransitionHandler,
            HomeIntentProvider homeIntentProvider,
            DesktopState desktopState,
            DesktopConfig desktopConfig) {
            DesktopConfig desktopConfig,
            VisualIndicatorUpdateScheduler visualIndicatorUpdateScheduler) {
        return new DesktopTasksController(
                context,
                shellInit,
@@ -880,7 +882,8 @@ public abstract class WMShellModule {
                moveToDisplayTransitionHandler,
                homeIntentProvider,
                desktopState,
                desktopConfig);
                desktopConfig,
                visualIndicatorUpdateScheduler);
    }

    @WMSingleton
@@ -1612,6 +1615,17 @@ public abstract class WMShellModule {
                        animExecutor, context, shellInit));
    }

    @WMSingleton
    @Provides
    static VisualIndicatorUpdateScheduler provideVisualIndicatorUpdateScheduler(
            ShellInit shellInit,
            @ShellMainThread MainCoroutineDispatcher mainDispatcher,
            @ShellBackgroundThread CoroutineScope bgScope,
            DisplayController displayController) {
        return new VisualIndicatorUpdateScheduler(shellInit, mainDispatcher, bgScope,
                displayController);
    }

    //
    // App zoom out
    //
+25 −5
Original line number Diff line number Diff line
@@ -130,6 +130,7 @@ public class DesktopModeVisualIndicator {
    private final SnapEventHandler mSnapEventHandler;

    private final boolean mUseSmallTabletRegions;
    private boolean mIsReleased = false;
    /**
     * Ordered list of {@link Rect} zones that we will match an input coordinate against.
     * List is traversed from first to last element. The first rect that contains the input event
@@ -235,6 +236,7 @@ public class DesktopModeVisualIndicator {

    /** Release the visual indicator view and its viewhost. */
    public void releaseVisualIndicator() {
        mIsReleased = true;
        mVisualIndicatorViewContainer.releaseVisualIndicator();
    }

@@ -253,23 +255,41 @@ public class DesktopModeVisualIndicator {

    /**
     * Based on the coordinates of the current drag event, determine which indicator type we should
     * display, including no visible indicator.
     * display, including no visible indicator, and update the indicator.
     */
    @NonNull
    IndicatorType updateIndicatorType(PointF inputCoordinates) {
        final IndicatorType result = calculateIndicatorType(inputCoordinates);
        updateIndicatorWithType(result);
        return result;
    }

    /**
     * Based on the coordinates of the current drag event, determine which indicator type we should
     * display, including no visible indicator.
     */
    @NonNull
    IndicatorType calculateIndicatorType(PointF inputCoordinates) {
        final IndicatorType result;
        if (mUseSmallTabletRegions) {
            result = getIndicatorSmallTablet(inputCoordinates);
        } else {
            result = getIndicatorLargeTablet(inputCoordinates);
        }
        if (mDragStartState != DragStartState.DRAGGED_INTENT) {
        return result;
    }

    /**
     * Update the indicator based on IndicatorType.
     */
    @NonNull
    void updateIndicatorWithType(IndicatorType type) {
        if (!mIsReleased && mDragStartState != DragStartState.DRAGGED_INTENT) {
            mVisualIndicatorViewContainer.transitionIndicator(
                    mTaskInfo, mDisplayController, mCurrentType, result
                    mTaskInfo, mDisplayController, mCurrentType, type
            );
            mCurrentType = result;
            mCurrentType = type;
        }
        return result;
    }

    @NonNull
+35 −7
Original line number Diff line number Diff line
@@ -232,6 +232,7 @@ class DesktopTasksController(
    private val homeIntentProvider: HomeIntentProvider,
    private val desktopState: DesktopState,
    private val desktopConfig: DesktopConfig,
    private val visualIndicatorUpdateScheduler: VisualIndicatorUpdateScheduler,
) :
    RemoteCallable<DesktopTasksController>,
    Transitions.TransitionHandler,
@@ -3710,10 +3711,12 @@ class DesktopTasksController(
        taskInfo: RunningTaskInfo,
        taskSurface: SurfaceControl,
        inputX: Float,
        inputY: Float,
        taskBounds: Rect,
    ) {
        if (taskInfo.windowingMode != WINDOWING_MODE_FREEFORM) return
        snapEventHandler.removeTaskIfTiled(taskInfo.displayId, taskInfo.taskId)
        if (!DesktopExperienceFlags.ENABLE_CONNECTED_DISPLAYS_WINDOW_DRAG.isTrue()) {
            updateVisualIndicator(
                taskInfo,
                taskSurface,
@@ -3721,6 +3724,21 @@ class DesktopTasksController(
                taskBounds.top.toFloat(),
                DragStartState.FROM_FREEFORM,
            )
            return
        }

        val indicator =
            getOrCreateVisualIndicator(taskInfo, taskSurface, DragStartState.FROM_FREEFORM)
        val indicatorType =
            indicator.calculateIndicatorType(PointF(inputX, taskBounds.top.toFloat()))
        visualIndicatorUpdateScheduler.schedule(
            taskInfo.displayId,
            indicatorType,
            inputX,
            inputY,
            taskBounds,
            indicator,
        )
    }

    fun updateVisualIndicator(
@@ -3729,7 +3747,17 @@ class DesktopTasksController(
        inputX: Float,
        taskTop: Float,
        dragStartState: DragStartState,
    ): DesktopModeVisualIndicator.IndicatorType {
    ): IndicatorType {
        return getOrCreateVisualIndicator(taskInfo, taskSurface, dragStartState)
            .updateIndicatorType(PointF(inputX, taskTop))
    }

    @VisibleForTesting
    fun getOrCreateVisualIndicator(
        taskInfo: RunningTaskInfo,
        taskSurface: SurfaceControl?,
        dragStartState: DragStartState,
    ): DesktopModeVisualIndicator {
        // If the visual indicator has the wrong start state, it was never cleared from a previous
        // drag event and needs to be cleared
        if (visualIndicator != null && visualIndicator?.dragStartState != dragStartState) {
@@ -3757,7 +3785,7 @@ class DesktopTasksController(
                    snapEventHandler,
                )
        if (visualIndicator == null) visualIndicator = indicator
        return indicator.updateIndicatorType(PointF(inputX, taskTop))
        return indicator
    }

    /**
+186 −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.desktopmode

import android.graphics.Rect
import android.hardware.display.DisplayTopology
import android.hardware.display.DisplayTopology.TreeNode.POSITION_LEFT
import android.hardware.display.DisplayTopology.TreeNode.POSITION_RIGHT
import android.hardware.display.DisplayTopology.TreeNode.POSITION_TOP
import android.hardware.display.DisplayTopologyGraph
import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType
import com.android.wm.shell.shared.annotations.ShellBackgroundThread
import com.android.wm.shell.shared.annotations.ShellMainThread
import com.android.wm.shell.sysui.ShellInit
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

/**
 * Manages scheduling updates for the visual indicator shown during task drags in desktop mode.
 *
 * This class introduces a delay before updating the indicator if the drag gesture is potentially
 * moving towards an adjacent display (cross-display drag). This prevents flickering or premature
 * updates while the user is dragging near the edge.
 */
class VisualIndicatorUpdateScheduler(
    shellInit: ShellInit,
    @ShellMainThread private val mainDispatcher: CoroutineDispatcher,
    @ShellBackgroundThread private val bgScope: CoroutineScope,
    private val displayController: DisplayController,
) {
    private var updateJob: Job? = null
    private val previousBounds = Rect()
    private var previousIndicatorType = IndicatorType.NO_INDICATOR
    private var displayTopologyGraph: DisplayTopologyGraph? = null

    private val displayTopologyListener =
        object : DisplayController.OnDisplaysChangedListener {
            override fun onTopologyChanged(topology: DisplayTopology?) {
                displayTopologyGraph = topology?.getGraph()
            }
        }

    init {
        shellInit.addInitCallback({ onInit() }, this)
    }

    private fun onInit() {
        displayController.addDisplayWindowListener(displayTopologyListener)
    }

    /**
     * Requests an update for the visual indicator based on the current drag state.
     *
     * This function determines whether to update the indicator immediately or schedule a delayed
     * update. A delay is introduced if the drag gesture, defined by the pointer coordinates
     * [inputX] and [inputY] (in pixels relative to the display identified by [displayId]), is
     * potentially moving towards an adjacent display, considering the requested [indicatorType]
     * (e.g., fullscreen, split).
     *
     * An immediate update occurs if the drag is not deemed a potential cross-display move. If a
     * delay *is* scheduled, it can be preempted and updated immediately if the dragged task's
     * bounds, provided in [taskBounds], change significantly compared to the last recorded bounds.
     *
     * The actual UI update logic should be encapsulated in the [performUpdateAction] lambda, which
     * will be executed either immediately or after the delay (on the [mainDispatcher] if delayed).
     */
    fun schedule(
        displayId: Int,
        indicatorType: IndicatorType,
        inputX: Float,
        inputY: Float,
        taskBounds: Rect,
        visualIndicator: DesktopModeVisualIndicator?,
    ) {
        if (!isPotentialCrossDisplayDrag(displayId, indicatorType, inputX, inputY)) {
            updateJob?.cancel()
            visualIndicator?.updateIndicatorWithType(indicatorType)
            return
        }

        if (previousIndicatorType != indicatorType || didBoundsChangeSignificantly(taskBounds)) {
            updateJob?.cancel()
            updateJob =
                bgScope.launch {
                    if (!isActive) return@launch
                    delay(timeMillis = DELAY_MILLIS)
                    withContext(mainDispatcher) {
                        if (!isActive) return@withContext
                        visualIndicator?.updateIndicatorWithType(indicatorType)
                    }
                }
        }

        previousIndicatorType = indicatorType
        previousBounds.set(taskBounds)
    }

    private fun isPotentialCrossDisplayDrag(
        displayId: Int,
        indicatorType: IndicatorType,
        inputX: Float,
        inputY: Float,
    ): Boolean {
        return when (indicatorType) {
            IndicatorType.TO_FULLSCREEN_INDICATOR ->
                isCursorNearAdjacentDisplayEdge(displayId, POSITION_TOP, inputX, inputY)
            IndicatorType.TO_SPLIT_LEFT_INDICATOR ->
                isCursorNearAdjacentDisplayEdge(displayId, POSITION_LEFT, inputX, inputY)
            IndicatorType.TO_SPLIT_RIGHT_INDICATOR ->
                isCursorNearAdjacentDisplayEdge(displayId, POSITION_RIGHT, inputX, inputY)
            // Ignore indicators that don't represent dragging towards a relevant display edge.
            // TO_FULLSCREEN (top edge), TO_SPLIT_LEFT (left edge), and TO_SPLIT_RIGHT (right edge)
            // are the only types considered for potential cross-display transitions.
            else -> false
        }
    }

    private fun isCursorNearAdjacentDisplayEdge(
        displayId: Int,
        position: Int,
        inputX: Float,
        inputY: Float,
    ): Boolean {
        val adjacentDisplays =
            displayTopologyGraph
                ?.displayNodes
                ?.find { node -> node.displayId == displayId }
                ?.adjacentDisplays ?: return false
        val adjacentDisplayId =
            adjacentDisplays.find { adjDisplay -> adjDisplay.position == position }?.displayId
                ?: return false

        val currentDisplayLayout = displayController.getDisplayLayout(displayId) ?: return false

        val adjacentDisplayLayout =
            displayController.getDisplayLayout(adjacentDisplayId) ?: return false

        val currentBounds = currentDisplayLayout.globalBoundsDp()
        val adjacentBounds = adjacentDisplayLayout.globalBoundsDp()
        return if (position == POSITION_TOP) {
            // Horizontal border: Calculate horizontal overlap and check inputX
            val overlapStart = max(currentBounds.left, adjacentBounds.left)
            val overlapEnd = min(currentBounds.right, adjacentBounds.right)
            currentDisplayLayout.pxToDp(inputX) in overlapStart..overlapEnd
        } else {
            // Vertical border (must be LEFT or RIGHT): Calculate vertical overlap and check inputY
            val overlapStart = max(currentBounds.top, adjacentBounds.top)
            val overlapEnd = min(currentBounds.bottom, adjacentBounds.bottom)
            currentDisplayLayout.pxToDp(inputY) in overlapStart..overlapEnd
        }
    }

    private fun didBoundsChangeSignificantly(currentBounds: Rect) =
        abs(currentBounds.left - previousBounds.left) > BOUNDS_CHANGE_THRESHOLD_PX ||
            abs(currentBounds.top - previousBounds.top) > BOUNDS_CHANGE_THRESHOLD_PX ||
            abs(currentBounds.right - previousBounds.right) > BOUNDS_CHANGE_THRESHOLD_PX ||
            abs(currentBounds.bottom - previousBounds.bottom) > BOUNDS_CHANGE_THRESHOLD_PX

    companion object {
        private const val DELAY_MILLIS: Long = 800L
        private const val BOUNDS_CHANGE_THRESHOLD_PX = 5
    }
}
+1 −0
Original line number Diff line number Diff line
@@ -1357,6 +1357,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
                    mDesktopTasksController.onDragPositioningMove(taskInfo,
                            decoration.mTaskSurface,
                            e.getRawX(dragPointerIdx),
                            e.getRawY(dragPointerIdx),
                            newTaskBounds);
                    //  Flip mIsDragging only if the bounds actually changed.
                    if (mIsDragging || !newTaskBounds.equals(mOnDragStartInitialBounds)) {
Loading