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