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

Commit 47aff7e8 authored by mpodolian's avatar mpodolian
Browse files

Fix for shell-handled drag interruption.

When a drag was handled in the shell, the DragLayout.hide() method
called the DragToBubbleController.onDragEnded() method, which ended drop
target management.

This change introduces hideDropTargets() methods to properly hide the
drop targets without ending drag handling.

Fixes: 427297270
Test: DragToBubbleControllerTest
Flag: com.android.wm.shell.enable_create_any_bubble
Change-Id: I88a1b67b72682b8ba948f95d1eaf3107f2cd5810
parent 6024882b
Loading
Loading
Loading
Loading
+24 −5
Original line number Diff line number Diff line
@@ -129,7 +129,7 @@ class DragToBubbleControllerTest {
    @Test
    fun droppedItemWithIntentAtTheLeftDropZone_noBubblesOnTheRight_bubbleCreationRequested() {
        val bubbleBarOriginalLocation = BubbleBarLocation.RIGHT
        prepareBubbleController(hasBubbles = false, bubbleBarLocation = bubbleBarOriginalLocation)
        prepareBubbleController(bubbleBarLocation = bubbleBarOriginalLocation)
        val pendingIntent = PendingIntent(mock<IIntentSender>())
        val userHandle = UserHandle(0)

@@ -148,7 +148,7 @@ class DragToBubbleControllerTest {
    @Test
    fun droppedItemWithShortcutInfoAtTheLeftDropZone_noBubblesOnTheRight_bubbleCreationRequested() {
        val bubbleBarOriginalLocation = BubbleBarLocation.RIGHT
        prepareBubbleController(hasBubbles = false, bubbleBarLocation = bubbleBarOriginalLocation)
        prepareBubbleController(bubbleBarLocation = bubbleBarOriginalLocation)
        val shortcutInfo = ShortcutInfo.Builder(context, "id").setLongLabel("Shortcut").build()

        dragToBubbleController.onDragStarted()
@@ -165,7 +165,7 @@ class DragToBubbleControllerTest {
    @Test
    fun droppedItem_afterNewDragStartedOnItemDropCleared() {
        val bubbleBarOriginalLocation = BubbleBarLocation.RIGHT
        prepareBubbleController(hasBubbles = false, bubbleBarLocation = bubbleBarOriginalLocation)
        prepareBubbleController(bubbleBarLocation = bubbleBarOriginalLocation)
        val shortcutInfo = ShortcutInfo.Builder(context, "id").setLongLabel("Shortcut").build()
        runOnMainSync {
            dragToBubbleController.onDragStarted()
@@ -191,16 +191,35 @@ class DragToBubbleControllerTest {
            .expandStackAndSelectBubble(any<PendingIntent>(), any(), any())
    }

    @Test
    fun hideDropTargets_dragEnteredDropZone_dropTargetsHidden() {
        // Given
        dragToBubbleController.onDragStarted()
        runOnMainSync {
            dragToBubbleController.onDragUpdate(rightDropRect.centerX(), rightDropRect.centerY())
            animatorTestRule.advanceTimeBy(250)
        }
        assertThat(dropTargetContainer.childCount).isEqualTo(1)
        assertThat(dropTargetView.alpha).isEqualTo(1f)

        // When
        runOnMainSync {
            dragToBubbleController.hideDropTargets()
            animatorTestRule.advanceTimeBy(250)
        }

        // Then
        assertThat(dropTargetView.alpha).isEqualTo(0f)
    }

    private fun runOnMainSync(action: () -> Unit) {
        InstrumentationRegistry.getInstrumentation().runOnMainSync { action() }
    }

    private fun prepareBubbleController(
        hasBubbles: Boolean = false,
        bubbleBarLocation: BubbleBarLocation = BubbleBarLocation.RIGHT,
    ) {
        bubbleController.stub {
            on { hasBubbles() } doReturn hasBubbles
            on { getBubbleBarLocation() } doReturn bubbleBarLocation
        }
    }
+23 −1
Original line number Diff line number Diff line
@@ -26,6 +26,8 @@ import androidx.annotation.VisibleForTesting
import androidx.core.animation.Animator
import androidx.core.animation.AnimatorListenerAdapter
import androidx.core.animation.ValueAnimator
import com.android.wm.shell.shared.bubbles.DragZone.Bounds.CircleZone
import com.android.wm.shell.shared.bubbles.DragZone.Bounds.RectZone
import com.android.wm.shell.shared.bubbles.DragZone.DropTargetRect
import com.android.wm.shell.shared.bubbles.DraggedObject.Bubble
import com.android.wm.shell.shared.bubbles.DraggedObject.BubbleBar
@@ -110,6 +112,26 @@ class DropTargetManager(
        return newDragZone
    }

    /**
     * Called when the drop target views should be hidden. This method will not remove the drop
     * target views from the container.
     *
     * It is mandatory to call [onDragEnded] when the drag operation ends, ensuring that the drop
     * target views are removed from the container.
     */
    fun hideDropTargets() {
        val dragZones = state?.dragZones
        if (dragZones.isNullOrEmpty()) return
        val lowestBottom = dragZones.map { it.bounds }.maxOf { dragZone ->
            when (dragZone) {
                is RectZone -> dragZone.rect.bottom // rect bottom
                is CircleZone -> dragZone.y - dragZone.radius // circle bottom
            }
        }
        // use coordinate that will not hit any drag zone, so drop targets will be hidden
        onDragUpdated(0, lowestBottom + 1)
    }

    /** Called when the drag ended. */
    fun onDragEnded() {
        val dropState = state ?: return
@@ -219,7 +241,7 @@ class DropTargetManager(

    /** Stores the current drag state. */
    private inner class DragState(
        private val dragZones: List<DragZone>,
        val dragZones: List<DragZone>,
        val draggedObject: DraggedObject,
    ) {
        val initialDragZone =
+5 −0
Original line number Diff line number Diff line
@@ -91,6 +91,11 @@ class DragToBubbleController(
        return lastDragZone != null
    }

    /** Called when drop targets should be hidden. */
    fun hideDropTargets() {
        dropTargetManager.hideDropTargets()
    }

    /** Called when the item with the [ShortcutInfo] is dropped over the bubble bar drop target. */
    fun onItemDropped(shortcutInfo: ShortcutInfo) {
        val dropLocation = lastDragZone?.getBubbleBarLocation() ?: return
+2 −2
Original line number Diff line number Diff line
@@ -637,9 +637,9 @@ public class DragLayout extends LinearLayout
            }
        });
        if (mIsOverBubblesDropZone) {
            // bubble bar is still showing drop target, notify bubbles of drag cancel
            // bubble bar is still showing drop target, notify bubbles to hide drop targets
            mIsOverBubblesDropZone = false;
            Objects.requireNonNull(mDragToBubbleController).onDragEnded();
            Objects.requireNonNull(mDragToBubbleController).hideDropTargets();
        }
        // Reset the state if we previously force-ignore the bottom margin
        mDropZoneView1.setForceIgnoreBottomMargin(false);
+143 −0
Original line number Diff line number Diff line
@@ -655,6 +655,149 @@ class DropTargetManagerTest {
        assertThat(container.childCount).isEqualTo(randomViewsCount) // All managers views removed
    }

    @Test
    fun hideDropTargets_whenInAZone_notifiesAndHidesDropTarget() {
        dropTargetManager.onDragStarted(
            DraggedObject.LauncherIcon(bubbleBarHasBubbles = true) {},
            listOf(bubbleLeftDragZone, bubbleRightDragZone)
        )
        // Initially, drag into the left zone
        InstrumentationRegistry.getInstrumentation().runOnMainSync {
            val dragZone = dropTargetManager.onDragUpdated(
                bubbleLeftDragZone.bounds.rect.centerX(),
                bubbleLeftDragZone.bounds.rect.centerY()
            )
            assertThat(dragZone).isEqualTo(bubbleLeftDragZone)
            animatorTestRule.advanceTimeBy(250)
        }
        assertThat(dropTargetView.alpha).isEqualTo(1f)
        assertThat(dragZoneChangedListener.toDragZone).isEqualTo(bubbleLeftDragZone)

        // Call hideDropTargets
        InstrumentationRegistry.getInstrumentation().runOnMainSync {
            dropTargetManager.hideDropTargets()
            animatorTestRule.advanceTimeBy(250)
        }

        // Verify listener was notified of leaving the zone
        assertThat(dragZoneChangedListener.fromDragZone).isEqualTo(bubbleLeftDragZone)
        assertThat(dragZoneChangedListener.toDragZone).isNull()
        // Verify drop target is hidden and container still has the view
        assertThat(dropTargetView.alpha).isEqualTo(0f)
        assertThat(container.childCount).isEqualTo(DROP_VIEWS_COUNT)
    }

    @Test
    fun hideDropTargets_whenInAZoneWithSecondDropTarget_notifiesAndHidesBothDropTargets() {
        dropTargetManager.onDragStarted(
            DraggedObject.LauncherIcon(bubbleBarHasBubbles = false) {},
            listOf(bubbleLeftDragZoneWithSecondDropTarget, bubbleRightDragZoneWithSecondDropTarget)
        )
        // Initially, drag into the left zone
        InstrumentationRegistry.getInstrumentation().runOnMainSync {
            val dragZone = dropTargetManager.onDragUpdated(
                bubbleLeftDragZoneWithSecondDropTarget.bounds.rect.centerX(),
                bubbleLeftDragZoneWithSecondDropTarget.bounds.rect.centerY()
            )
            assertThat(dragZone).isEqualTo(bubbleLeftDragZoneWithSecondDropTarget)
            animatorTestRule.advanceTimeBy(250)
        }
        assertThat(dropTargetView.alpha).isEqualTo(1f)
        assertThat(secondDropTargetView!!.alpha).isEqualTo(1f)
        assertThat(dragZoneChangedListener.toDragZone).isEqualTo(
            bubbleLeftDragZoneWithSecondDropTarget
        )

        // Call hideDropTargets
        InstrumentationRegistry.getInstrumentation().runOnMainSync {
            dropTargetManager.hideDropTargets()
            animatorTestRule.advanceTimeBy(250)
        }

        // Verify listener was notified of leaving the zone
        assertThat(dragZoneChangedListener.fromDragZone).isEqualTo(
            bubbleLeftDragZoneWithSecondDropTarget
        )
        assertThat(dragZoneChangedListener.toDragZone).isNull()
        // Verify drop targets are hidden and container still has the views
        assertThat(dropTargetView.alpha).isEqualTo(0f)
        assertThat(secondDropTargetView!!.alpha).isEqualTo(0f)
        assertThat(container.childCount).isEqualTo(DROP_VIEWS_COUNT_FOR_TWO_DROP_TARGETS)
    }

    @Test
    fun hideDropTargets_whenAlreadyOutsideZones_doesNothingAndDoesNotNotify() {
        dropTargetManager.onDragStarted(
            DraggedObject.Bubble(BubbleBarLocation.LEFT),
            listOf(bubbleLeftDragZone, bubbleRightDragZone)
        )
        // Initially, drag outside all zones
        InstrumentationRegistry.getInstrumentation().runOnMainSync {
            val dragZone = dropTargetManager.onDragUpdated(
                500, // outside any defined zone
                500
            )
            assertThat(dragZone).isNull()
            animatorTestRule.advanceTimeBy(250)
        }
        assertThat(dropTargetView.alpha).isEqualTo(0f)
        assertThat(dragZoneChangedListener.fromDragZone).isEqualTo(bubbleLeftDragZone)
        assertThat(dragZoneChangedListener.toDragZone).isNull()

        // Reset listener state
        dragZoneChangedListener.fromDragZone = null
        dragZoneChangedListener.toDragZone = null

        // Call hideDropTargets
        InstrumentationRegistry.getInstrumentation().runOnMainSync {
            // No animation should occur as it's already hidden
            dropTargetManager.hideDropTargets()
        }

        // Verify listener was NOT notified again (already outside)
        assertThat(dragZoneChangedListener.fromDragZone).isNull()
        assertThat(dragZoneChangedListener.toDragZone).isNull()
        // Verify drop target remains hidden
        assertThat(dropTargetView.alpha).isEqualTo(0f)
        assertThat(container.childCount).isEqualTo(DROP_VIEWS_COUNT)
    }

    @Test
    fun hideDropTargets_dragNotStarted_doesNothing() {
        // Call hideDropTargets
        InstrumentationRegistry.getInstrumentation().runOnMainSync {
            dropTargetManager.hideDropTargets()
        }

        // Verify no listener calls
        assertThat(dragZoneChangedListener.initialDragZone).isNull()
        assertThat(dragZoneChangedListener.fromDragZone).isNull()
        assertThat(dragZoneChangedListener.toDragZone).isNull()
        assertThat(container.childCount).isEqualTo(0)
    }

    @Test
    fun hideDropTargets_afterDragEnded_doesNothing() {
        dropTargetManager.onDragStarted(
            DraggedObject.Bubble(BubbleBarLocation.LEFT),
            listOf(bubbleLeftDragZone, bubbleRightDragZone)
        )
        InstrumentationRegistry.getInstrumentation().runOnMainSync {
            dropTargetManager.onDragEnded()
            animatorTestRule.advanceTimeBy(250)
        }
        assertThat(container.childCount).isEqualTo(0) // Views removed on drag end

        // Call hideDropTargets
        InstrumentationRegistry.getInstrumentation().runOnMainSync {
            dropTargetManager.hideDropTargets()
        }

        // Drop target alpha is irrelevant as views are removed.
        // Listener should not be affected further.
        assertThat(dragZoneChangedListener.endedDragZone).isEqualTo(bubbleLeftDragZone) // From onDragEnded
    }

    private fun verifyDropTargetPosition(rect: Rect) {
        verifyDropTargetPosition(dropTargetView, rect)
    }