Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 2c0caa00 authored by Nataniel Borges's avatar Nataniel Borges
Browse files

2/ Update app helpers for new apps

Migrate helpers from WMShell app helpers to flicker test packages

Test: atest FlickerLibTest && atest FlickerTests
Change-Id: I98cd3b850e8b873f09c3df2ac5f079b3209a6b44
parent 9c3d6286
Loading
Loading
Loading
Loading
+55 −0
Original line number Diff line number Diff line
/*
 * 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.wm.shell.flicker

import android.app.Instrumentation
import android.content.Context
import android.provider.Settings
import android.util.Log
import com.android.compatibility.common.util.SystemUtil
import java.io.IOException

object MultiWindowUtils {
    private fun executeShellCommand(instrumentation: Instrumentation, cmd: String) {
        try {
            SystemUtil.runShellCommand(instrumentation, cmd)
        } catch (e: IOException) {
            Log.e(MultiWindowUtils::class.simpleName, "executeShellCommand error! $e")
        }
    }

    fun getDevEnableNonResizableMultiWindow(context: Context): Int =
        Settings.Global.getInt(context.contentResolver,
            Settings.Global.DEVELOPMENT_ENABLE_NON_RESIZABLE_MULTI_WINDOW)

    fun setDevEnableNonResizableMultiWindow(context: Context, configValue: Int) =
        Settings.Global.putInt(context.contentResolver,
            Settings.Global.DEVELOPMENT_ENABLE_NON_RESIZABLE_MULTI_WINDOW, configValue)

    fun setSupportsNonResizableMultiWindow(instrumentation: Instrumentation, configValue: Int) =
        executeShellCommand(
            instrumentation,
            createConfigSupportsNonResizableMultiWindowCommand(configValue))

    fun resetMultiWindowConfig(instrumentation: Instrumentation) =
        executeShellCommand(instrumentation, resetMultiWindowConfigCommand)

    private fun createConfigSupportsNonResizableMultiWindowCommand(configValue: Int): String =
        "wm set-multi-window-config --supportsNonResizable $configValue"

    private const val resetMultiWindowConfigCommand: String = "wm reset-multi-window-config"
}
+0 −27
Original line number Diff line number Diff line
/*
 * 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.wm.shell.flicker.helpers

import android.app.Instrumentation
import com.android.server.wm.traces.parser.toFlickerComponent
import com.android.wm.shell.flicker.testapp.Components

class FixedAppHelper(instrumentation: Instrumentation) : BaseAppHelper(
    instrumentation,
    Components.FixedActivity.LABEL,
    Components.FixedActivity.COMPONENT.toFlickerComponent()
)
 No newline at end of file
+0 −76
Original line number Diff line number Diff line
/*
 * 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.wm.shell.flicker.helpers

import android.app.Instrumentation
import androidx.test.uiautomator.By
import androidx.test.uiautomator.Until
import com.android.server.wm.flicker.helpers.FIND_TIMEOUT
import com.android.server.wm.traces.parser.toFlickerComponent
import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
import com.android.wm.shell.flicker.testapp.Components

open class ImeAppHelper(instrumentation: Instrumentation) : BaseAppHelper(
    instrumentation,
    Components.ImeActivity.LABEL,
    Components.ImeActivity.COMPONENT.toFlickerComponent()
) {
    /**
     * Opens the IME and wait for it to be displayed
     *
     * @param wmHelper Helper used to wait for WindowManager states
     */
    open fun openIME(wmHelper: WindowManagerStateHelper) {
        if (!isTelevision) {
            val editText = uiDevice.wait(
                Until.findObject(By.res(getPackage(), "plain_text_input")),
                FIND_TIMEOUT)

            require(editText != null) {
                "Text field not found, this usually happens when the device " +
                    "was left in an unknown state (e.g. in split screen)"
            }
            editText.click()
            wmHelper.StateSyncBuilder()
                .withImeShown()
                .waitForAndVerify()
        } else {
            // If we do the same thing as above - editText.click() - on TV, that's going to force TV
            // into the touch mode. We really don't want that.
            launchViaIntent(action = Components.ImeActivity.ACTION_OPEN_IME)
        }
    }

    /**
     * Opens the IME and wait for it to be gone
     *
     * @param wmHelper Helper used to wait for WindowManager states
     */
    open fun closeIME(wmHelper: WindowManagerStateHelper) {
        if (!isTelevision) {
            uiDevice.pressBack()
            // Using only the AccessibilityInfo it is not possible to identify if the IME is active
            wmHelper.StateSyncBuilder()
                .withImeGone()
                .waitForAndVerify()
        } else {
            // While pressing the back button should close the IME on TV as well, it may also lead
            // to the app closing. So let's instead just ask the app to close the IME.
            launchViaIntent(action = Components.ImeActivity.ACTION_CLOSE_IME)
        }
    }
}
+0 −28
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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.wm.shell.flicker.helpers

import android.app.Instrumentation
import com.android.server.wm.traces.parser.toFlickerComponent
import com.android.wm.shell.flicker.testapp.Components

class SimpleAppHelper(instrumentation: Instrumentation) : BaseAppHelper(
    instrumentation,
    Components.SimpleActivity.LABEL,
    Components.SimpleActivity.COMPONENT.toFlickerComponent()
)
 No newline at end of file
+0 −354
Original line number Diff line number Diff line
/*
 * 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.wm.shell.flicker.helpers

import android.app.Instrumentation
import android.graphics.Point
import android.os.SystemClock
import android.view.InputDevice
import android.view.MotionEvent
import android.view.ViewConfiguration
import androidx.test.uiautomator.By
import androidx.test.uiautomator.BySelector
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.Until
import com.android.launcher3.tapl.LauncherInstrumentation
import com.android.server.wm.traces.common.IComponentMatcher
import com.android.server.wm.traces.common.IComponentNameMatcher
import com.android.server.wm.traces.parser.toFlickerComponent
import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
import com.android.wm.shell.flicker.SPLIT_DECOR_MANAGER
import com.android.wm.shell.flicker.SYSTEM_UI_PACKAGE_NAME
import com.android.wm.shell.flicker.testapp.Components

class SplitScreenHelper(
    instrumentation: Instrumentation,
    activityLabel: String,
    componentInfo: IComponentNameMatcher
) : BaseAppHelper(instrumentation, activityLabel, componentInfo) {

    companion object {
        const val TIMEOUT_MS = 3_000L
        const val DRAG_DURATION_MS = 1_000L
        const val NOTIFICATION_SCROLLER = "notification_stack_scroller"
        const val DIVIDER_BAR = "docked_divider_handle"
        const val GESTURE_STEP_MS = 16L
        const val LONG_PRESS_TIME_MS = 100L

        private val notificationScrollerSelector: BySelector
            get() = By.res(SYSTEM_UI_PACKAGE_NAME, NOTIFICATION_SCROLLER)
        private val notificationContentSelector: BySelector
            get() = By.text("Notification content")
        private val dividerBarSelector: BySelector
            get() = By.res(SYSTEM_UI_PACKAGE_NAME, DIVIDER_BAR)

        fun getPrimary(instrumentation: Instrumentation): SplitScreenHelper =
            SplitScreenHelper(
                instrumentation,
                Components.SplitScreenActivity.LABEL,
                Components.SplitScreenActivity.COMPONENT.toFlickerComponent()
            )

        fun getSecondary(instrumentation: Instrumentation): SplitScreenHelper =
            SplitScreenHelper(
                instrumentation,
                Components.SplitScreenSecondaryActivity.LABEL,
                Components.SplitScreenSecondaryActivity.COMPONENT.toFlickerComponent()
            )

        fun getNonResizeable(instrumentation: Instrumentation): SplitScreenHelper =
            SplitScreenHelper(
                instrumentation,
                Components.NonResizeableActivity.LABEL,
                Components.NonResizeableActivity.COMPONENT.toFlickerComponent()
            )

        fun getSendNotification(instrumentation: Instrumentation): SplitScreenHelper =
            SplitScreenHelper(
                instrumentation,
                Components.SendNotificationActivity.LABEL,
                Components.SendNotificationActivity.COMPONENT.toFlickerComponent()
            )

        fun getIme(instrumentation: Instrumentation): SplitScreenHelper =
            SplitScreenHelper(
                instrumentation,
                Components.ImeActivity.LABEL,
                Components.ImeActivity.COMPONENT.toFlickerComponent()
            )

        fun waitForSplitComplete(
            wmHelper: WindowManagerStateHelper,
            primaryApp: IComponentMatcher,
            secondaryApp: IComponentMatcher,
        ) {
            wmHelper.StateSyncBuilder()
                .withWindowSurfaceAppeared(primaryApp)
                .withWindowSurfaceAppeared(secondaryApp)
                .withSplitDividerVisible()
                .waitForAndVerify()
        }

        fun enterSplit(
            wmHelper: WindowManagerStateHelper,
            tapl: LauncherInstrumentation,
            primaryApp: SplitScreenHelper,
            secondaryApp: SplitScreenHelper
        ) {
            tapl.workspace.switchToOverview().dismissAllTasks()
            primaryApp.launchViaIntent(wmHelper)
            secondaryApp.launchViaIntent(wmHelper)
            tapl.goHome()
            wmHelper.StateSyncBuilder()
                .withHomeActivityVisible()
                .waitForAndVerify()
            splitFromOverview(tapl)
            waitForSplitComplete(wmHelper, primaryApp, secondaryApp)
        }

        fun splitFromOverview(tapl: LauncherInstrumentation) {
            // Note: The initial split position in landscape is different between tablet and phone.
            // In landscape, tablet will let the first app split to right side, and phone will
            // split to left side.
            if (tapl.isTablet) {
                tapl.workspace.switchToOverview().overviewActions
                    .clickSplit()
                    .currentTask
                    .open()
            } else {
                tapl.workspace.switchToOverview().currentTask
                    .tapMenu()
                    .tapSplitMenuItem()
                    .currentTask
                    .open()
            }
            SystemClock.sleep(TIMEOUT_MS)
        }

        fun dragFromNotificationToSplit(
            instrumentation: Instrumentation,
            device: UiDevice,
            wmHelper: WindowManagerStateHelper
        ) {
            val displayBounds = wmHelper.currentState.layerState
                .displays.firstOrNull { !it.isVirtual }
                ?.layerStackSpace
                ?: error("Display not found")

            // Pull down the notifications
            device.swipe(
                displayBounds.centerX(), 5,
                displayBounds.centerX(), displayBounds.bottom, 20 /* steps */
            )
            SystemClock.sleep(TIMEOUT_MS)

            // Find the target notification
            val notificationScroller = device.wait(
                Until.findObject(notificationScrollerSelector), TIMEOUT_MS
            )
            var notificationContent = notificationScroller.findObject(notificationContentSelector)

            while (notificationContent == null) {
                device.swipe(
                    displayBounds.centerX(), displayBounds.centerY(),
                    displayBounds.centerX(), displayBounds.centerY() - 150, 20 /* steps */
                )
                notificationContent = notificationScroller.findObject(notificationContentSelector)
            }

            // Drag to split
            val dragStart = notificationContent.visibleCenter
            val dragMiddle = Point(dragStart.x + 50, dragStart.y)
            val dragEnd = Point(displayBounds.width / 4, displayBounds.width / 4)
            val downTime = SystemClock.uptimeMillis()

            touch(
                instrumentation, MotionEvent.ACTION_DOWN, downTime, downTime,
                TIMEOUT_MS, dragStart
            )
            // It needs a horizontal movement to trigger the drag
            touchMove(
                instrumentation, downTime, SystemClock.uptimeMillis(),
                DRAG_DURATION_MS, dragStart, dragMiddle
            )
            touchMove(
                instrumentation, downTime, SystemClock.uptimeMillis(),
                DRAG_DURATION_MS, dragMiddle, dragEnd
            )
            // Wait for a while to start splitting
            SystemClock.sleep(TIMEOUT_MS)
            touch(
                instrumentation, MotionEvent.ACTION_UP, downTime, SystemClock.uptimeMillis(),
                GESTURE_STEP_MS, dragEnd
            )
            SystemClock.sleep(TIMEOUT_MS)
        }

        fun touch(
            instrumentation: Instrumentation,
            action: Int,
            downTime: Long,
            eventTime: Long,
            duration: Long,
            point: Point
        ) {
            val motionEvent = MotionEvent.obtain(
                downTime, eventTime, action, point.x.toFloat(), point.y.toFloat(), 0
            )
            motionEvent.source = InputDevice.SOURCE_TOUCHSCREEN
            instrumentation.uiAutomation.injectInputEvent(motionEvent, true)
            motionEvent.recycle()
            SystemClock.sleep(duration)
        }

        fun touchMove(
            instrumentation: Instrumentation,
            downTime: Long,
            eventTime: Long,
            duration: Long,
            from: Point,
            to: Point
        ) {
            val steps: Long = duration / GESTURE_STEP_MS
            var currentTime = eventTime
            var currentX = from.x.toFloat()
            var currentY = from.y.toFloat()
            val stepX = (to.x.toFloat() - from.x.toFloat()) / steps.toFloat()
            val stepY = (to.y.toFloat() - from.y.toFloat()) / steps.toFloat()

            for (i in 1..steps) {
                val motionMove = MotionEvent.obtain(
                    downTime, currentTime, MotionEvent.ACTION_MOVE, currentX, currentY, 0
                )
                motionMove.source = InputDevice.SOURCE_TOUCHSCREEN
                instrumentation.uiAutomation.injectInputEvent(motionMove, true)
                motionMove.recycle()

                currentTime += GESTURE_STEP_MS
                if (i == steps - 1) {
                    currentX = to.x.toFloat()
                    currentY = to.y.toFloat()
                } else {
                    currentX += stepX
                    currentY += stepY
                }
                SystemClock.sleep(GESTURE_STEP_MS)
            }
        }

        fun longPress(
            instrumentation: Instrumentation,
            point: Point
        ) {
            val downTime = SystemClock.uptimeMillis()
            touch(instrumentation, MotionEvent.ACTION_DOWN, downTime, downTime, TIMEOUT_MS, point)
            SystemClock.sleep(LONG_PRESS_TIME_MS)
            touch(instrumentation, MotionEvent.ACTION_UP, downTime, downTime, TIMEOUT_MS, point)
        }

        fun createShortcutOnHotseatIfNotExist(
            tapl: LauncherInstrumentation,
            appName: String
        ) {
            tapl.workspace.deleteAppIcon(tapl.workspace.getHotseatAppIcon(0))
            val allApps = tapl.workspace.switchToAllApps()
            allApps.freeze()
            try {
                allApps.getAppIcon(appName).dragToHotseat(0)
            } finally {
                allApps.unfreeze()
            }
        }

        fun dragDividerToResizeAndWait(
            device: UiDevice,
            wmHelper: WindowManagerStateHelper
        ) {
            val displayBounds = wmHelper.currentState.layerState
                .displays.firstOrNull { !it.isVirtual }
                ?.layerStackSpace
                ?: error("Display not found")
            val dividerBar = device.wait(Until.findObject(dividerBarSelector), TIMEOUT_MS)
            dividerBar.drag(Point(displayBounds.width * 1 / 3, displayBounds.height * 2 / 3))

            wmHelper.StateSyncBuilder()
                .withWindowSurfaceDisappeared(SPLIT_DECOR_MANAGER)
                .waitForAndVerify()
        }

        fun dragDividerToDismissSplit(
            device: UiDevice,
            wmHelper: WindowManagerStateHelper,
            dragToRight: Boolean,
            dragToBottom: Boolean
        ) {
            val displayBounds = wmHelper.currentState.layerState
                .displays.firstOrNull { !it.isVirtual }
                ?.layerStackSpace
                ?: error("Display not found")
            val dividerBar = device.wait(Until.findObject(dividerBarSelector), TIMEOUT_MS)
            dividerBar.drag(Point(
                if (dragToRight) {
                    displayBounds.width * 4 / 5
                } else {
                    displayBounds.width * 1 / 5
                },
                if (dragToBottom) {
                    displayBounds.height * 4 / 5
                } else {
                    displayBounds.height * 1 / 5
                }))
        }

        fun doubleTapDividerToSwitch(device: UiDevice) {
            val dividerBar = device.wait(Until.findObject(dividerBarSelector), TIMEOUT_MS)
            val interval = (ViewConfiguration.getDoubleTapTimeout() +
                ViewConfiguration.getDoubleTapMinTime()) / 2
            dividerBar.click()
            SystemClock.sleep(interval.toLong())
            dividerBar.click()
        }

        fun copyContentInSplit(
            instrumentation: Instrumentation,
            device: UiDevice,
            sourceApp: IComponentNameMatcher,
            destinationApp: IComponentNameMatcher,
        ) {
            // Copy text from sourceApp
            val textView = device.wait(Until.findObject(
                By.res(sourceApp.packageName, "SplitScreenTest")), TIMEOUT_MS)
            longPress(instrumentation, textView.getVisibleCenter())

            val copyBtn = device.wait(Until.findObject(By.text("Copy")), TIMEOUT_MS)
            copyBtn.click()

            // Paste text to destinationApp
            val editText = device.wait(Until.findObject(
                By.res(destinationApp.packageName, "plain_text_input")), TIMEOUT_MS)
            longPress(instrumentation, editText.getVisibleCenter())

            val pasteBtn = device.wait(Until.findObject(By.text("Paste")), TIMEOUT_MS)
            pasteBtn.click()

            // Verify text
            if (!textView.getText().contentEquals(editText.getText())) {
                error("Fail to copy content in split")
            }
        }
    }
}
Loading