diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/DragToBubbleController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/DragToBubbleController.kt index 5d7579406e..2263ecfb53 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/DragToBubbleController.kt +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/DragToBubbleController.kt @@ -39,18 +39,18 @@ import com.android.wm.shell.shared.bubbles.DraggedObject import com.android.wm.shell.shared.bubbles.DraggedObject.LauncherIcon import com.android.wm.shell.shared.bubbles.DropTargetManager import com.android.wm.shell.shared.bubbles.DropTargetManager.DragZoneChangedListener +import com.google.common.annotations.VisibleForTesting -// TODO(b/411506181) add tests class DragToBubbleController( private val context: Context, private val bubbleBarContainer: FrameLayout, ) : DragController.DragListener { - private val dropTargetManager: DropTargetManager + @VisibleForTesting val dropTargetManager: DropTargetManager - private lateinit var bubbleBarLeftDropTarget: BubbleBarLocationDropTarget - private lateinit var bubbleBarRightDropTarget: BubbleBarLocationDropTarget - private lateinit var dragZoneFactory: DragZoneFactory + @VisibleForTesting lateinit var bubbleBarLeftDropTarget: BubbleBarLocationDropTarget + @VisibleForTesting lateinit var bubbleBarRightDropTarget: BubbleBarLocationDropTarget + @VisibleForTesting lateinit var dragZoneFactory: DragZoneFactory private lateinit var systemUiProxy: SystemUiProxy private lateinit var bubbleBarViewController: BubbleBarViewController private val isRtl: Boolean diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/DragToBubbleControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/DragToBubbleControllerTest.kt new file mode 100644 index 0000000000..f8534fbe84 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/DragToBubbleControllerTest.kt @@ -0,0 +1,301 @@ +/* + * 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.launcher3.taskbar.bubbles + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.graphics.Insets +import android.graphics.Rect +import android.widget.FrameLayout +import androidx.core.animation.AnimatorTestRule +import androidx.test.core.app.ApplicationProvider.getApplicationContext +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry +import com.android.launcher3.DropTarget +import com.android.launcher3.DropTarget.DragObject +import com.android.launcher3.dragndrop.DragOptions +import com.android.launcher3.model.data.AppInfo +import com.android.quickstep.SystemUiProxy +import com.android.wm.shell.shared.bubbles.BubbleBarLocation +import com.android.wm.shell.shared.bubbles.DeviceConfig +import com.android.wm.shell.shared.bubbles.DragZoneFactory +import com.android.wm.shell.shared.bubbles.DragZoneFactory.BubbleBarPropertiesProvider +import com.android.wm.shell.shared.bubbles.DragZoneFactory.SplitScreenModeChecker.SplitScreenMode +import com.android.wm.shell.shared.bubbles.DropTargetView +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.stub +import org.mockito.kotlin.verify + +/** Unit tests for [DragToBubbleControllerTest]. */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class DragToBubbleControllerTest { + + @get:Rule val animatorTestRule = AnimatorTestRule() + + private val context = getApplicationContext() + private val container = FrameLayout(context) + private val bubbleBarViewController: BubbleBarViewController = mock() + private val systemUiProxy: SystemUiProxy = mock() + private val bubbleBarPropertiesProvider = FakeBubbleBarPropertiesProvider() + private val testDragZonesFactory = createTestDragZoneFactory() + private val dragObject = DragObject(context) + private lateinit var dragToBubbleController: DragToBubbleController + + private val dropTargetView: DropTargetView + get() = dragToBubbleController.dropTargetManager.dropTargetView + + private val secondDropTargetView: DropTargetView? + get() = dragToBubbleController.dropTargetManager.secondDropTargetView + + private val bubbleBarLeftDropTarget: DropTarget + get() = dragToBubbleController.bubbleBarLeftDropTarget + + private val bubbleBarRightDropTarget: DropTarget + get() = dragToBubbleController.bubbleBarRightDropTarget + + private val leftDropTargetRect: Rect + get() = testDragZonesFactory.getBubbleBarDropRect(isLeftSide = true) + + private val rightDropTargetRect: Rect + get() = testDragZonesFactory.getBubbleBarDropRect(isLeftSide = false) + + @Before + fun setUp() { + dragToBubbleController = DragToBubbleController(context, container) + dragToBubbleController.init( + bubbleBarViewController, + bubbleBarPropertiesProvider, + systemUiProxy, + ) + dragToBubbleController.dragZoneFactory = testDragZonesFactory + } + + @Test + fun dragStarted_noBubbleBar_dropZonesAdded() { + dragToBubbleController.onDragStart(dragObject, DragOptions()) + + assertThat(container.childCount).isEqualTo(DROP_VIEWS_COUNT_NO_BUBBLE_BAR) + assertThat(dropTargetView.parent).isEqualTo(container) + assertThat(secondDropTargetView!!.parent).isEqualTo(container) + assertThat(dropTargetView.alpha).isEqualTo(0f) + assertThat(secondDropTargetView!!.alpha).isEqualTo(0f) + } + + @Test + fun dragStarted_hasBubbleBar_dropZonesAdded() { + prepareBubbleBarViewController(hasBubbles = true) + dragToBubbleController.onDragStart(dragObject, DragOptions()) + + assertThat(container.childCount).isEqualTo(DROP_VIEWS_COUNT) + assertThat(dropTargetView.parent).isEqualTo(container) + assertThat(dropTargetView.alpha).isEqualTo(0f) + assertThat(secondDropTargetView).isNull() + } + + @Test + fun dragEnded_allViewsRemoved() { + dragToBubbleController.onDragStart(dragObject, DragOptions()) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + dragToBubbleController.onDragEnd() + animatorTestRule.advanceTimeBy(250) + } + + assertThat(container.childCount).isEqualTo(0) + } + + @Test + fun draggedToTheRightDropZone_noBubbles_dropTargetViewsShown() { + dragToBubbleController.onDragStart(dragObject, DragOptions()) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + dragObject.updateXYToCenterOf(rightDropTargetRect) + bubbleBarRightDropTarget.onDragEnter(dragObject) + animatorTestRule.advanceTimeBy(250) + } + + assertThat(dropTargetView.alpha).isEqualTo(1f) + assertThat(secondDropTargetView!!.alpha).isEqualTo(1f) + verify(bubbleBarViewController, never()).animateBubbleBarLocation(any()) + } + + @Test + fun draggedToTheRightDropZone_hasBubblesOnTheRight_dropTargetViewShown() { + prepareBubbleBarViewController(hasBubbles = true) + dragToBubbleController.onDragStart(dragObject, DragOptions()) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + dragObject.updateXYToCenterOf(rightDropTargetRect) + bubbleBarRightDropTarget.onDragEnter(dragObject) + animatorTestRule.advanceTimeBy(250) + } + + assertThat(dropTargetView.alpha).isEqualTo(1f) + assertThat(secondDropTargetView).isNull() + } + + @Test + fun draggedToTheRightDropZone_hasBubblesOnTheRight_bubbleBarLocationChangeNotRequested() { + prepareBubbleBarViewController(hasBubbles = true) + dragToBubbleController.onDragStart(dragObject, DragOptions()) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + dragObject.updateXYToCenterOf(rightDropTargetRect) + bubbleBarRightDropTarget.onDragEnter(dragObject) + animatorTestRule.advanceTimeBy(250) + } + verify(bubbleBarViewController, never()).animateBubbleBarLocation(any()) + } + + @Test + fun draggedToTheLeftDropZone_hasBubblesOnTheRight_bubbleBarLocationChangeRequested() { + prepareBubbleBarViewController( + hasBubbles = true, + bubbleBarLocation = BubbleBarLocation.RIGHT, + ) + dragToBubbleController.onDragStart(dragObject, DragOptions()) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + dragObject.updateXYToCenterOf(leftDropTargetRect) + bubbleBarLeftDropTarget.onDragEnter(dragObject) + } + verify(bubbleBarViewController).animateBubbleBarLocation(BubbleBarLocation.LEFT) + } + + @Test + fun draggedToTheLeftDropZone_dragEnded_hasBubblesOnTheRight_locationRestored() { + val bubbleBarOriginalLocation = BubbleBarLocation.RIGHT + prepareBubbleBarViewController( + hasBubbles = true, + bubbleBarLocation = bubbleBarOriginalLocation, + ) + dragToBubbleController.onDragStart(dragObject, DragOptions()) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + dragObject.updateXYToCenterOf(leftDropTargetRect) + bubbleBarLeftDropTarget.onDragEnter(dragObject) + dragObject.updateXY(x = 0, y = 0) + bubbleBarLeftDropTarget.onDragExit(dragObject) + dragToBubbleController.onDragEnd() + } + + verify(bubbleBarViewController).animateBubbleBarLocation(BubbleBarLocation.LEFT) + verify(bubbleBarViewController).animateBubbleBarLocation(bubbleBarOriginalLocation) + assertThat(container.childCount).isEqualTo(DROP_VIEWS_COUNT) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animatorTestRule.advanceTimeBy(250) + } + assertThat(container.childCount).isEqualTo(0) + } + + @Test + fun droppedAtTheLeftDropZone_noBubblesOnTheRight_appBubbleCreationRequested() { + val bubbleBarOriginalLocation = BubbleBarLocation.RIGHT + prepareBubbleBarViewController( + hasBubbles = false, + bubbleBarLocation = bubbleBarOriginalLocation, + ) + val packageName = "test.package" + val itemIntent = + Intent().apply { + component = ComponentName(packageName, "TestClass") + `package` = packageName + } + val appInfo = AppInfo().apply { intent = itemIntent } + dragObject.dragInfo = appInfo + + dragToBubbleController.onDragStart(dragObject, DragOptions()) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + dragObject.updateXYToCenterOf(leftDropTargetRect) + bubbleBarLeftDropTarget.onDragEnter(dragObject) + bubbleBarLeftDropTarget.onDragExit(dragObject) + bubbleBarLeftDropTarget.onDrop(dragObject, DragOptions()) + } + + verify(systemUiProxy).showAppBubble(itemIntent, appInfo.user, BubbleBarLocation.LEFT) + } + + private fun prepareBubbleBarViewController( + hasBubbles: Boolean = false, + bubbleBarLocation: BubbleBarLocation = BubbleBarLocation.RIGHT, + ) { + bubbleBarViewController.stub { + on { hasBubbles() } doReturn hasBubbles + on { getBubbleBarLocation() } doReturn bubbleBarLocation + } + } + + private fun DragObject.updateXYToCenterOf(rect: Rect) { + updateXY(rect.centerX(), rect.centerY()) + } + + private fun DragObject.updateXY(x: Int, y: Int) { + this.x = x + this.y = y + } + + private fun createTestDragZoneFactory(): DragZoneFactory { + val deviceConfig = + DeviceConfig( + isLargeScreen = true, + isSmallTablet = false, + isLandscape = true, + isRtl = false, + windowBounds = Rect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT), + insets = Insets.NONE, + ) + return DragZoneFactory( + context, + deviceConfig, + { SplitScreenMode.NONE }, + { false }, + bubbleBarPropertiesProvider, + ) + } + + private class FakeBubbleBarPropertiesProvider : BubbleBarPropertiesProvider { + + override fun getHeight(): Int = BUBBLE_BAR_HEIGHT + + override fun getWidth(): Int = BUBBLE_BAR_WIDTH + + override fun getBottomPadding(): Int = BUBBLE_BAR_BOTTOM_PADDING + } + + companion object { + const val BUBBLE_BAR_WIDTH = 100 + const val BUBBLE_BAR_HEIGHT = 110 + const val BUBBLE_BAR_BOTTOM_PADDING = 70 + const val SCREEN_WIDTH = 2000 + const val SCREEN_HEIGHT = 1000 + const val DROP_VIEWS_COUNT = 1 + const val DROP_VIEWS_COUNT_NO_BUBBLE_BAR = 2 + } +}