| /* |
| * Copyright (C) 2020 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.server.wm.flicker.helpers |
| |
| import android.content.Context |
| import android.content.pm.PackageManager |
| import android.graphics.Point |
| import android.graphics.Rect |
| import android.os.RemoteException |
| import android.os.SystemClock |
| import android.util.Log |
| import android.util.Rational |
| import android.view.Surface |
| import android.view.View |
| import android.view.ViewConfiguration |
| import androidx.annotation.VisibleForTesting |
| import androidx.test.uiautomator.By |
| import androidx.test.uiautomator.BySelector |
| import androidx.test.uiautomator.Configurator |
| import androidx.test.uiautomator.UiDevice |
| import androidx.test.uiautomator.Until |
| import com.android.compatibility.common.util.SystemUtil |
| import com.android.server.wm.flicker.helpers.WindowUtils.displayBounds |
| import com.android.server.wm.flicker.helpers.WindowUtils.getNavigationBarPosition |
| import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper |
| import org.junit.Assert |
| import org.junit.Assert.assertNotNull |
| |
| const val FIND_TIMEOUT: Long = 10000 |
| const val FAST_WAIT_TIMEOUT: Long = 0 |
| const val DOCKED_STACK_DIVIDER = "DockedStackDivider" |
| const val IME_PACKAGE = "com.google.android.inputmethod.latin" |
| @VisibleForTesting |
| const val SYSTEMUI_PACKAGE = "com.android.systemui" |
| private val LONG_PRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout() * 2L |
| private const val TAG = "FLICKER" |
| |
| /** |
| * Sets [android.app.UiAutomation.waitForIdle] global timeout to 0 causing the |
| * [android.app.UiAutomation.waitForIdle] function to timeout instantly. This |
| * removes some delays when using the UIAutomator library required to create fast UI |
| * transitions. |
| */ |
| fun setFastWait() { |
| Configurator.getInstance().waitForIdleTimeout = FAST_WAIT_TIMEOUT |
| } |
| |
| /** |
| * Reverts [android.app.UiAutomation.waitForIdle] to default behavior. |
| */ |
| fun setDefaultWait() { |
| Configurator.getInstance().waitForIdleTimeout = FIND_TIMEOUT |
| } |
| |
| /** |
| * Checks if the device is running on gestural or 2-button navigation modes |
| */ |
| fun UiDevice.isQuickstepEnabled(): Boolean { |
| val enabled = this.findObject(By.res(SYSTEMUI_PACKAGE, "recent_apps")) == null |
| Log.d(TAG, "Quickstep enabled: $enabled") |
| return enabled |
| } |
| |
| /** |
| * Checks if the display is rotated or not |
| */ |
| fun UiDevice.isRotated(): Boolean { |
| return this.displayRotation.isRotated() |
| } |
| |
| /** |
| * Reopens the first device window from the list of recent apps (overview) |
| */ |
| fun UiDevice.reopenAppFromOverview( |
| wmHelper: WindowManagerStateHelper |
| ) { |
| val x = this.displayWidth / 2 |
| val y = this.displayHeight / 2 |
| this.click(x, y) |
| |
| wmHelper.waitForAppTransitionIdle() |
| } |
| |
| /** |
| * Shows quickstep |
| * |
| * @throws AssertionError When quickstep does not appear |
| */ |
| fun UiDevice.openQuickstep( |
| wmHelper: WindowManagerStateHelper |
| ) { |
| if (this.isQuickstepEnabled()) { |
| val navBar = this.findObject(By.res(SYSTEMUI_PACKAGE, "navigation_bar_frame")) |
| val navBarVisibleBounds: Rect |
| |
| // TODO(vishnun) investigate why this object cannot be found. |
| navBarVisibleBounds = if (navBar != null) { |
| navBar.visibleBounds |
| } else { |
| Log.e(TAG, "Could not find nav bar, infer location") |
| getNavigationBarPosition(Surface.ROTATION_0).bounds |
| } |
| |
| val startX = navBarVisibleBounds.centerX() |
| val startY = navBarVisibleBounds.centerY() |
| val endX: Int |
| val endY: Int |
| val height: Int |
| val steps: Int |
| if (this.isRotated()) { |
| height = this.displayWidth |
| endX = height * 2 / 3 |
| endY = navBarVisibleBounds.centerY() |
| steps = (endX - startX) / 100 // 100 px/step |
| } else { |
| height = this.displayHeight |
| endX = navBarVisibleBounds.centerX() |
| endY = height * 2 / 3 |
| steps = (startY - endY) / 100 // 100 px/step |
| } |
| // Swipe from nav bar to 2/3rd down the screen. |
| this.swipe(startX, startY, endX, endY, steps) |
| } |
| |
| // use a long timeout to wait until recents populated |
| val recentsSysUISelector = By.res(this.launcherPackageName, "overview_panel") |
| var recents = this.wait(Until.findObject(recentsSysUISelector), FIND_TIMEOUT) |
| |
| // Quickstep detection is flaky on AOSP, UIDevice doesn't always find SysUI elements |
| // If it couldn't find, try pressing 'recent items' button |
| if (recents == null) { |
| try { |
| this.pressRecentApps() |
| } catch (e: RemoteException) { |
| throw RuntimeException(e) |
| } |
| recents = this.wait(Until.findObject(recentsSysUISelector), FIND_TIMEOUT) |
| } |
| assertNotNull("Recent items didn't appear", recents) |
| wmHelper.waitForNavBarStatusBarVisible() |
| wmHelper.waitForAppTransitionIdle() |
| } |
| |
| private fun getLauncherOverviewSelector(device: UiDevice): BySelector { |
| return By.res(device.launcherPackageName, "overview_panel") |
| } |
| |
| private fun longPressRecents(device: UiDevice) { |
| val recentsSelector = By.res(SYSTEMUI_PACKAGE, "recent_apps") |
| val recentsButton = device.wait(Until.findObject(recentsSelector), FIND_TIMEOUT) |
| assertNotNull("Unable to find 'recent items' button", recentsButton) |
| recentsButton.click(LONG_PRESS_TIMEOUT) |
| } |
| |
| /** |
| * Wait for any IME view to appear |
| */ |
| fun UiDevice.waitForIME(): Boolean { |
| val ime = this.wait(Until.findObject(By.pkg(IME_PACKAGE)), FIND_TIMEOUT) |
| return ime != null |
| } |
| |
| private fun openQuickStepAndLongPressOverviewIcon( |
| device: UiDevice, |
| wmHelper: WindowManagerStateHelper |
| ) { |
| if (device.isQuickstepEnabled()) { |
| device.openQuickstep(wmHelper) |
| } else { |
| try { |
| device.pressRecentApps() |
| } catch (e: RemoteException) { |
| Log.e(TAG, "launchSplitScreen", e) |
| } |
| } |
| val overviewIconSelector = By.res(device.launcherPackageName, "icon") |
| .clazz(View::class.java) |
| val overviewIcon = device.wait(Until.findObject(overviewIconSelector), FIND_TIMEOUT) |
| assertNotNull("Unable to find app icon in Overview", overviewIcon) |
| overviewIcon.click() |
| } |
| |
| fun UiDevice.openQuickStepAndClearRecentAppsFromOverview( |
| wmHelper: WindowManagerStateHelper |
| ) { |
| if (this.isQuickstepEnabled()) { |
| this.openQuickstep(wmHelper) |
| } else { |
| try { |
| this.pressRecentApps() |
| } catch (e: RemoteException) { |
| Log.e(TAG, "launchSplitScreen", e) |
| } |
| } |
| for (i in 0..9) { |
| this.swipe( |
| this.getDisplayWidth() / 2, |
| this.getDisplayHeight() / 2, |
| this.getDisplayWidth(), |
| this.getDisplayHeight() / 2, |
| 5) |
| // If "Clear all" button appears, use it |
| val clearAllSelector = By.res(this.getLauncherPackageName(), "clear_all") |
| val clearAllButton = this.wait(Until.findObject(clearAllSelector), FAST_WAIT_TIMEOUT) |
| if (clearAllButton != null) { |
| clearAllButton.click() |
| } |
| } |
| this.pressHome() |
| } |
| |
| /** |
| * Opens quick step and puts the first app from the list of recently used apps into |
| * split-screen |
| * |
| * @throws AssertionError when unable to open the list of recently used apps, or when it does |
| * not contain a button to enter split screen mode |
| */ |
| fun UiDevice.launchSplitScreen( |
| wmHelper: WindowManagerStateHelper |
| ) { |
| openQuickStepAndLongPressOverviewIcon(this, wmHelper) |
| val splitScreenButtonSelector = By.text("Split screen") |
| val splitScreenButton = |
| this.wait(Until.findObject(splitScreenButtonSelector), FIND_TIMEOUT) |
| assertNotNull("Unable to find Split screen button in Overview", splitScreenButton) |
| splitScreenButton.click() |
| |
| // Wait for animation to complete. |
| this.wait(Until.findObject(splitScreenDividerSelector), FIND_TIMEOUT) |
| wmHelper.waitForSurfaceAppeared(DOCKED_STACK_DIVIDER) |
| |
| if (!this.isInSplitScreen()) { |
| Assert.fail("Unable to find Split screen divider") |
| } |
| } |
| |
| /** |
| * Checks if the recent application is able to split screen(resizeable) |
| */ |
| fun UiDevice.canSplitScreen( |
| wmHelper: WindowManagerStateHelper |
| ): Boolean { |
| openQuickStepAndLongPressOverviewIcon(this, wmHelper) |
| val splitScreenButtonSelector = By.text("Split screen") |
| val canSplitScreen = |
| this.wait(Until.findObject(splitScreenButtonSelector), FIND_TIMEOUT) != null |
| this.pressHome() |
| return canSplitScreen |
| } |
| |
| /** |
| * Checks if the device is in split screen by searching for the split screen divider |
| */ |
| fun UiDevice.isInSplitScreen(): Boolean { |
| return this.wait(Until.findObject(splitScreenDividerSelector), FIND_TIMEOUT) != null |
| } |
| |
| fun waitSplitScreenGone(wmHelper: WindowManagerStateHelper): Boolean { |
| val dividerGone = wmHelper.waitFor("stackDividerGone") { |
| !it.layerState.isVisible(DOCKED_STACK_DIVIDER) |
| } |
| wmHelper.waitForAppTransitionIdle() |
| return dividerGone |
| } |
| |
| private val splitScreenDividerSelector: BySelector |
| get() = By.res(SYSTEMUI_PACKAGE, "docked_divider_handle") |
| |
| /** |
| * Drags the split screen divider to the top of the screen to close it |
| * |
| * @throws AssertionError when unable to find the split screen divider |
| */ |
| fun UiDevice.exitSplitScreen() { |
| // Quickstep enabled |
| val divider = this.wait(Until.findObject(splitScreenDividerSelector), FIND_TIMEOUT) |
| assertNotNull("Unable to find Split screen divider", divider) |
| |
| // Drag the split screen divider to the top of the screen |
| val dstPoint = if (this.isRotated()) { |
| Point(0, this.displayWidth / 2) |
| } else { |
| Point(this.displayWidth / 2, 0) |
| } |
| divider.drag(dstPoint, 400) |
| // Wait for animation to complete. |
| SystemClock.sleep(2000) |
| } |
| |
| /** |
| * Drags the split screen divider to the bottom of the screen to close it |
| * |
| * @throws AssertionError when unable to find the split screen divider |
| */ |
| fun UiDevice.exitSplitScreenFromBottom(wmHelper: WindowManagerStateHelper) { |
| // Quickstep enabled |
| val divider = this.wait(Until.findObject(splitScreenDividerSelector), FIND_TIMEOUT) |
| assertNotNull("Unable to find Split screen divider", divider) |
| |
| // Drag the split screen divider to the bottom of the screen |
| val dstPoint = if (this.isRotated()) { |
| Point(this.displayWidth, this.displayWidth / 2) |
| } else { |
| Point(this.displayWidth / 2, this.displayHeight) |
| } |
| divider.drag(dstPoint, 400) |
| if (!waitSplitScreenGone(wmHelper)) { |
| Assert.fail("Split screen divider never disappeared") |
| } |
| } |
| |
| /** |
| * Drags the split screen divider to resize the windows in split screen |
| * |
| * @throws AssertionError when unable to find the split screen divider |
| */ |
| fun UiDevice.resizeSplitScreen(windowHeightRatio: Rational) { |
| val dividerSelector = splitScreenDividerSelector |
| val divider = this.wait(Until.findObject(dividerSelector), FIND_TIMEOUT) |
| assertNotNull("Unable to find Split screen divider", divider) |
| val destHeight = (displayBounds.height() * windowHeightRatio.toFloat()).toInt() |
| |
| // Drag the split screen divider to so that the ratio of top window height and bottom |
| // window height is windowHeightRatio |
| this.drag( |
| divider.visibleBounds.centerX(), |
| divider.visibleBounds.centerY(), |
| this.displayWidth / 2, |
| destHeight, |
| 10) |
| this.wait(Until.findObject(dividerSelector), FIND_TIMEOUT) |
| // Wait for animation to complete. |
| SystemClock.sleep(2000) |
| } |
| |
| /** |
| * Checks if the device has a window with the package name |
| */ |
| fun UiDevice.hasWindow(packageName: String): Boolean { |
| return this.wait(Until.findObject(By.pkg(packageName)), FIND_TIMEOUT) != null |
| } |
| |
| /** |
| * Waits until the package with that name is gone |
| */ |
| fun UiDevice.waitUntilGone(packageName: String): Boolean { |
| return this.wait(Until.gone(By.pkg(packageName)), FIND_TIMEOUT) != null |
| } |
| |
| fun stopPackage(context: Context, packageName: String) { |
| SystemUtil.runShellCommand("am force-stop $packageName") |
| val packageUid = try { |
| context.packageManager.getPackageUid(packageName, /* flags= */0) |
| } catch (e: PackageManager.NameNotFoundException) { |
| return |
| } |
| while (targetPackageIsRunning(packageUid)) { |
| try { |
| Thread.sleep(100) |
| } catch (e: InterruptedException) { // ignore |
| } |
| } |
| } |
| |
| private fun targetPackageIsRunning(uid: Int): Boolean { |
| val result = SystemUtil.runShellCommand("cmd activity get-uid-state $uid") |
| return !result.contains("(NONEXISTENT)") |
| } |
| |
| /** |
| * Turns on the device display and presses the home button to reach the launcher screen |
| */ |
| fun UiDevice.wakeUpAndGoToHomeScreen() { |
| try { |
| this.wakeUp() |
| } catch (e: RemoteException) { |
| throw RuntimeException(e) |
| } |
| this.pressHome() |
| } |