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

Commit cbae1259 authored by Pablo Gamito's avatar Pablo Gamito
Browse files

Restructure splitscreen tests to new test format

Bug: 287185020
Test: atest WMShellFlickerServiceTests
Change-Id: I0de758ce8c3824cac08846175aeb5e8c19b3c894
parent bf1b4aa1
Loading
Loading
Loading
Loading
+387 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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.service.splitscreen

import android.app.Instrumentation
import android.graphics.Point
import android.os.SystemClock
import android.tools.common.traces.component.ComponentNameMatcher
import android.tools.common.traces.component.IComponentMatcher
import android.tools.common.traces.component.IComponentNameMatcher
import android.tools.device.apphelpers.StandardAppHelper
import android.tools.device.traces.parsers.WindowManagerStateHelper
import android.tools.device.traces.parsers.toFlickerComponent
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.UiObject2
import androidx.test.uiautomator.Until
import com.android.launcher3.tapl.LauncherInstrumentation
import com.android.server.wm.flicker.helpers.ImeAppHelper
import com.android.server.wm.flicker.helpers.NonResizeableAppHelper
import com.android.server.wm.flicker.helpers.NotificationAppHelper
import com.android.server.wm.flicker.helpers.SimpleAppHelper
import com.android.server.wm.flicker.testapp.ActivityOptions
import com.android.wm.shell.flicker.LAUNCHER_UI_PACKAGE_NAME
import com.android.wm.shell.flicker.SYSTEM_UI_PACKAGE_NAME
import org.junit.Assert.assertNotNull

object SplitScreenUtils {
    private const val TIMEOUT_MS = 3_000L
    private const val DRAG_DURATION_MS = 1_000L
    private const val NOTIFICATION_SCROLLER = "notification_stack_scroller"
    private const val DIVIDER_BAR = "docked_divider_handle"
    private const val OVERVIEW_SNAPSHOT = "snapshot"
    private const val GESTURE_STEP_MS = 16L
    private val LONG_PRESS_TIME_MS = ViewConfiguration.getLongPressTimeout() * 2L
    private val SPLIT_DECOR_MANAGER = ComponentNameMatcher("", "SplitDecorManager#")

    private val notificationScrollerSelector: BySelector
        get() = By.res(SYSTEM_UI_PACKAGE_NAME, NOTIFICATION_SCROLLER)
    private val notificationContentSelector: BySelector
        get() = By.text("Flicker Test Notification")
    private val dividerBarSelector: BySelector
        get() = By.res(SYSTEM_UI_PACKAGE_NAME, DIVIDER_BAR)
    private val overviewSnapshotSelector: BySelector
        get() = By.res(LAUNCHER_UI_PACKAGE_NAME, OVERVIEW_SNAPSHOT)

    fun getPrimary(instrumentation: Instrumentation): StandardAppHelper =
        SimpleAppHelper(
            instrumentation,
            ActivityOptions.SplitScreen.Primary.LABEL,
            ActivityOptions.SplitScreen.Primary.COMPONENT.toFlickerComponent()
        )

    fun getSecondary(instrumentation: Instrumentation): StandardAppHelper =
        SimpleAppHelper(
            instrumentation,
            ActivityOptions.SplitScreen.Secondary.LABEL,
            ActivityOptions.SplitScreen.Secondary.COMPONENT.toFlickerComponent()
        )

    fun getNonResizeable(instrumentation: Instrumentation): NonResizeableAppHelper =
        NonResizeableAppHelper(instrumentation)

    fun getSendNotification(instrumentation: Instrumentation): NotificationAppHelper =
        NotificationAppHelper(instrumentation)

    fun getIme(instrumentation: Instrumentation): ImeAppHelper = ImeAppHelper(instrumentation)

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

    fun enterSplit(
        wmHelper: WindowManagerStateHelper,
        tapl: LauncherInstrumentation,
        device: UiDevice,
        primaryApp: StandardAppHelper,
        secondaryApp: StandardAppHelper
    ) {
        primaryApp.launchViaIntent(wmHelper)
        secondaryApp.launchViaIntent(wmHelper)
        tapl.goHome()
        wmHelper.StateSyncBuilder().withHomeActivityVisible().waitForAndVerify()
        splitFromOverview(tapl, device)
        waitForSplitComplete(wmHelper, primaryApp, secondaryApp)
    }

    fun splitFromOverview(tapl: LauncherInstrumentation, device: UiDevice) {
        // 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's currentTask on tablet is sometimes not what we expected if the overview
            // contains more than 3 task views. We need to use uiautomator directly to find the
            // second task to split.
            tapl.workspace.switchToOverview().overviewActions.clickSplit()
            val snapshots = device.wait(Until.findObjects(overviewSnapshotSelector), TIMEOUT_MS)
            if (snapshots == null || snapshots.size < 1) {
                error("Fail to find a overview snapshot to split.")
            }

            // Find the second task in the upper right corner in split select mode by sorting
            // 'left' in descending order and 'top' in ascending order.
            snapshots.sortWith { t1: UiObject2, t2: UiObject2 ->
                t2.getVisibleBounds().left - t1.getVisibleBounds().left
            }
            snapshots.sortWith { t1: UiObject2, t2: UiObject2 ->
                t1.getVisibleBounds().top - t2.getVisibleBounds().top
            }
            snapshots[0].click()
        } else {
            tapl.workspace
                .switchToOverview()
                .currentTask
                .tapMenu()
                .tapSplitMenuItem()
                .currentTask
                .open()
        }
        SystemClock.sleep(TIMEOUT_MS)
    }

    fun enterSplitViaIntent(
        wmHelper: WindowManagerStateHelper,
        primaryApp: StandardAppHelper,
        secondaryApp: StandardAppHelper
    ) {
        val stringExtras =
            mapOf(ActivityOptions.SplitScreen.Primary.EXTRA_LAUNCH_ADJACENT to "true")
        primaryApp.launchViaIntent(wmHelper, null, null, stringExtras)
        SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp)
    }

    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,
            50 /* steps */
        )
        SystemClock.sleep(TIMEOUT_MS)

        // Find the target notification
        val notificationScroller =
            device.wait(Until.findObject(notificationScrollerSelector), TIMEOUT_MS)
                ?: error("Unable to find view $notificationScrollerSelector")
        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 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), 200)

        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
            )
        assertNotNull("Unable to find the TextView", textView)
        textView.click(LONG_PRESS_TIME_MS)

        val copyBtn = device.wait(Until.findObject(By.text("Copy")), TIMEOUT_MS)
        assertNotNull("Unable to find the copy button", copyBtn)
        copyBtn.click()

        // Paste text to destinationApp
        val editText =
            device.wait(
                Until.findObject(By.res(destinationApp.packageName, "plain_text_input")),
                TIMEOUT_MS
            )
        assertNotNull("Unable to find the EditText", editText)
        editText.click(LONG_PRESS_TIME_MS)

        val pasteBtn = device.wait(Until.findObject(By.text("Paste")), TIMEOUT_MS)
        assertNotNull("Unable to find the paste button", pasteBtn)
        pasteBtn.click()

        // Verify text
        if (!textView.text.contentEquals(editText.text)) {
            error("Fail to copy content in split")
        }
    }
}
+42 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.service.splitscreen.flicker

import android.tools.common.Rotation
import android.tools.common.flicker.FlickerConfig
import android.tools.common.flicker.annotation.ExpectedScenarios
import android.tools.common.flicker.annotation.FlickerConfigProvider
import android.tools.common.flicker.config.FlickerConfig
import android.tools.common.flicker.config.FlickerServiceConfig
import android.tools.device.flicker.junit.FlickerServiceJUnit4ClassRunner
import com.android.wm.shell.flicker.service.splitscreen.scenarios.CopyContentInSplit
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(FlickerServiceJUnit4ClassRunner::class)
class CopyContentInSplitGesturalNavLandscape : CopyContentInSplit(Rotation.ROTATION_90) {
    @ExpectedScenarios([])
    @Test
    override fun copyContentInSplit() = super.copyContentInSplit()

    companion object {
        @JvmStatic
        @FlickerConfigProvider
        fun flickerConfigProvider(): FlickerConfig =
            FlickerConfig().use(FlickerServiceConfig.DEFAULT)
    }
}
+42 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.service.splitscreen.flicker

import android.tools.common.Rotation
import android.tools.common.flicker.FlickerConfig
import android.tools.common.flicker.annotation.ExpectedScenarios
import android.tools.common.flicker.annotation.FlickerConfigProvider
import android.tools.common.flicker.config.FlickerConfig
import android.tools.common.flicker.config.FlickerServiceConfig
import android.tools.device.flicker.junit.FlickerServiceJUnit4ClassRunner
import com.android.wm.shell.flicker.service.splitscreen.scenarios.CopyContentInSplit
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(FlickerServiceJUnit4ClassRunner::class)
class CopyContentInSplitGesturalNavPortrait : CopyContentInSplit(Rotation.ROTATION_0) {
    @ExpectedScenarios([])
    @Test
    override fun copyContentInSplit() = super.copyContentInSplit()

    companion object {
        @JvmStatic
        @FlickerConfigProvider
        fun flickerConfigProvider(): FlickerConfig =
            FlickerConfig().use(FlickerServiceConfig.DEFAULT)
    }
}
+44 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.service.splitscreen.flicker

import android.tools.common.Rotation
import android.tools.common.flicker.FlickerConfig
import android.tools.common.flicker.annotation.ExpectedScenarios
import android.tools.common.flicker.annotation.FlickerConfigProvider
import android.tools.common.flicker.config.FlickerConfig
import android.tools.common.flicker.config.FlickerServiceConfig
import android.tools.device.flicker.junit.FlickerServiceJUnit4ClassRunner
import com.android.wm.shell.flicker.service.splitscreen.scenarios.DismissSplitScreenByDivider
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(FlickerServiceJUnit4ClassRunner::class)
class DismissSplitScreenByDividerGesturalNavLandscape :
    DismissSplitScreenByDivider(Rotation.ROTATION_90) {

    @ExpectedScenarios(["SPLIT_SCREEN_EXIT"])
    @Test
    override fun dismissSplitScreenByDivider() = super.dismissSplitScreenByDivider()

    companion object {
        @JvmStatic
        @FlickerConfigProvider
        fun flickerConfigProvider(): FlickerConfig =
            FlickerConfig().use(FlickerServiceConfig.DEFAULT)
    }
}
+44 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.service.splitscreen.flicker

import android.tools.common.Rotation
import android.tools.common.flicker.FlickerConfig
import android.tools.common.flicker.annotation.ExpectedScenarios
import android.tools.common.flicker.annotation.FlickerConfigProvider
import android.tools.common.flicker.config.FlickerConfig
import android.tools.common.flicker.config.FlickerServiceConfig
import android.tools.device.flicker.junit.FlickerServiceJUnit4ClassRunner
import com.android.wm.shell.flicker.service.splitscreen.scenarios.DismissSplitScreenByDivider
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(FlickerServiceJUnit4ClassRunner::class)
class DismissSplitScreenByDividerGesturalNavPortrait :
    DismissSplitScreenByDivider(Rotation.ROTATION_0) {

    @ExpectedScenarios(["SPLIT_SCREEN_EXIT"])
    @Test
    override fun dismissSplitScreenByDivider() = super.dismissSplitScreenByDivider()

    companion object {
        @JvmStatic
        @FlickerConfigProvider
        fun flickerConfigProvider(): FlickerConfig =
            FlickerConfig().use(FlickerServiceConfig.DEFAULT)
    }
}
Loading