Introduce BetterFling

This CL introduces BetterFling, a utility to easily fling an object
which is using BetterSwipe under the hood.

I decided to introduce this after noticing with local test runs that
UiObject2.fling() was not deterministic at all and sometimes the flings
would almost not scroll the object being flinged.

Bug: 238993727
Test: PeopleSpaceFlingTest
Change-Id: I31a5d71fe3c5e3e5b3fb80ede788696bad633cae
diff --git a/libraries/uiautomator-helpers/src/android/platform/uiautomator_helpers/BetterFling.kt b/libraries/uiautomator-helpers/src/android/platform/uiautomator_helpers/BetterFling.kt
new file mode 100644
index 0000000..4978892
--- /dev/null
+++ b/libraries/uiautomator-helpers/src/android/platform/uiautomator_helpers/BetterFling.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2023 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 android.platform.uiautomator_helpers
+
+import android.graphics.Point
+import android.platform.uiautomator_helpers.DeviceHelpers.context
+import android.platform.uiautomator_helpers.DeviceHelpers.uiDevice
+import android.util.TypedValue
+import androidx.test.uiautomator.Direction
+import androidx.test.uiautomator.UiObject2
+import androidx.test.uiautomator.Until
+import java.time.Duration
+import java.time.temporal.ChronoUnit
+import kotlin.math.roundToInt
+
+/** A fling utility that should be used instead of [UiObject2.fling] for more reliable flings. */
+object BetterFling {
+    private const val DEFAULT_FLING_MARGIN_DP = 30
+    private val DEFAULT_FLING_DURATION = Duration.of(100, ChronoUnit.MILLIS)
+    private val DEFAULT_WAIT_TIMEOUT = Duration.of(5, ChronoUnit.SECONDS)
+
+    /** Fling [obj] in the given [direction]. */
+    @JvmStatic
+    @JvmOverloads
+    fun fling(
+        obj: UiObject2,
+        direction: Direction,
+        duration: Duration = DEFAULT_FLING_DURATION,
+        marginDp: Int = DEFAULT_FLING_MARGIN_DP,
+    ) {
+        val margin = marginDp.dpToPx().roundToInt()
+        val bounds = obj.visibleBounds
+        val centerX = bounds.centerX()
+        val centerY = bounds.centerY()
+        val (start, stop) =
+            when (direction) {
+                Direction.LEFT -> {
+                    Point(bounds.right - margin, centerY) to Point(bounds.left + margin, centerY)
+                }
+                Direction.RIGHT -> {
+                    Point(bounds.left + margin, centerY) to Point(bounds.right - margin, centerY)
+                }
+                Direction.UP -> {
+                    Point(centerX, bounds.bottom - margin) to Point(centerX, bounds.top + margin)
+                }
+                Direction.DOWN -> {
+                    Point(centerX, bounds.top + margin) to Point(centerX, bounds.bottom - margin)
+                }
+            }
+
+        uiDevice.performActionAndWait(
+            { BetterSwipe.from(start).to(stop, duration).release() },
+            Until.scrollFinished(Direction.reverse(direction)),
+            DEFAULT_WAIT_TIMEOUT.toMillis()
+        )
+    }
+
+    private fun Number.dpToPx(): Float {
+        return TypedValue.applyDimension(
+            TypedValue.COMPLEX_UNIT_DIP,
+            toFloat(),
+            context.resources.displayMetrics,
+        )
+    }
+}