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