1 /*
2 * Copyright (C) 2020 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
17 package com.android.server.wm.flicker.helpers
18
19 import android.content.Context
20 import android.content.pm.PackageManager
21 import android.graphics.Point
22 import android.graphics.Rect
23 import android.os.RemoteException
24 import android.os.SystemClock
25 import android.util.Log
26 import android.util.Rational
27 import android.view.Display
28 import android.view.Surface
29 import android.view.View
30 import android.view.ViewConfiguration
31 import androidx.annotation.VisibleForTesting
32 import androidx.test.uiautomator.By
33 import androidx.test.uiautomator.BySelector
34 import androidx.test.uiautomator.Configurator
35 import androidx.test.uiautomator.UiDevice
36 import androidx.test.uiautomator.Until
37 import com.android.compatibility.common.util.SystemUtil
38 import com.android.server.wm.flicker.helpers.WindowUtils.displayBounds
39 import com.android.server.wm.flicker.helpers.WindowUtils.estimateNavigationBarPosition
40 import com.android.server.wm.traces.common.FlickerComponentName
41 import com.android.server.wm.traces.common.WindowManagerConditionsFactory
42 import com.android.server.wm.traces.parser.toAndroidRect
43 import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
44 import org.junit.Assert
45 import org.junit.Assert.assertNotNull
46
47 const val FIND_TIMEOUT: Long = 10000
48 const val FAST_WAIT_TIMEOUT: Long = 0
49 const val DOCKED_STACK_DIVIDER = "DockedStackDivider"
50 const val IME_PACKAGE = "com.google.android.inputmethod.latin"
51 @VisibleForTesting
52 const val SYSTEMUI_PACKAGE = "com.android.systemui"
53 private val LONG_PRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout() * 2L
54 private const val TAG = "FLICKER"
55
56 /**
57 * Sets [android.app.UiAutomation.waitForIdle] global timeout to 0 causing the
58 * [android.app.UiAutomation.waitForIdle] function to timeout instantly. This
59 * removes some delays when using the UIAutomator library required to create fast UI
60 * transitions.
61 */
setFastWaitnull62 fun setFastWait() {
63 Configurator.getInstance().waitForIdleTimeout = FAST_WAIT_TIMEOUT
64 }
65
66 /**
67 * Reverts [android.app.UiAutomation.waitForIdle] to default behavior.
68 */
setDefaultWaitnull69 fun setDefaultWait() {
70 Configurator.getInstance().waitForIdleTimeout = FIND_TIMEOUT
71 }
72
73 /**
74 * Checks if the device is running on gestural or 2-button navigation modes
75 */
isQuickstepEnablednull76 fun UiDevice.isQuickstepEnabled(): Boolean {
77 val enabled = this.findObject(By.res(SYSTEMUI_PACKAGE, "recent_apps")) == null
78 Log.d(TAG, "Quickstep enabled: $enabled")
79 return enabled
80 }
81
82 /**
83 * Checks if the display is rotated or not
84 */
UiDevicenull85 fun UiDevice.isRotated(): Boolean {
86 return this.displayRotation.isRotated()
87 }
88
89 /**
90 * Reopens the first device window from the list of recent apps (overview)
91 */
reopenAppFromOverviewnull92 fun UiDevice.reopenAppFromOverview(
93 wmHelper: WindowManagerStateHelper
94 ) {
95 val x = this.displayWidth / 2
96 val y = this.displayHeight / 2
97 this.click(x, y)
98
99 wmHelper.waitFor(
100 WindowManagerConditionsFactory.isAppTransitionIdle(Display.DEFAULT_DISPLAY),
101 WindowManagerConditionsFactory.isLayerVisible(FlickerComponentName.SNAPSHOT).negate(),
102 WindowManagerConditionsFactory.isLayerVisible(FlickerComponentName.SPLASH_SCREEN).negate()
103 )
104 }
105
106 /**
107 * Shows quickstep
108 *
109 * @throws AssertionError When quickstep does not appear
110 */
openQuickstepnull111 fun UiDevice.openQuickstep(
112 wmHelper: WindowManagerStateHelper
113 ) {
114 if (this.isQuickstepEnabled()) {
115 val navBar = this.findObject(By.res(SYSTEMUI_PACKAGE, "navigation_bar_frame"))
116 val navBarVisibleBounds: Rect
117
118 // TODO(vishnun) investigate why this object cannot be found.
119 navBarVisibleBounds = if (navBar != null) {
120 navBar.visibleBounds
121 } else {
122 Log.e(TAG, "Could not find nav bar, infer location")
123 estimateNavigationBarPosition(Surface.ROTATION_0).bounds.toAndroidRect()
124 }
125
126 val startX = navBarVisibleBounds.centerX()
127 val startY = navBarVisibleBounds.centerY()
128 val endX: Int
129 val endY: Int
130 val height: Int
131 val steps: Int
132 if (this.isRotated()) {
133 height = this.displayWidth
134 endX = height * 2 / 3
135 endY = navBarVisibleBounds.centerY()
136 steps = (endX - startX) / 100 // 100 px/step
137 } else {
138 height = this.displayHeight
139 endX = navBarVisibleBounds.centerX()
140 endY = height * 2 / 3
141 steps = (startY - endY) / 100 // 100 px/step
142 }
143 // Swipe from nav bar to 2/3rd down the screen.
144 this.swipe(startX, startY, endX, endY, steps)
145 }
146
147 // use a long timeout to wait until recents populated
148 val recentsSysUISelector = By.res(this.launcherPackageName, "overview_panel")
149 var recents = this.wait(Until.findObject(recentsSysUISelector), FIND_TIMEOUT)
150
151 // Quickstep detection is flaky on AOSP, UIDevice doesn't always find SysUI elements
152 // If it couldn't find, try pressing 'recent items' button
153 if (recents == null) {
154 try {
155 this.pressRecentApps()
156 } catch (e: RemoteException) {
157 throw RuntimeException(e)
158 }
159 recents = this.wait(Until.findObject(recentsSysUISelector), FIND_TIMEOUT)
160 }
161 assertNotNull("Recent items didn't appear", recents)
162 wmHelper.waitFor(
163 WindowManagerConditionsFactory.isNavBarVisible(),
164 WindowManagerConditionsFactory.isStatusBarVisible(),
165 WindowManagerConditionsFactory.isAppTransitionIdle(Display.DEFAULT_DISPLAY)
166 )
167 }
168
getLauncherOverviewSelectornull169 private fun getLauncherOverviewSelector(device: UiDevice): BySelector {
170 return By.res(device.launcherPackageName, "overview_panel")
171 }
172
longPressRecentsnull173 private fun longPressRecents(device: UiDevice) {
174 val recentsSelector = By.res(SYSTEMUI_PACKAGE, "recent_apps")
175 val recentsButton = device.wait(Until.findObject(recentsSelector), FIND_TIMEOUT)
176 assertNotNull("Unable to find 'recent items' button", recentsButton)
177 recentsButton.click(LONG_PRESS_TIMEOUT)
178 }
179
180 /**
181 * Wait for any IME view to appear
182 */
UiDevicenull183 fun UiDevice.waitForIME(): Boolean {
184 val ime = this.wait(Until.findObject(By.pkg(IME_PACKAGE)), FIND_TIMEOUT)
185 return ime != null
186 }
187
openQuickStepAndLongPressOverviewIconnull188 private fun openQuickStepAndLongPressOverviewIcon(
189 device: UiDevice,
190 wmHelper: WindowManagerStateHelper
191 ) {
192 if (device.isQuickstepEnabled()) {
193 device.openQuickstep(wmHelper)
194 } else {
195 try {
196 device.pressRecentApps()
197 } catch (e: RemoteException) {
198 Log.e(TAG, "launchSplitScreen", e)
199 }
200 }
201 val overviewIconSelector = By.res(device.launcherPackageName, "icon")
202 .clazz(View::class.java)
203 val overviewIcon = device.wait(Until.findObject(overviewIconSelector), FIND_TIMEOUT)
204 assertNotNull("Unable to find app icon in Overview", overviewIcon)
205 overviewIcon.click()
206 }
207
openQuickStepAndClearRecentAppsFromOverviewnull208 fun UiDevice.openQuickStepAndClearRecentAppsFromOverview(
209 wmHelper: WindowManagerStateHelper
210 ) {
211 if (this.isQuickstepEnabled()) {
212 this.openQuickstep(wmHelper)
213 } else {
214 try {
215 this.pressRecentApps()
216 } catch (e: RemoteException) {
217 Log.e(TAG, "launchSplitScreen", e)
218 }
219 }
220 for (i in 0..9) {
221 this.swipe(
222 this.getDisplayWidth() / 2,
223 this.getDisplayHeight() / 2,
224 this.getDisplayWidth(),
225 this.getDisplayHeight() / 2,
226 5)
227 // If "Clear all" button appears, use it
228 val clearAllSelector = By.res(this.getLauncherPackageName(), "clear_all")
229 val clearAllButton = this.wait(Until.findObject(clearAllSelector), FAST_WAIT_TIMEOUT)
230 if (clearAllButton != null) {
231 clearAllButton.click()
232 }
233 }
234 this.pressHome()
235 }
236
237 /**
238 * Opens quick step and puts the first app from the list of recently used apps into
239 * split-screen
240 *
241 * @throws AssertionError when unable to open the list of recently used apps, or when it does
242 * not contain a button to enter split screen mode
243 */
UiDevicenull244 fun UiDevice.launchSplitScreen(
245 wmHelper: WindowManagerStateHelper
246 ) {
247 openQuickStepAndLongPressOverviewIcon(this, wmHelper)
248 val splitScreenButtonSelector = By.text("Split screen")
249 val splitScreenButton =
250 this.wait(Until.findObject(splitScreenButtonSelector), FIND_TIMEOUT)
251 assertNotNull("Unable to find Split screen button in Overview", splitScreenButton)
252 splitScreenButton.click()
253
254 // Wait for animation to complete.
255 this.wait(Until.findObject(splitScreenDividerSelector), FIND_TIMEOUT)
256 wmHelper.waitFor(
257 WindowManagerConditionsFactory.isLayerVisible(DOCKED_STACK_DIVIDER),
258 WindowManagerConditionsFactory.isAppTransitionIdle(Display.DEFAULT_DISPLAY))
259
260 if (!this.isInSplitScreen()) {
261 Assert.fail("Unable to find Split screen divider")
262 }
263 }
264
265 /**
266 * Checks if the recent application is able to split screen(resizeable)
267 */
UiDevicenull268 fun UiDevice.canSplitScreen(
269 wmHelper: WindowManagerStateHelper
270 ): Boolean {
271 openQuickStepAndLongPressOverviewIcon(this, wmHelper)
272 val splitScreenButtonSelector = By.text("Split screen")
273 val canSplitScreen =
274 this.wait(Until.findObject(splitScreenButtonSelector), FIND_TIMEOUT) != null
275 this.pressHome()
276 return canSplitScreen
277 }
278
279 /**
280 * Checks if the device is in split screen by searching for the split screen divider
281 */
isInSplitScreennull282 fun UiDevice.isInSplitScreen(): Boolean {
283 return this.wait(Until.findObject(splitScreenDividerSelector), FIND_TIMEOUT) != null
284 }
285
waitSplitScreenGonenull286 fun waitSplitScreenGone(wmHelper: WindowManagerStateHelper): Boolean {
287 return wmHelper.waitFor(
288 WindowManagerConditionsFactory.isLayerVisible(DOCKED_STACK_DIVIDER),
289 WindowManagerConditionsFactory.isAppTransitionIdle(Display.DEFAULT_DISPLAY))
290 }
291
292 private val splitScreenDividerSelector: BySelector
293 get() = By.res(SYSTEMUI_PACKAGE, "docked_divider_handle")
294
295 /**
296 * Drags the split screen divider to the top of the screen to close it
297 *
298 * @throws AssertionError when unable to find the split screen divider
299 */
UiDevicenull300 fun UiDevice.exitSplitScreen() {
301 // Quickstep enabled
302 val divider = this.wait(Until.findObject(splitScreenDividerSelector), FIND_TIMEOUT)
303 assertNotNull("Unable to find Split screen divider", divider)
304
305 // Drag the split screen divider to the top of the screen
306 val dstPoint = if (this.isRotated()) {
307 Point(0, this.displayWidth / 2)
308 } else {
309 Point(this.displayWidth / 2, 0)
310 }
311 divider.drag(dstPoint, 400)
312 // Wait for animation to complete.
313 SystemClock.sleep(2000)
314 }
315
316 /**
317 * Drags the split screen divider to the bottom of the screen to close it
318 *
319 * @throws AssertionError when unable to find the split screen divider
320 */
exitSplitScreenFromBottomnull321 fun UiDevice.exitSplitScreenFromBottom(wmHelper: WindowManagerStateHelper) {
322 // Quickstep enabled
323 val divider = this.wait(Until.findObject(splitScreenDividerSelector), FIND_TIMEOUT)
324 assertNotNull("Unable to find Split screen divider", divider)
325
326 // Drag the split screen divider to the bottom of the screen
327 val dstPoint = if (this.isRotated()) {
328 Point(this.displayWidth, this.displayWidth / 2)
329 } else {
330 Point(this.displayWidth / 2, this.displayHeight)
331 }
332 divider.drag(dstPoint, 400)
333 if (!waitSplitScreenGone(wmHelper)) {
334 Assert.fail("Split screen divider never disappeared")
335 }
336 }
337
338 /**
339 * Drags the split screen divider to resize the windows in split screen
340 *
341 * @throws AssertionError when unable to find the split screen divider
342 */
resizeSplitScreennull343 fun UiDevice.resizeSplitScreen(windowHeightRatio: Rational) {
344 val dividerSelector = splitScreenDividerSelector
345 val divider = this.wait(Until.findObject(dividerSelector), FIND_TIMEOUT)
346 assertNotNull("Unable to find Split screen divider", divider)
347 val destHeight = (displayBounds.height() * windowHeightRatio.toFloat()).toInt()
348
349 // Drag the split screen divider to so that the ratio of top window height and bottom
350 // window height is windowHeightRatio
351 this.drag(
352 divider.visibleBounds.centerX(),
353 divider.visibleBounds.centerY(),
354 this.displayWidth / 2,
355 destHeight,
356 10)
357 this.wait(Until.findObject(dividerSelector), FIND_TIMEOUT)
358 // Wait for animation to complete.
359 SystemClock.sleep(2000)
360 }
361
362 /**
363 * Checks if the device has a window with the package name
364 */
hasWindownull365 fun UiDevice.hasWindow(packageName: String): Boolean {
366 return this.wait(Until.findObject(By.pkg(packageName)), FIND_TIMEOUT) != null
367 }
368
369 /**
370 * Waits until the package with that name is gone
371 */
waitUntilGonenull372 fun UiDevice.waitUntilGone(packageName: String): Boolean {
373 return this.wait(Until.gone(By.pkg(packageName)), FIND_TIMEOUT) != null
374 }
375
stopPackagenull376 fun stopPackage(context: Context, packageName: String) {
377 SystemUtil.runShellCommand("am force-stop $packageName")
378 val packageUid = try {
379 context.packageManager.getPackageUid(packageName, /* flags= */0)
380 } catch (e: PackageManager.NameNotFoundException) {
381 return
382 }
383 while (targetPackageIsRunning(packageUid)) {
384 try {
385 Thread.sleep(100)
386 } catch (e: InterruptedException) { // ignore
387 }
388 }
389 }
390
targetPackageIsRunningnull391 private fun targetPackageIsRunning(uid: Int): Boolean {
392 val result = SystemUtil.runShellCommand("cmd activity get-uid-state $uid")
393 return !result.contains("(NONEXISTENT)")
394 }
395
396 /**
397 * Turns on the device display and presses the home button to reach the launcher screen
398 */
wakeUpAndGoToHomeScreennull399 fun UiDevice.wakeUpAndGoToHomeScreen() {
400 try {
401 this.wakeUp()
402 } catch (e: RemoteException) {
403 throw RuntimeException(e)
404 }
405 this.pressHome()
406 }
407