1 /*
2 * Copyright (C) 2023 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 android.tools.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.tools.Rotation
26 import android.tools.helpers.WindowUtils.displayBounds
27 import android.tools.helpers.WindowUtils.estimateNavigationBarPosition
28 import android.tools.traces.ConditionsFactory
29 import android.tools.traces.component.ComponentNameMatcher
30 import android.tools.traces.executeShellCommand
31 import android.tools.traces.parsers.WindowManagerStateHelper
32 import android.util.Log
33 import android.util.Rational
34 import android.view.View
35 import android.view.ViewConfiguration
36 import androidx.annotation.VisibleForTesting
37 import androidx.test.uiautomator.By
38 import androidx.test.uiautomator.BySelector
39 import androidx.test.uiautomator.Configurator
40 import androidx.test.uiautomator.UiDevice
41 import androidx.test.uiautomator.Until
42 import org.junit.Assert
43 import org.junit.Assert.assertNotNull
44
45 const val FIND_TIMEOUT: Long = 10000
46 const val FAST_WAIT_TIMEOUT: Long = 0
47 val DOCKED_STACK_DIVIDER = ComponentNameMatcher("", "DockedStackDivider")
48 const val IME_PACKAGE = "com.google.android.inputmethod.latin"
49
50 @VisibleForTesting const val SYSTEMUI_PACKAGE = "com.android.systemui"
51 private val LONG_PRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout() * 2L
52 private const val TAG = "FLICKER"
53
54 /**
55 * Sets [android.app.UiAutomation.waitForIdle] global timeout to 0 causing the
56 * [android.app.UiAutomation.waitForIdle] function to timeout instantly. This removes some delays
57 * when using the UIAutomator library required to create fast UI transitions.
58 */
setFastWaitnull59 fun setFastWait() {
60 Configurator.getInstance().waitForIdleTimeout = FAST_WAIT_TIMEOUT
61 }
62
63 /** Reverts [android.app.UiAutomation.waitForIdle] to default behavior. */
setDefaultWaitnull64 fun setDefaultWait() {
65 Configurator.getInstance().waitForIdleTimeout = FIND_TIMEOUT
66 }
67
68 /** Checks if the device is running on gestural or 2-button navigation modes */
isQuickstepEnablednull69 fun UiDevice.isQuickstepEnabled(): Boolean {
70 val enabled = this.findObject(By.res(SYSTEMUI_PACKAGE, "recent_apps")) == null
71 Log.d(TAG, "Quickstep enabled: $enabled")
72 return enabled
73 }
74
75 /** Checks if the display is rotated or not */
UiDevicenull76 fun UiDevice.isRotated(): Boolean {
77 return Rotation.getByValue(this.displayRotation).isRotated()
78 }
79
80 /**
81 * Shows quickstep
82 *
83 * @throws AssertionError When quickstep does not appear
84 */
openQuickstepnull85 fun UiDevice.openQuickstep(wmHelper: WindowManagerStateHelper) {
86 if (this.isQuickstepEnabled()) {
87 val navBar = this.findObject(By.res(SYSTEMUI_PACKAGE, "navigation_bar_frame"))
88
89 // TODO(vishnun) investigate why this object cannot be found.
90 val navBarVisibleBounds: Rect =
91 if (navBar != null) {
92 navBar.visibleBounds
93 } else {
94 Log.e(TAG, "Could not find nav bar, infer location")
95 estimateNavigationBarPosition(Rotation.ROTATION_0).bounds
96 }
97
98 val startX = navBarVisibleBounds.centerX()
99 val startY = navBarVisibleBounds.centerY()
100 val endX: Int
101 val endY: Int
102 val height: Int
103 val steps: Int
104 if (this.isRotated()) {
105 height = this.displayWidth
106 endX = height * 2 / 3
107 endY = navBarVisibleBounds.centerY()
108 steps = (endX - startX) / 100 // 100 px/step
109 } else {
110 height = this.displayHeight
111 endX = navBarVisibleBounds.centerX()
112 endY = height * 2 / 3
113 steps = (startY - endY) / 100 // 100 px/step
114 }
115 // Swipe from nav bar to 2/3rd down the screen.
116 this.swipe(startX, startY, endX, endY, steps)
117 }
118
119 // use a long timeout to wait until recents populated
120 val recentsSysUISelector = By.res(this.launcherPackageName, "overview_panel")
121 var recents = this.wait(Until.findObject(recentsSysUISelector), FIND_TIMEOUT)
122
123 // Quickstep detection is flaky on AOSP, UIDevice doesn't always find SysUI elements
124 // If it couldn't find, try pressing 'recent items' button
125 if (recents == null) {
126 try {
127 this.pressRecentApps()
128 } catch (e: RemoteException) {
129 throw RuntimeException(e)
130 }
131 recents = this.wait(Until.findObject(recentsSysUISelector), FIND_TIMEOUT)
132 }
133 assertNotNull("Recent items didn't appear", recents)
134 wmHelper
135 .StateSyncBuilder()
136 .withNavOrTaskBarVisible()
137 .withStatusBarVisible()
138 .withAppTransitionIdle()
139 .waitForAndVerify()
140 }
141
getLauncherOverviewSelectornull142 private fun getLauncherOverviewSelector(device: UiDevice): BySelector {
143 return By.res(device.launcherPackageName, "overview_panel")
144 }
145
longPressRecentsnull146 private fun longPressRecents(device: UiDevice) {
147 val recentsSelector = By.res(SYSTEMUI_PACKAGE, "recent_apps")
148 val recentsButton = device.wait(Until.findObject(recentsSelector), FIND_TIMEOUT)
149 assertNotNull("Unable to find 'recent items' button", recentsButton)
150 recentsButton.click(LONG_PRESS_TIMEOUT)
151 }
152
153 /** Wait for any IME view to appear */
UiDevicenull154 fun UiDevice.waitForIME(): Boolean {
155 val ime = this.wait(Until.findObject(By.pkg(IME_PACKAGE)), FIND_TIMEOUT)
156 return ime != null
157 }
158
openQuickStepAndLongPressOverviewIconnull159 private fun openQuickStepAndLongPressOverviewIcon(
160 device: UiDevice,
161 wmHelper: WindowManagerStateHelper
162 ) {
163 if (device.isQuickstepEnabled()) {
164 device.openQuickstep(wmHelper)
165 } else {
166 try {
167 device.pressRecentApps()
168 } catch (e: RemoteException) {
169 Log.e(TAG, "launchSplitScreen", e)
170 }
171 }
172 val overviewIconSelector = By.res(device.launcherPackageName, "icon").clazz(View::class.java)
173 val overviewIcon = device.wait(Until.findObject(overviewIconSelector), FIND_TIMEOUT)
174 assertNotNull("Unable to find app icon in Overview", overviewIcon)
175 overviewIcon.click()
176 }
177
openQuickStepAndClearRecentAppsFromOverviewnull178 fun UiDevice.openQuickStepAndClearRecentAppsFromOverview(wmHelper: WindowManagerStateHelper) {
179 if (this.isQuickstepEnabled()) {
180 this.openQuickstep(wmHelper)
181 } else {
182 try {
183 this.pressRecentApps()
184 } catch (e: RemoteException) {
185 Log.e(TAG, "launchSplitScreen", e)
186 }
187 }
188 for (i in 0..9) {
189 this.swipe(
190 this.displayWidth / 2,
191 this.displayHeight / 2,
192 this.displayWidth,
193 this.displayHeight / 2,
194 5
195 )
196 // If "Clear all" button appears, use it
197 val clearAllSelector = By.res(this.launcherPackageName, "clear_all")
198 wait(Until.findObject(clearAllSelector), FAST_WAIT_TIMEOUT)?.click()
199 }
200 this.pressHome()
201 }
202
203 /**
204 * Opens quick step and puts the first app from the list of recently used apps into split-screen
205 *
206 * @throws AssertionError when unable to open the list of recently used apps, or when it does not
207 * contain a button to enter split screen mode
208 */
UiDevicenull209 fun UiDevice.launchSplitScreen(wmHelper: WindowManagerStateHelper) {
210 openQuickStepAndLongPressOverviewIcon(this, wmHelper)
211 val splitScreenButtonSelector = By.text("Split screen")
212 val splitScreenButton = this.wait(Until.findObject(splitScreenButtonSelector), FIND_TIMEOUT)
213 assertNotNull("Unable to find Split screen button in Overview", splitScreenButton)
214 splitScreenButton.click()
215
216 // Wait for animation to complete.
217 this.wait(Until.findObject(splitScreenDividerSelector), FIND_TIMEOUT)
218 wmHelper
219 .StateSyncBuilder()
220 .add(ConditionsFactory.isLayerVisible(DOCKED_STACK_DIVIDER))
221 .withAppTransitionIdle()
222 .waitForAndVerify()
223
224 if (!this.isInSplitScreen()) {
225 Assert.fail("Unable to find Split screen divider")
226 }
227 }
228
229 /** Checks if the recent application is able to split screen(resizeable) */
UiDevicenull230 fun UiDevice.canSplitScreen(wmHelper: WindowManagerStateHelper): Boolean {
231 openQuickStepAndLongPressOverviewIcon(this, wmHelper)
232 val splitScreenButtonSelector = By.text("Split screen")
233 val canSplitScreen =
234 this.wait(Until.findObject(splitScreenButtonSelector), FIND_TIMEOUT) != null
235 this.pressHome()
236 return canSplitScreen
237 }
238
239 /** Checks if the device is in split screen by searching for the split screen divider */
isInSplitScreennull240 fun UiDevice.isInSplitScreen(): Boolean {
241 return this.wait(Until.findObject(splitScreenDividerSelector), FIND_TIMEOUT) != null
242 }
243
waitSplitScreenGonenull244 fun waitSplitScreenGone(wmHelper: WindowManagerStateHelper) {
245 return wmHelper
246 .StateSyncBuilder()
247 .add(ConditionsFactory.isLayerVisible(DOCKED_STACK_DIVIDER).negate())
248 .withAppTransitionIdle()
249 .waitForAndVerify()
250 }
251
252 private val splitScreenDividerSelector: BySelector
253 get() = By.res(SYSTEMUI_PACKAGE, "docked_divider_handle")
254
255 /**
256 * Drags the split screen divider to the top of the screen to close it
257 *
258 * @throws AssertionError when unable to find the split screen divider
259 */
UiDevicenull260 fun UiDevice.exitSplitScreen() {
261 // Quickstep enabled
262 val divider = this.wait(Until.findObject(splitScreenDividerSelector), FIND_TIMEOUT)
263 assertNotNull("Unable to find Split screen divider", divider)
264
265 // Drag the split screen divider to the top of the screen
266 val dstPoint =
267 if (this.isRotated()) {
268 Point(0, this.displayWidth / 2)
269 } else {
270 Point(this.displayWidth / 2, 0)
271 }
272 divider.drag(dstPoint, 400)
273 // Wait for animation to complete.
274 SystemClock.sleep(2000)
275 }
276
277 /**
278 * Drags the split screen divider to the bottom of the screen to close it
279 *
280 * @throws AssertionError when unable to find the split screen divider
281 */
exitSplitScreenFromBottomnull282 fun UiDevice.exitSplitScreenFromBottom(wmHelper: WindowManagerStateHelper) {
283 // Quickstep enabled
284 val divider = this.wait(Until.findObject(splitScreenDividerSelector), FIND_TIMEOUT)
285 assertNotNull("Unable to find Split screen divider", divider)
286
287 // Drag the split screen divider to the bottom of the screen
288 val dstPoint =
289 if (this.isRotated()) {
290 Point(this.displayWidth, this.displayWidth / 2)
291 } else {
292 Point(this.displayWidth / 2, this.displayHeight)
293 }
294 divider.drag(dstPoint, 400)
295 waitSplitScreenGone(wmHelper)
296 }
297
298 /**
299 * Drags the split screen divider to resize the windows in split screen
300 *
301 * @throws AssertionError when unable to find the split screen divider
302 */
resizeSplitScreennull303 fun UiDevice.resizeSplitScreen(windowHeightRatio: Rational) {
304 val dividerSelector = splitScreenDividerSelector
305 val divider = this.wait(Until.findObject(dividerSelector), FIND_TIMEOUT)
306 assertNotNull("Unable to find Split screen divider", divider)
307 val destHeight = (displayBounds.height() * windowHeightRatio.toFloat()).toInt()
308
309 // Drag the split screen divider to so that the ratio of top window height and bottom
310 // window height is windowHeightRatio
311 this.drag(
312 divider.visibleBounds.centerX(),
313 divider.visibleBounds.centerY(),
314 this.displayWidth / 2,
315 destHeight,
316 10
317 )
318 this.wait(Until.findObject(dividerSelector), FIND_TIMEOUT)
319 // Wait for animation to complete.
320 SystemClock.sleep(2000)
321 }
322
323 /** Checks if the device has a window with the package name */
hasWindownull324 fun UiDevice.hasWindow(packageName: String): Boolean {
325 return this.wait(Until.findObject(By.pkg(packageName)), FIND_TIMEOUT) != null
326 }
327
328 /** Waits until the package with that name is gone */
waitUntilGonenull329 fun UiDevice.waitUntilGone(packageName: String): Boolean {
330 return this.wait(Until.gone(By.pkg(packageName)), FIND_TIMEOUT) != null
331 }
332
stopPackagenull333 fun stopPackage(context: Context, packageName: String) {
334 executeShellCommand("am force-stop $packageName")
335 val packageUid =
336 try {
337 context.packageManager.getPackageUid(packageName, 0)
338 } catch (e: PackageManager.NameNotFoundException) {
339 return
340 }
341 while (targetPackageIsRunning(packageUid)) {
342 try {
343 Thread.sleep(100)
344 } catch (e: InterruptedException) { // ignore
345 }
346 }
347 }
348
targetPackageIsRunningnull349 private fun targetPackageIsRunning(uid: Int): Boolean {
350 val result = String(executeShellCommand("cmd activity get-uid-state $uid"))
351 return !result.contains("(NONEXISTENT)")
352 }
353
354 /** Turns on the device display and presses the home button to reach the launcher screen */
wakeUpAndGoToHomeScreennull355 fun UiDevice.wakeUpAndGoToHomeScreen() {
356 try {
357 this.wakeUp()
358 } catch (e: RemoteException) {
359 throw RuntimeException(e)
360 }
361 this.pressHome()
362 }
363