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

Commit 679c1336 authored by Mykola Podolian's avatar Mykola Podolian Committed by Android (Google) Code Review
Browse files

Merge "Fix for shell-handled drag interruption." into main

parents b4db3a5f 47aff7e8
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)
    }