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.device.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.common.Logger
26 import android.tools.common.Rotation
27 import android.tools.common.traces.ConditionsFactory
28 import android.tools.common.traces.component.ComponentNameMatcher
29 import android.tools.device.helpers.WindowUtils.displayBounds
30 import android.tools.device.helpers.WindowUtils.estimateNavigationBarPosition
31 import android.tools.device.traces.executeShellCommand
32 import android.tools.device.traces.parsers.WindowManagerStateHelper
33 import android.tools.device.traces.parsers.toAndroidRect
34 import android.util.Rational
35 import android.view.View
36 import android.view.ViewConfiguration
37 import androidx.annotation.VisibleForTesting
38 import androidx.test.uiautomator.By
39 import androidx.test.uiautomator.BySelector
40 import androidx.test.uiautomator.Configurator
41 import androidx.test.uiautomator.UiDevice
42 import androidx.test.uiautomator.Until
43 import org.junit.Assert
44 import org.junit.Assert.assertNotNull
45
46 const val FIND_TIMEOUT: Long = 10000
47 const val FAST_WAIT_TIMEOUT: Long = 0
48 val DOCKED_STACK_DIVIDER = ComponentNameMatcher("", "DockedStackDivider")
49 const val IME_PACKAGE = "com.google.android.inputmethod.latin"
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 Logger.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 /** Reopens the first device window from the list of recent apps (overview) */
reopenAppFromOverviewnull81 fun UiDevice.reopenAppFromOverview(wmHelper: WindowManagerStateHelper) {
82 val x = this.displayWidth / 2
83 val y = this.displayHeight / 2
84 this.click(x, y)
85
86 wmHelper.StateSyncBuilder().withAppTransitionIdle().waitFor()
87 }
88
89 /**
90 * Shows quickstep
91 *
92 * @throws AssertionError When quickstep does not appear
93 */
openQuickstepnull94 fun UiDevice.openQuickstep(wmHelper: WindowManagerStateHelper) {
95 if (this.isQuickstepEnabled()) {
96 val navBar = this.findObject(By.res(SYSTEMUI_PACKAGE, "navigation_bar_frame"))
97
98 // TODO(vishnun) investigate why this object cannot be found.
99 val navBarVisibleBounds: Rect =
100 if (navBar != null) {
101 navBar.visibleBounds
102 } else {
103 Logger.e(TAG, "Could not find nav bar, infer location")
104 estimateNavigationBarPosition(Rotation.ROTATION_0).bounds.toAndroidRect()
105 }
106
107 val startX = navBarVisibleBounds.centerX()
108 val startY = navBarVisibleBounds.centerY()
109 val endX: Int
110 val endY: Int
111 val height: Int
112 val steps: Int
113 if (this.isRotated()) {
114 height = this.displayWidth
115 endX = height * 2 / 3
116 endY = navBarVisibleBounds.centerY()
117 steps = (endX - startX) / 100 // 100 px/step
118 } else {
119 height = this.displayHeight
120 endX = navBarVisibleBounds.centerX()
121 endY = height * 2 / 3
122 steps = (startY - endY) / 100 // 100 px/step
123 }
124 // Swipe from nav bar to 2/3rd down the screen.
125 this.swipe(startX, startY, endX, endY, steps)
126 }
127
128 // use a long timeout to wait until recents populated
129 val recentsSysUISelector = By.res(this.launcherPackageName, "overview_panel")
130 var recents = this.wait(Until.findObject(recentsSysUISelector), FIND_TIMEOUT)
131
132 // Quickstep detection is flaky on AOSP, UIDevice doesn't always find SysUI elements
133 // If it couldn't find, try pressing 'recent items' button
134 if (recents == null) {
135 try {
136 this.pressRecentApps()
137 } catch (e: RemoteException) {
138 throw RuntimeException(e)
139 }
140 recents = this.wait(Until.findObject(recentsSysUISelector), FIND_TIMEOUT)
141 }
142 assertNotNull("Recent items didn't appear", recents)
143 wmHelper
144 .StateSyncBuilder()
145 .withNavOrTaskBarVisible()
146 .withStatusBarVisible()
147 .withAppTransitionIdle()
148 .waitForAndVerify()
149 }
150
getLauncherOverviewSelectornull151 private fun getLauncherOverviewSelector(device: UiDevice): BySelector {
152 return By.res(device.launcherPackageName, "overview_panel")
153 }
154
longPressRecentsnull155 private fun longPressRecents(device: UiDevice) {
156 val recentsSelector = By.res(SYSTEMUI_PACKAGE, "recent_apps")
157 val recentsButton = device.wait(Until.findObject(recentsSelector), FIND_TIMEOUT)
158 assertNotNull("Unable to find 'recent items' button", recentsButton)
159 recentsButton.click(LONG_PRESS_TIMEOUT)
160 }
161
162 /** Wait for any IME view to appear */
UiDevicenull163 fun UiDevice.waitForIME(): Boolean {
164 val ime = this.wait(Until.findObject(By.pkg(IME_PACKAGE)), FIND_TIMEOUT)
165 return ime != null
166 }
167
openQuickStepAndLongPressOverviewIconnull168 private fun openQuickStepAndLongPressOverviewIcon(
169 device: UiDevice,
170 wmHelper: WindowManagerStateHelper
171 ) {
172 if (device.isQuickstepEnabled()) {
173 device.openQuickstep(wmHelper)
174 } else {
175 try {
176 device.pressRecentApps()
177 } catch (e: RemoteException) {
178 Logger.e(TAG, "launchSplitScreen", e)
179 }
180 }
181 val overviewIconSelector = By.res(device.launcherPackageName, "icon").clazz(View::class.java)
182 val overviewIcon = device.wait(Until.findObject(overviewIconSelector), FIND_TIMEOUT)
183 assertNotNull("Unable to find app icon in Overview", overviewIcon)
184 overviewIcon.click()
185 }
186
openQuickStepAndClearRecentAppsFromOverviewnull187 fun UiDevice.openQuickStepAndClearRecentAppsFromOverview(wmHelper: WindowManagerStateHelper) {
188 if (this.isQuickstepEnabled()) {
189 this.openQuickstep(wmHelper)
190 } else {
191 try {
192 this.pressRecentApps()
193 } catch (e: RemoteException) {
194 Logger.e(TAG, "launchSplitScreen", e)
195 }
196 }
197 for (i in 0..9) {
198 this.swipe(
199 this.displayWidth / 2,
200 this.displayHeight / 2,
201 this.displayWidth,
202 this.displayHeight / 2,
203 5
204 )
205 // If "Clear all" button appears, use it
206 val clearAllSelector = By.res(this.launcherPackageName, "clear_all")
207 wait(Until.findObject(clearAllSelector), FAST_WAIT_TIMEOUT)?.click()
208 }
209 this.pressHome()
210 }
211
212 /**
213 * Opens quick step and puts the first app from the list of recently used apps into split-screen
214 *
215 * @throws AssertionError when unable to open the list of recently used apps, or when it does not
216 * contain a button to enter split screen mode
217 */
UiDevicenull218 fun UiDevice.launchSplitScreen(wmHelper: WindowManagerStateHelper) {
219 openQuickStepAndLongPressOverviewIcon(this, wmHelper)
220 val splitScreenButtonSelector = By.text("Split screen")
221 val splitScreenButton = this.wait(Until.findObject(splitScreenButtonSelector), FIND_TIMEOUT)
222 assertNotNull("Unable to find Split screen button in Overview", splitScreenButton)
223 splitScreenButton.click()
224
225 // Wait for animation to complete.
226 this.wait(Until.findObject(splitScreenDividerSelector), FIND_TIMEOUT)
227 wmHelper
228 .StateSyncBuilder()
229 .add(ConditionsFactory.isLayerVisible(DOCKED_STACK_DIVIDER))
230 .withAppTransitionIdle()
231 .waitForAndVerify()
232
233 if (!this.isInSplitScreen()) {
234 Assert.fail("Unable to find Split screen divider")
235 }
236 }
237
238 /** Checks if the recent application is able to split screen(resizeable) */
UiDevicenull239 fun UiDevice.canSplitScreen(wmHelper: WindowManagerStateHelper): Boolean {
240 openQuickStepAndLongPressOverviewIcon(this, wmHelper)
241 val splitScreenButtonSelector = By.text("Split screen")
242 val canSplitScreen =
243 this.wait(Until.findObject(splitScreenButtonSelector), FIND_TIMEOUT) != null
244 this.pressHome()
245 return canSplitScreen
246 }
247
248 /** Checks if the device is in split screen by searching for the split screen divider */
isInSplitScreennull249 fun UiDevice.isInSplitScreen(): Boolean {
250 return this.wait(Until.findObject(splitScreenDividerSelector), FIND_TIMEOUT) != null
251 }
252
waitSplitScreenGonenull253 fun waitSplitScreenGone(wmHelper: WindowManagerStateHelper) {
254 return wmHelper
255 .StateSyncBuilder()
256 .add(ConditionsFactory.isLayerVisible(DOCKED_STACK_DIVIDER).negate())
257 .withAppTransitionIdle()
258 .waitForAndVerify()
259 }
260
261 private val splitScreenDividerSelector: BySelector
262 get() = By.res(SYSTEMUI_PACKAGE, "docked_divider_handle")
263
264 /**
265 * Drags the split screen divider to the top of the screen to close it
266 *
267 * @throws AssertionError when unable to find the split screen divider
268 */
UiDevicenull269 fun UiDevice.exitSplitScreen() {
270 // Quickstep enabled
271 val divider = this.wait(Until.findObject(splitScreenDividerSelector), FIND_TIMEOUT)
272 assertNotNull("Unable to find Split screen divider", divider)
273
274 // Drag the split screen divider to the top of the screen
275 val dstPoint =
276 if (this.isRotated()) {
277 Point(0, this.displayWidth / 2)
278 } else {
279 Point(this.displayWidth / 2, 0)
280 }
281 divider.drag(dstPoint, 400)
282 // Wait for animation to complete.
283 SystemClock.sleep(2000)
284 }
285
286 /**
287 * Drags the split screen divider to the bottom of the screen to close it
288 *
289 * @throws AssertionError when unable to find the split screen divider
290 */
exitSplitScreenFromBottomnull291 fun UiDevice.exitSplitScreenFromBottom(wmHelper: WindowManagerStateHelper) {
292 // Quickstep enabled
293 val divider = this.wait(Until.findObject(splitScreenDividerSelector), FIND_TIMEOUT)
294 assertNotNull("Unable to find Split screen divider", divider)
295
296 // Drag the split screen divider to the bottom of the screen
297 val dstPoint =
298 if (this.isRotated()) {
299 Point(this.displayWidth, this.displayWidth / 2)
300 } else {
301 Point(this.displayWidth / 2, this.displayHeight)
302 }
303 divider.drag(dstPoint, 400)
304 waitSplitScreenGone(wmHelper)
305 }
306
307 /**
308 * Drags the split screen divider to resize the windows in split screen
309 *
310 * @throws AssertionError when unable to find the split screen divider
311 */
resizeSplitScreennull312 fun UiDevice.resizeSplitScreen(windowHeightRatio: Rational) {
313 val dividerSelector = splitScreenDividerSelector
314 val divider = this.wait(Until.findObject(dividerSelector), FIND_TIMEOUT)
315 assertNotNull("Unable to find Split screen divider", divider)
316 val destHeight = (displayBounds.height() * windowHeightRatio.toFloat()).toInt()
317
318 // Drag the split screen divider to so that the ratio of top window height and bottom
319 // window height is windowHeightRatio
320 this.drag(
321 divider.visibleBounds.centerX(),
322 divider.visibleBounds.centerY(),
323 this.displayWidth / 2,
324 destHeight,
325 10
326 )
327 this.wait(Until.findObject(dividerSelector), FIND_TIMEOUT)
328 // Wait for animation to complete.
329 SystemClock.sleep(2000)
330 }
331
332 /** Checks if the device has a window with the package name */
hasWindownull333 fun UiDevice.hasWindow(packageName: String): Boolean {
334 return this.wait(Until.findObject(By.pkg(packageName)), FIND_TIMEOUT) != null
335 }
336
337 /** Waits until the package with that name is gone */
waitUntilGonenull338 fun UiDevice.waitUntilGone(packageName: String): Boolean {
339 return this.wait(Until.gone(By.pkg(packageName)), FIND_TIMEOUT) != null
340 }
341
stopPackagenull342 fun stopPackage(context: Context, packageName: String) {
343 executeShellCommand("am force-stop $packageName")
344 val packageUid =
345 try {
346 context.packageManager.getPackageUid(packageName, /* flags */ 0)
347 } catch (e: PackageManager.NameNotFoundException) {
348 return
349 }
350 while (targetPackageIsRunning(packageUid)) {
351 try {
352 Thread.sleep(100)
353 } catch (e: InterruptedException) { // ignore
354 }
355 }
356 }
357
targetPackageIsRunningnull358 private fun targetPackageIsRunning(uid: Int): Boolean {
359 val result = String(executeShellCommand("cmd activity get-uid-state $uid"))
360 return !result.contains("(NONEXISTENT)")
361 }
362
363 /** Turns on the device display and presses the home button to reach the launcher screen */
wakeUpAndGoToHomeScreennull364 fun UiDevice.wakeUpAndGoToHomeScreen() {
365 try {
366 this.wakeUp()
367 } catch (e: RemoteException) {
368 throw RuntimeException(e)
369 }
370 this.pressHome()
371 }
372