• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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