1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 @file:JvmName("StretchEdgeUtil")
17 
18 package android.widget.cts.util
19 
20 import android.app.Instrumentation
21 import android.app.UiAutomation
22 import android.content.Context
23 import android.os.SystemClock
24 import android.view.InputDevice
25 import android.view.MotionEvent
26 import android.view.View
27 import android.widget.EdgeEffect
28 import androidx.test.InstrumentationRegistry
29 import androidx.test.rule.ActivityTestRule
30 import com.android.compatibility.common.util.UserHelper
31 import com.android.compatibility.common.util.WidgetTestUtils
32 
33 private val userHelper: UserHelper = UserHelper()
34 
35 /**
36  * Flings [view] from the center by ([deltaX], [deltaY]) pixels over 16 milliseconds.
37  */
flingnull38 fun fling(
39     activityRule: ActivityTestRule<*>,
40     view: View,
41     deltaX: Int,
42     deltaY: Int
43 ) {
44     val locationOnScreen = IntArray(2)
45     activityRule.runOnUiThread {
46         view.getLocationOnScreen(locationOnScreen)
47     }
48 
49     val screenX = locationOnScreen[0]
50     val screenY = locationOnScreen[1]
51     val instrumentation = InstrumentationRegistry.getInstrumentation()
52 
53     emulateDragGesture(instrumentation, activityRule,
54             screenX + (view.width / 2),
55             screenY + (view.height / 2),
56             deltaX,
57             deltaY,
58             16,
59             4
60     )
61 }
62 
63 /**
64  * Drags [view] from the center by [dragX], [dragY]. Returns `true` if [edgeEffect]'s distance
65  * is more than 0 after the stretch.
66  */
dragStretchesnull67 fun dragStretches(
68     activityRule: ActivityTestRule<*>,
69     view: View,
70     edgeEffect: NoReleaseEdgeEffect,
71     dragX: Int,
72     dragY: Int
73 ): Boolean {
74     val locationOnScreen = IntArray(2)
75     activityRule.runOnUiThread {
76         view.getLocationOnScreen(locationOnScreen)
77     }
78 
79     val screenX = locationOnScreen[0]
80     val screenY = locationOnScreen[1]
81     val instrumentation = InstrumentationRegistry.getInstrumentation()
82     emulateDragGesture(instrumentation, activityRule,
83             screenX + view.width / 2,
84             screenY + view.height / 2,
85             dragX,
86             dragY,
87             160,
88             20
89     )
90     return edgeEffect.distance > 0
91 }
92 
93 /**
94  * Drags [view] from the center by [dragX], [dragY], then taps to hold.
95  * [edgeEffect] must be the [EdgeEffect] that is being stretched in [view].
96  * After the drag and before the tap down, [beforeDown] is called. After the
97  * tap down and before the tap up, [beforeUp] is called.
98  */
dragAndHoldExecutenull99 fun dragAndHoldExecute(
100     activityRule: ActivityTestRule<*>,
101     view: View,
102     edgeEffect: NoReleaseEdgeEffect,
103     dragX: Int = 0,
104     dragY: Int = 0,
105     beforeDown: Runnable?,
106     beforeUp: Runnable?
107 ) {
108     val locationOnScreen = IntArray(2)
109     activityRule.runOnUiThread {
110         view.getLocationOnScreen(locationOnScreen)
111     }
112 
113     val screenX = locationOnScreen[0] + view.width / 2
114     val screenY = locationOnScreen[1] + view.height / 2
115     val instrumentation = InstrumentationRegistry.getInstrumentation()
116     emulateDragGesture(instrumentation, activityRule,
117             screenX,
118             screenY,
119             dragX,
120             dragY,
121             160,
122             20
123     )
124     edgeEffect.pauseRelease = false
125     edgeEffect.onRelease()
126 
127     beforeDown?.run()
128 
129     val downTime = SystemClock.uptimeMillis()
130     injectDownEvent(
131             instrumentation.getUiAutomation(),
132             downTime,
133             screenX,
134             screenY
135     )
136 
137     beforeUp?.run()
138 
139     injectUpEvent(
140             instrumentation.getUiAutomation(),
141             downTime,
142             downTime + 10,
143             screenX,
144             screenY
145     )
146 }
147 
148 /**
149  * Drag, release, then tap-and-hold and ensure that the stretch stays after the hold.
150  */
dragAndHoldKeepsStretchnull151 fun dragAndHoldKeepsStretch(
152     activityRule: ActivityTestRule<*>,
153     view: View,
154     edgeEffect: NoReleaseEdgeEffect,
155     dragX: Int = 0,
156     dragY: Int = 0
157 ): Boolean {
158     var startDistance = 0f
159     var nextFrameDistance = 0f
160 
161     dragAndHoldExecute(
162             activityRule,
163             view,
164             edgeEffect,
165             dragX,
166             dragY,
167             beforeDown = null,
168             beforeUp = Runnable {
169                 activityRule.runOnUiThread {
170                     startDistance = edgeEffect.distance
171                 }
172                 activityRule.runOnUiThread {
173                     nextFrameDistance = edgeEffect.distance
174                 }
175             }
176     )
177 
178     return startDistance == nextFrameDistance && startDistance > 0f
179 }
180 
181 /**
182  * An [EdgeEffect] that does not release with [onRelease] unless [pauseRelease] is `false`.
183  */
184 open class NoReleaseEdgeEffect(context: Context) : EdgeEffect(context) {
185     var pauseRelease = true
186 
187     var onReleaseCalled = false
188 
onReleasenull189     override fun onRelease() {
190         onReleaseCalled = true
191         if (!pauseRelease) {
192             super.onRelease()
193         }
194     }
195 }
196 
197 /**
198  * Emulates a linear drag gesture between 2 points across the screen.
199  *
200  * @param instrumentation the instrumentation used to run the test
201  * @param dragStartX Start X of the emulated drag gesture
202  * @param dragStartY Start Y of the emulated drag gesture
203  * @param dragAmountX X amount of the emulated drag gesture
204  * @param dragAmountY Y amount of the emulated drag gesture
205  * @param dragDurationMs The time in milliseconds over which the drag occurs
206  * @param moveEventCount The number of events that produce the movement
207  */
emulateDragGesturenull208 private fun emulateDragGesture(
209     instrumentation: Instrumentation,
210     activityTestRule: ActivityTestRule<*>?,
211     dragStartX: Int,
212     dragStartY: Int,
213     dragAmountX: Int,
214     dragAmountY: Int,
215     dragDurationMs: Int,
216     moveEventCount: Int
217 ) {
218     // We are using the UiAutomation object to inject events so that drag works
219     // across view / window boundaries (such as for the emulated drag and drop
220     // sequences)
221     val uiAutomation = instrumentation.uiAutomation
222     val downTime = SystemClock.uptimeMillis()
223     injectDownEvent(uiAutomation, downTime, dragStartX, dragStartY)
224 
225     val dragEndX = dragStartX + dragAmountX
226     val dragEndY = dragStartY + dragAmountY
227     // Inject a sequence of MOVE events that emulate the "move" part of the gesture
228     injectMoveEventsForDrag(uiAutomation, downTime, downTime, dragStartX, dragStartY,
229             dragEndX, dragEndY, moveEventCount, dragDurationMs)
230     injectUpEvent(uiAutomation, downTime, downTime + dragDurationMs, dragEndX, dragEndY)
231 
232     // Wait for the system to process all events in the queue
233     if (activityTestRule != null) {
234         WidgetTestUtils.runOnMainAndDrawSync(activityTestRule,
235                 activityTestRule.activity.getWindow().getDecorView(), null)
236     } else {
237         instrumentation.waitForIdleSync()
238     }
239 }
240 
injectMoveEventsForDragnull241 fun injectMoveEventsForDrag(
242     uiAutomation: UiAutomation,
243     downTime: Long,
244     dragStartTime: Long = downTime,
245     dragStartX: Int,
246     dragStartY: Int,
247     dragEndX: Int,
248     dragEndY: Int,
249     moveEventCount: Int,
250     dragDurationMs: Int
251 ) {
252     val dragAmountX = dragEndX - dragStartX
253     val dragAmountY = dragEndY - dragStartY
254 
255     for (i in 0 until moveEventCount) {
256         // Note that the first MOVE event is generated "away" from the coordinates
257         // of the start / DOWN event, and the last MOVE event is generated
258         // at the same coordinates as the subsequent UP event.
259         val moveX = dragStartX + (dragAmountX * i / moveEventCount)
260         val moveY = dragStartY + (dragAmountY * i / moveEventCount)
261         val eventTime = dragStartTime + (dragDurationMs * i / moveEventCount)
262         injectEvent(uiAutomation, MotionEvent.ACTION_MOVE, downTime, eventTime, moveX, moveY)
263     }
264 }
265 
266 /**
267  * Injects an [MotionEvent.ACTION_UP] event at the given coordinates.
268  *
269  * @param downTime The time of the event, usually from [SystemClock.uptimeMillis]
270  * @param xOnScreen The x screen coordinate to press on
271  * @param yOnScreen The y screen coordinate to press on
272  * sent.
273  */
injectUpEventnull274 fun injectUpEvent(
275     uiAutomation: UiAutomation,
276     downTime: Long,
277     upTime: Long,
278     xOnScreen: Int,
279     yOnScreen: Int
280 ) = injectEvent(uiAutomation, MotionEvent.ACTION_UP, downTime, upTime, xOnScreen, yOnScreen)
281 
282 /**
283  * Injects an [MotionEvent.ACTION_DOWN] event at the given coordinates.
284  *
285  * @param downTime The time of the event, usually from [SystemClock.uptimeMillis]
286  * @param xOnScreen The x screen coordinate to press on
287  * @param yOnScreen The y screen coordinate to press on
288  * sent.
289  */
290 fun injectDownEvent(
291     uiAutomation: UiAutomation,
292     downTime: Long,
293     xOnScreen: Int,
294     yOnScreen: Int
295 ) = injectEvent(uiAutomation, MotionEvent.ACTION_DOWN, downTime, downTime, xOnScreen, yOnScreen)
296 
297 private fun injectEvent(
298     uiAutomation: UiAutomation,
299     action: Int,
300     downTime: Long,
301     eventTime: Long,
302     xOnScreen: Int,
303     yOnScreen: Int
304 ) {
305     val eventUp = MotionEvent.obtain(
306             downTime, eventTime, action, xOnScreen.toFloat(), yOnScreen.toFloat(), 1)
307     userHelper.injectDisplayIdIfNeeded(eventUp)
308     eventUp.source = InputDevice.SOURCE_TOUCHSCREEN
309     uiAutomation.injectInputEvent(eventUp, true)
310     eventUp.recycle()
311 }
312