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

Commit f3f47b74 authored by mpodolian's avatar mpodolian
Browse files

Added support for a second drop target view in DropTargetManager

This change introduces support for managing a second drop target view
within the DropTargetManager. Also added tests that verify updated
behavior.

Bug: 403363673
Flag: com.android.wm.shell.enable_create_any_bubble
Test: DropTargetManagerTest
Change-Id: Ibffcd85184ce67923c9ad6c26123f48c5adc29a2
parent d47f6d69
Loading
Loading
Loading
Loading
+2 −1
Original line number Diff line number Diff line
@@ -25,5 +25,6 @@ sealed interface DraggedObject {

    data class ExpandedView(val initialLocation: BubbleBarLocation) : DraggedObject

    data class LauncherIcon(val bubbleBarHasBubbles: Boolean) : DraggedObject
    data class LauncherIcon(val bubbleBarHasBubbles: Boolean, val onDropAction: Runnable) :
        DraggedObject
}
+80 −34
Original line number Diff line number Diff line
@@ -19,8 +19,10 @@ package com.android.wm.shell.shared.bubbles
import android.content.Context
import android.graphics.RectF
import android.util.TypedValue
import android.view.View
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.widget.FrameLayout
import androidx.annotation.VisibleForTesting
import androidx.core.animation.Animator
import androidx.core.animation.AnimatorListenerAdapter
import androidx.core.animation.ValueAnimator
@@ -41,10 +43,12 @@ class DropTargetManager(
) {

    private var state: DragState? = null
    private val dropTargetView = DropTargetView(context)
    private var animator: ValueAnimator? = null

    @VisibleForTesting val dropTargetView = DropTargetView(context)
    @VisibleForTesting var secondDropTargetView: DropTargetView? = null
    private var morphRect: RectF = RectF(0f, 0f, 0f, 0f)
    private val isLayoutRtl = container.isLayoutRtl
    private val viewAnimatorsMap = mutableMapOf<View, ValueAnimator>()

    private companion object {
        const val MORPH_ANIM_DURATION = 250L
@@ -58,26 +62,38 @@ class DropTargetManager(
        val state = DragState(dragZones, draggedObject)
        dragZoneChangedListener.onInitialDragZoneSet(state.initialDragZone)
        this.state = state
        animator?.cancel()
        setupDropTarget()
        viewAnimatorsMap.values.forEach { it.cancel() }
        setupDropTarget(dropTargetView)
        if (dragZones.any { it.secondDropTarget != null }) {
            secondDropTargetView = secondDropTargetView ?: DropTargetView(context)
            setupDropTarget(secondDropTargetView)
        } else {
            secondDropTargetView?.let { container.removeView(it) }
            secondDropTargetView = null
        }
    }

    private fun setupDropTarget() {
        if (dropTargetView.parent != null) container.removeView(dropTargetView)
        container.addView(dropTargetView, 0)
        dropTargetView.alpha = 0f
    private fun setupDropTarget(view: View?) {
        if (view == null) return
        if (view.parent != null) container.removeView(view)
        container.addView(view, 0)
        view.alpha = 0f

        dropTargetView.elevation = TypedValue.applyDimension(
        view.elevation = TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_DIP,
            DROP_TARGET_ELEVATION_DP, context.resources.displayMetrics
        )
        // Match parent and the target is drawn within the view
        dropTargetView.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
        view.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
    }

    /** Called when the user drags to a new location. */
    fun onDragUpdated(x: Int, y: Int) {
        val state = state ?: return
    /**
     * Called when the user drags to a new location.
     *
     * @return DragZone that matches provided x and y coordinates.
     */
    fun onDragUpdated(x: Int, y: Int): DragZone? {
        val state = state ?: return null
        val oldDragZone = state.currentDragZone
        val newDragZone = state.getMatchingDragZone(x = x, y = y)
        state.currentDragZone = newDragZone
@@ -85,50 +101,78 @@ class DropTargetManager(
            dragZoneChangedListener.onDragZoneChanged(
                draggedObject = state.draggedObject,
                from = oldDragZone,
                to = newDragZone
                to = newDragZone,
            )
            updateDropTarget()
        }
        return newDragZone
    }

    /** Called when the drag ended. */
    fun onDragEnded() {
        val dropState = state ?: return
        startFadeAnimation(from = dropTargetView.alpha, to = 0f) {
            container.removeView(dropTargetView)
        startFadeAnimation(dropTargetView, to = 0f) { container.removeView(dropTargetView) }
        startFadeAnimation(secondDropTargetView, to = 0f) {
            container.removeView(secondDropTargetView)
            secondDropTargetView = null
        }
        dragZoneChangedListener.onDragEnded(dropState.currentDragZone)
        state = null
    }

    private fun updateDropTarget() {
        val currentDragZone = state?.currentDragZone ?: return
        val dropState = state ?: return
        val currentDragZone = dropState.currentDragZone
        if (currentDragZone == null) {
            startFadeAnimation(dropTargetView, to = 0f)
            startFadeAnimation(secondDropTargetView, to = 0f) {
                container.removeView(secondDropTargetView)
            }
            return
        }
        val dropTargetRect = currentDragZone.dropTarget
        when {
            dropTargetRect == null -> startFadeAnimation(from = dropTargetView.alpha, to = 0f)
            dropTargetRect == null -> startFadeAnimation(dropTargetView, to = 0f)

            dropTargetView.alpha == 0f -> {
                dropTargetView.update(RectF(dropTargetRect.rect), dropTargetRect.cornerRadius)
                startFadeAnimation(from = 0f, to = 1f)
                startFadeAnimation(dropTargetView, to = 1f)
            }

            else -> startMorphAnimation(dropTargetRect)
        }

        val secondDropTargetRect = currentDragZone.secondDropTarget
        when {
            secondDropTargetRect == null -> startFadeAnimation(secondDropTargetView, to = 0f)
            else -> {
                val secondDropTargetView = secondDropTargetView ?: return
                secondDropTargetView.update(
                    RectF(secondDropTargetRect.rect),
                    secondDropTargetRect.cornerRadius,
                )
                startFadeAnimation(secondDropTargetView, to = 1f)
            }
        }
    }

    private fun startFadeAnimation(from: Float, to: Float, onEnd: (() -> Unit)? = null) {
        animator?.cancel()
    private fun startFadeAnimation(view: View?, to: Float, onEnd: (() -> Unit)? = null) {
        if (view == null) return
        val from = view.alpha
        viewAnimatorsMap[view]?.cancel()
        val duration =
            if (from < to) DROP_TARGET_ALPHA_IN_DURATION else DROP_TARGET_ALPHA_OUT_DURATION
        val animator = ValueAnimator.ofFloat(from, to).setDuration(duration)
        animator.addUpdateListener { _ -> dropTargetView.alpha = animator.animatedValue as Float }
        animator.addUpdateListener { _ -> view.alpha = animator.animatedValue as Float }
        if (onEnd != null) {
            animator.doOnEnd(onEnd)
        }
        this.animator = animator
        viewAnimatorsMap[view] = animator
        animator.start()
    }

    private fun startMorphAnimation(dropTargetRect: DropTargetRect) {
        animator?.cancel()
        viewAnimatorsMap[dropTargetView]?.cancel()
        val startAlpha = dropTargetView.alpha
        val startRect = dropTargetView.getRect()
        val endRect = dropTargetRect.rect
@@ -143,16 +187,17 @@ class DropTargetManager(
            morphRect.bottom = (startRect.bottom + (endRect.bottom - startRect.bottom) * fraction)
            dropTargetView.update(morphRect, dropTargetRect.cornerRadius)
        }
        this.animator = animator
        viewAnimatorsMap[dropTargetView] = animator
        animator.start()
    }

    /** Stores the current drag state. */
    private inner class DragState(
        private val dragZones: List<DragZone>,
        val draggedObject: DraggedObject
        val draggedObject: DraggedObject,
    ) {
        val initialDragZone = draggedObject.initialLocation?.let {
        val initialDragZone =
            draggedObject.initialLocation?.let {
                if (it.isOnLeft(isLayoutRtl)) {
                    dragZones.filterIsInstance<DragZone.Bubble.Left>().first()
                } else {
@@ -162,12 +207,13 @@ class DropTargetManager(
        var currentDragZone: DragZone? = initialDragZone

        fun getMatchingDragZone(x: Int, y: Int): DragZone? {
            return dragZones.firstOrNull { it.contains(x, y) } ?: currentDragZone
            return dragZones.firstOrNull { it.contains(x, y) }
        }
    }

    private val DraggedObject.initialLocation: BubbleBarLocation?
        get() = when (this) {
        get() =
            when (this) {
                is Bubble -> initialLocation
                is BubbleBar -> initialLocation
                is ExpandedView -> initialLocation
+2 −2
Original line number Diff line number Diff line
@@ -378,7 +378,7 @@ class DragZoneFactoryTest {
            )
        val dragZones =
            dragZoneFactory.createSortedDragZones(
                DraggedObject.LauncherIcon(bubbleBarHasBubbles = true)
                DraggedObject.LauncherIcon(bubbleBarHasBubbles = true) { }
            )
        val expectedZones: List<DragZoneVerifier> =
            listOf(verifyInstance<DragZone.Bubble.Left>(), verifyInstance<DragZone.Bubble.Right>())
@@ -401,7 +401,7 @@ class DragZoneFactoryTest {
            )
        val dragZones =
            dragZoneFactory.createSortedDragZones(
                DraggedObject.LauncherIcon(bubbleBarHasBubbles = false)
                DraggedObject.LauncherIcon(bubbleBarHasBubbles = false) { }
            )
        val expectedZones: List<DragZoneVerifier> =
            listOf(verifyInstance<DragZone.Bubble.Left>(), verifyInstance<DragZone.Bubble.Right>())
+161 −23
Original line number Diff line number Diff line
@@ -39,7 +39,8 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class DropTargetManagerTest {

    @get:Rule val animatorTestRule = AnimatorTestRule()
    @get:Rule
    val animatorTestRule = AnimatorTestRule()

    private val context = getApplicationContext<Context>()
    private lateinit var dropTargetManager: DropTargetManager
@@ -59,6 +60,12 @@ class DropTargetManagerTest {
            bounds = RectZone(Rect(0, 0, 100, 100)),
            dropTarget = DropTargetRect(Rect(0, 0, 50, 200), cornerRadius = 30f)
        )
    private val bubbleLeftDragZoneWithSecondDropTarget =
        DragZone.Bubble.Left(
            bounds = RectZone(Rect(0, 0, 100, 100)),
            dropTarget = DropTargetRect(Rect(0, 0, 50, 200), cornerRadius = 30f),
            secondDropTarget = DropTargetRect(Rect(0, 250, 50, 300), cornerRadius = 25f)
        )
    private val dismissDragZone =
        DragZone.Dismiss(bounds = CircleZone(x = 150, y = 50, radius = 50))
    private val bubbleRightDragZone =
@@ -66,9 +73,18 @@ class DropTargetManagerTest {
            bounds = RectZone(Rect(200, 0, 300, 100)),
            dropTarget = DropTargetRect(Rect(200, 0, 280, 150), cornerRadius = 30f)
        )
    private val bubbleRightDragZoneWithSecondDropTarget =
        DragZone.Bubble.Right(
            bounds = RectZone(Rect(200, 0, 300, 100)),
            dropTarget = DropTargetRect(Rect(200, 0, 280, 150), cornerRadius = 30f),
            secondDropTarget = DropTargetRect(Rect(200, 200, 80, 280), cornerRadius = 25f)
        )

    private val dropTargetView: DropTargetView
        get() = container.getChildAt(0) as DropTargetView
        get() = dropTargetManager.dropTargetView

    private val secondDropTargetView: DropTargetView?
        get() = dropTargetManager.secondDropTargetView

    @Before
    fun setUp() {
@@ -104,19 +120,21 @@ class DropTargetManagerTest {
            listOf(bubbleLeftDragZone, bubbleRightDragZone, dismissDragZone)
        )
        InstrumentationRegistry.getInstrumentation().runOnMainSync {
            dropTargetManager.onDragUpdated(
            val dragZone = dropTargetManager.onDragUpdated(
                bubbleRightDragZone.bounds.rect.centerX(),
                bubbleRightDragZone.bounds.rect.centerY()
            )
            assertThat(dragZone).isNotNull()
        }
        assertThat(dragZoneChangedListener.fromDragZone).isEqualTo(bubbleLeftDragZone)
        assertThat(dragZoneChangedListener.toDragZone).isEqualTo(bubbleRightDragZone)

        InstrumentationRegistry.getInstrumentation().runOnMainSync {
            dropTargetManager.onDragUpdated(
            val dragZone = dropTargetManager.onDragUpdated(
                dismissDragZone.bounds.x,
                dismissDragZone.bounds.y
            )
            assertThat(dragZone).isNotNull()
        }
        assertThat(dragZoneChangedListener.fromDragZone).isEqualTo(bubbleRightDragZone)
        assertThat(dragZoneChangedListener.toDragZone).isEqualTo(dismissDragZone)
@@ -129,17 +147,18 @@ class DropTargetManagerTest {
            listOf(bubbleLeftDragZone, bubbleRightDragZone, dismissDragZone)
        )
        InstrumentationRegistry.getInstrumentation().runOnMainSync {
            dropTargetManager.onDragUpdated(
            val dragZone = dropTargetManager.onDragUpdated(
                bubbleLeftDragZone.bounds.rect.centerX(),
                bubbleLeftDragZone.bounds.rect.centerY()
            )
            assertThat(dragZone).isNotNull()
        }
        assertThat(dragZoneChangedListener.fromDragZone).isNull()
        assertThat(dragZoneChangedListener.toDragZone).isNull()
    }

    @Test
    fun onDragUpdated_outsideAllZones_doesNotNotify() {
    fun onDragUpdated_outsideAllZones_notifiesDragZoneChanged() {
        dropTargetManager.onDragStarted(
            DraggedObject.Bubble(BubbleBarLocation.LEFT),
            listOf(bubbleLeftDragZone, bubbleRightDragZone)
@@ -149,9 +168,10 @@ class DropTargetManagerTest {
        assertThat(bubbleLeftDragZone.contains(pointX, pointY)).isFalse()
        assertThat(bubbleRightDragZone.contains(pointX, pointY)).isFalse()
        InstrumentationRegistry.getInstrumentation().runOnMainSync {
            dropTargetManager.onDragUpdated(pointX, pointY)
            val dragZone = dropTargetManager.onDragUpdated(pointX, pointY)
            assertThat(dragZone).isNull()
        }
        assertThat(dragZoneChangedListener.fromDragZone).isNull()
        assertThat(dragZoneChangedListener.fromDragZone).isNotNull()
        assertThat(dragZoneChangedListener.toDragZone).isNull()
    }

@@ -170,17 +190,19 @@ class DropTargetManagerTest {
        val pointY = bubbleRightDragZone.bounds.rect.centerY()
        assertThat(splitDragZone.contains(pointX, pointY)).isTrue()
        InstrumentationRegistry.getInstrumentation().runOnMainSync {
            dropTargetManager.onDragUpdated(pointX, pointY)
            val dragZone = dropTargetManager.onDragUpdated(pointX, pointY)
            assertThat(dragZone).isNotNull()
        }
        // verify we dragged to the bubble right zone because that has higher priority than split
        assertThat(dragZoneChangedListener.fromDragZone).isEqualTo(bubbleLeftDragZone)
        assertThat(dragZoneChangedListener.toDragZone).isEqualTo(bubbleRightDragZone)

        InstrumentationRegistry.getInstrumentation().runOnMainSync {
            dropTargetManager.onDragUpdated(
            val dragZone = dropTargetManager.onDragUpdated(
                bubbleRightDragZone.bounds.rect.centerX(),
                150 // below the bubble and dismiss drag zones but within split
            )
            assertThat(dragZone).isNotNull()
        }
        assertThat(dragZoneChangedListener.fromDragZone).isEqualTo(bubbleRightDragZone)
        assertThat(dragZoneChangedListener.toDragZone).isEqualTo(splitDragZone)
@@ -189,7 +211,8 @@ class DropTargetManagerTest {
        val dismissPointY = dismissDragZone.bounds.y
        assertThat(splitDragZone.contains(dismissPointX, dismissPointY)).isTrue()
        InstrumentationRegistry.getInstrumentation().runOnMainSync {
            dropTargetManager.onDragUpdated(dismissPointX, dismissPointY)
            val dragZone = dropTargetManager.onDragUpdated(dismissPointX, dismissPointY)
            assertThat(dragZone).isNotNull()
        }
        assertThat(dragZoneChangedListener.fromDragZone).isEqualTo(splitDragZone)
        assertThat(dragZoneChangedListener.toDragZone).isEqualTo(dismissDragZone)
@@ -204,10 +227,11 @@ class DropTargetManagerTest {
        InstrumentationRegistry.getInstrumentation().runOnMainSync {
            dropTargetManager.onDragEnded()
        }
        dropTargetManager.onDragUpdated(
        val dragZone = dropTargetManager.onDragUpdated(
            bubbleRightDragZone.bounds.rect.centerX(),
            bubbleRightDragZone.bounds.rect.centerY()
        )
        assertThat(dragZone).isNull()
        assertThat(dragZoneChangedListener.fromDragZone).isNull()
        assertThat(dragZoneChangedListener.toDragZone).isNull()
    }
@@ -218,7 +242,7 @@ class DropTargetManagerTest {
            DraggedObject.Bubble(BubbleBarLocation.LEFT),
            listOf(bubbleLeftDragZone, bubbleRightDragZone)
        )
        assertThat(container.childCount).isEqualTo(1)
        assertThat(container.childCount).isEqualTo(DROP_VIEWS_COUNT)
        assertThat(dropTargetView.alpha).isEqualTo(0)
    }

@@ -228,7 +252,7 @@ class DropTargetManagerTest {
            DraggedObject.Bubble(BubbleBarLocation.LEFT),
            listOf(bubbleLeftDragZone, bubbleRightDragZone)
        )
        assertThat(container.childCount).isEqualTo(1)
        assertThat(container.childCount).isEqualTo(DROP_VIEWS_COUNT)

        InstrumentationRegistry.getInstrumentation().runOnMainSync {
            dropTargetManager.onDragEnded()
@@ -244,10 +268,11 @@ class DropTargetManagerTest {
            listOf(bubbleLeftDragZone, bubbleRightDragZone, dismissDragZone)
        )
        InstrumentationRegistry.getInstrumentation().runOnMainSync {
            dropTargetManager.onDragUpdated(
            val dragZone = dropTargetManager.onDragUpdated(
                bubbleRightDragZone.bounds.rect.centerX(),
                bubbleRightDragZone.bounds.rect.centerY()
            )
            assertThat(dragZone).isNotNull()
            dropTargetManager.onDragEnded()
        }
        assertThat(dragZoneChangedListener.endedDragZone).isEqualTo(bubbleRightDragZone)
@@ -259,7 +284,7 @@ class DropTargetManagerTest {
            DraggedObject.Bubble(BubbleBarLocation.LEFT),
            listOf(bubbleLeftDragZone, bubbleRightDragZone)
        )
        assertThat(container.childCount).isEqualTo(1)
        assertThat(container.childCount).isEqualTo(DROP_VIEWS_COUNT)

        InstrumentationRegistry.getInstrumentation().runOnMainSync {
            dropTargetManager.onDragEnded()
@@ -267,7 +292,7 @@ class DropTargetManagerTest {
            // needs to be < DropTargetManager.DROP_TARGET_ALPHA_OUT_DURATION
            animatorTestRule.advanceTimeBy(50)
        }
        assertThat(container.childCount).isEqualTo(1)
        assertThat(container.childCount).isEqualTo(DROP_VIEWS_COUNT)

        InstrumentationRegistry.getInstrumentation().runOnMainSync {
            dropTargetManager.onDragStarted(
@@ -275,7 +300,7 @@ class DropTargetManagerTest {
                listOf(bubbleLeftDragZone, bubbleRightDragZone)
            )
        }
        assertThat(container.childCount).isEqualTo(1)
        assertThat(container.childCount).isEqualTo(DROP_VIEWS_COUNT)
    }

    @Test
@@ -286,10 +311,11 @@ class DropTargetManagerTest {
        )

        InstrumentationRegistry.getInstrumentation().runOnMainSync {
            dropTargetManager.onDragUpdated(
            val dragZone = dropTargetManager.onDragUpdated(
                bubbleRightDragZone.bounds.rect.centerX(),
                bubbleRightDragZone.bounds.rect.centerY()
            )
            assertThat(dragZone).isNotNull()
            animatorTestRule.advanceTimeBy(250)
        }

@@ -305,7 +331,9 @@ class DropTargetManagerTest {
        )

        InstrumentationRegistry.getInstrumentation().runOnMainSync {
            val dragZone =
                dropTargetManager.onDragUpdated(dismissDragZone.bounds.x, dismissDragZone.bounds.y)
            assertThat(dragZone).isNotNull()
            animatorTestRule.advanceTimeBy(250)
        }

@@ -320,10 +348,11 @@ class DropTargetManagerTest {
        )

        InstrumentationRegistry.getInstrumentation().runOnMainSync {
            dropTargetManager.onDragUpdated(
            val dragZone = dropTargetManager.onDragUpdated(
                bubbleRightDragZone.bounds.rect.centerX(),
                bubbleRightDragZone.bounds.rect.centerY()
            )
            assertThat(dragZone).isNotNull()
            animatorTestRule.advanceTimeBy(250)
        }

@@ -331,18 +360,120 @@ class DropTargetManagerTest {
        verifyDropTargetPosition(bubbleRightDragZone.dropTarget.rect)

        InstrumentationRegistry.getInstrumentation().runOnMainSync {
            dropTargetManager.onDragUpdated(
            val dragZone = dropTargetManager.onDragUpdated(
                bubbleLeftDragZone.bounds.rect.centerX(),
                bubbleLeftDragZone.bounds.rect.centerY()
            )
            assertThat(dragZone).isNotNull()
            animatorTestRule.advanceTimeBy(250)
        }

        assertThat(dropTargetView.alpha).isEqualTo(1)
        verifyDropTargetPosition(bubbleLeftDragZone.dropTarget.rect)
    }

    @Test
    fun onDragStarted_noInitialDragZone_notifiesInitialDragZoneNull() {
        dropTargetManager.onDragStarted(
            DraggedObject.LauncherIcon(bubbleBarHasBubbles = true) {},
            listOf(bubbleLeftDragZone, bubbleRightDragZone)
        )
        assertThat(dragZoneChangedListener.initialDragZone).isNull()
    }

    @Test
    fun onDragStartedMultipleTimes_secondDropViewRemoved() {
        dropTargetManager.onDragStarted(
            DraggedObject.LauncherIcon(bubbleBarHasBubbles = false) {},
            listOf(bubbleLeftDragZoneWithSecondDropTarget, bubbleRightDragZoneWithSecondDropTarget)
        )
        dropTargetManager.onDragStarted(
            DraggedObject.LauncherIcon(bubbleBarHasBubbles = false) {},
            listOf(bubbleLeftDragZoneWithSecondDropTarget, bubbleRightDragZoneWithSecondDropTarget)
        )
        dropTargetManager.onDragStarted(
            DraggedObject.LauncherIcon(bubbleBarHasBubbles = true) {},
            listOf(bubbleLeftDragZone, bubbleRightDragZone)
        )
        assertThat(container.childCount).isEqualTo(DROP_VIEWS_COUNT)
    }

    @Test
    fun onDragUpdated_noZoneToZoneWithDropTargetView_listenerNotified() {
        val onDropAction = Runnable { }
        dropTargetManager.onDragStarted(
            DraggedObject.LauncherIcon(bubbleBarHasBubbles = true, onDropAction),
            listOf(bubbleLeftDragZone, bubbleRightDragZone)
        )

        InstrumentationRegistry.getInstrumentation().runOnMainSync {
            val dragZone = dropTargetManager.onDragUpdated(
                bubbleLeftDragZone.bounds.rect.centerX(),
                bubbleLeftDragZone.bounds.rect.centerY()
            )
            assertThat(dragZone).isNotNull()
        }
        assertThat(dragZoneChangedListener.fromDragZone).isNull()
        assertThat(dragZoneChangedListener.toDragZone).isEqualTo(bubbleLeftDragZone)
        val launcherIconAction =
            (dragZoneChangedListener.draggedObject as DraggedObject.LauncherIcon).onDropAction
        assertThat(launcherIconAction).isEqualTo(onDropAction)
    }

    @Test
    fun onDragUpdated_noZoneToZoneWithDropTargetView_dropTargetShown() {
        dropTargetManager.onDragStarted(
            DraggedObject.LauncherIcon(bubbleBarHasBubbles = true) {},
            listOf(bubbleLeftDragZone, bubbleRightDragZone)
        )

        assertThat(container.childCount).isEqualTo(DROP_VIEWS_COUNT)
        InstrumentationRegistry.getInstrumentation().runOnMainSync {
            val dragZone = dropTargetManager.onDragUpdated(
                bubbleLeftDragZone.bounds.rect.centerX(),
                bubbleLeftDragZone.bounds.rect.centerY()
            )
            assertThat(dragZone).isNotNull()
            animatorTestRule.advanceTimeBy(250)
        }

        assertThat(container.childCount).isEqualTo(DROP_VIEWS_COUNT)
        assertThat(dropTargetView.alpha).isEqualTo(1)
        assertThat(secondDropTargetView).isNull()
        verifyDropTargetPosition(bubbleLeftDragZone.dropTarget.rect)
    }

    @Test
    fun onDragUpdated_noZoneToZoneWithTwoDropTargetViews_dropTargetsShown() {
        dropTargetManager.onDragStarted(
            DraggedObject.LauncherIcon(bubbleBarHasBubbles = false) {},
            listOf(bubbleLeftDragZoneWithSecondDropTarget, bubbleRightDragZoneWithSecondDropTarget)
        )

        InstrumentationRegistry.getInstrumentation().runOnMainSync {
            val dragZone = dropTargetManager.onDragUpdated(
                bubbleRightDragZoneWithSecondDropTarget.bounds.rect.centerX(),
                bubbleRightDragZoneWithSecondDropTarget.bounds.rect.centerY()
            )
            assertThat(dragZone).isNotNull()
            animatorTestRule.advanceTimeBy(250)
        }

        assertThat(container.childCount).isEqualTo(DROP_VIEWS_COUNT_FOR_LAUNCHER_ICON)
        assertThat(dropTargetView.alpha).isEqualTo(1)
        assertThat(secondDropTargetView!!.alpha).isEqualTo(1)
        verifyDropTargetPosition(bubbleRightDragZoneWithSecondDropTarget.dropTarget.rect)
        verifyDropTargetPosition(
            secondDropTargetView!!,
            bubbleRightDragZoneWithSecondDropTarget.secondDropTarget!!.rect
        )
    }

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

    private fun verifyDropTargetPosition(dropTargetView: DropTargetView, rect: Rect) {
        assertThat(dropTargetView.getRect().left).isEqualTo(rect.left)
        assertThat(dropTargetView.getRect().top).isEqualTo(rect.top)
        assertThat(dropTargetView.getRect().right).isEqualTo(rect.right)
@@ -354,6 +485,7 @@ class DropTargetManagerTest {
        var fromDragZone: DragZone? = null
        var toDragZone: DragZone? = null
        var endedDragZone: DragZone? = null
        var draggedObject: DraggedObject? = null

        override fun onInitialDragZoneSet(dragZone: DragZone?) {
            initialDragZone = dragZone
@@ -364,6 +496,7 @@ class DropTargetManagerTest {
            from: DragZone?,
            to: DragZone?
        ) {
            this.draggedObject = draggedObject
            fromDragZone = from
            toDragZone = to
        }
@@ -372,4 +505,9 @@ class DropTargetManagerTest {
            endedDragZone = zone
        }
    }

    companion object {
        const val DROP_VIEWS_COUNT = 1
        const val DROP_VIEWS_COUNT_FOR_LAUNCHER_ICON = 2
    }
}
 No newline at end of file