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

Commit d39fd18d authored by Mark Renouf's avatar Mark Renouf
Browse files

Improved screenshot policy for desktop and split mode

The new unified policy class simplifies the rules and behavior
of screenshots, handling situations with mixed profile content
on screen.

Improvements:

When two work profile tasks apear as split-screen mode, the
screenshot will now capture both.

When desktop mode is active with freeform windows, content
will be saved to the private profile if any is private profile,
otherwise to the personal profile.

When in desktop mode with a single maximized window, it is
treated as a full screen task and handled accordingly.

The focused task is no longer used to determine the owner of the
screenshot making the result less surprising to end users.

If work content appears beside personal content in split mode,
the screenshot will remain full-screen and be saved to the personal
profile.

Bug: 365597999
Flag: com.android.systemui.screenshot_policy_split_and_desktop_mode
Test: atest ScreenshotPolicyTest
Change-Id: I5353b477d527b79f8b95803d7ba6136fdba441be
parent 9c6750cf
Loading
Loading
Loading
Loading
+94 −43
Original line number Diff line number Diff line
@@ -21,8 +21,8 @@ import android.graphics.Rect
import com.android.systemui.screenshot.data.model.DisplayContentScenarios.Bounds.FREE_FORM
import com.android.systemui.screenshot.data.model.DisplayContentScenarios.Bounds.FULL_SCREEN
import com.android.systemui.screenshot.data.model.DisplayContentScenarios.Bounds.PIP
import com.android.systemui.screenshot.data.model.DisplayContentScenarios.Bounds.SPLIT_BOTTOM
import com.android.systemui.screenshot.data.model.DisplayContentScenarios.Bounds.SPLIT_TOP
import com.android.systemui.screenshot.data.model.DisplayContentScenarios.Orientation.HORIZONTAL
import com.android.systemui.screenshot.data.model.DisplayContentScenarios.Orientation.VERTICAL
import com.android.systemui.screenshot.data.model.DisplayContentScenarios.RootTasks.emptyRootSplit
import com.android.systemui.screenshot.data.model.DisplayContentScenarios.RootTasks.freeForm
import com.android.systemui.screenshot.data.model.DisplayContentScenarios.RootTasks.fullScreen
@@ -39,16 +39,14 @@ object DisplayContentScenarios {

    data class TaskSpec(val taskId: Int, val userId: Int, val name: String)

    val emptyDisplayContent = DisplayContentModel(0, SystemUiState(shadeExpanded = false), listOf())

    /** Home screen, with only the launcher visible */
    fun launcherOnly(shadeExpanded: Boolean = false) =
        DisplayContentModel(
            displayId = 0,
            systemUiState = SystemUiState(shadeExpanded = shadeExpanded),
            rootTasks =
                listOf(
                    launcher(visible = true),
                    emptyRootSplit,
                )
            rootTasks = listOf(launcher(visible = true), emptyRootSplit),
        )

    /** A Full screen activity for the personal (primary) user, with launcher behind it */
@@ -57,48 +55,72 @@ object DisplayContentScenarios {
            displayId = 0,
            systemUiState = SystemUiState(shadeExpanded = shadeExpanded),
            rootTasks =
                listOf(
                    fullScreen(spec, visible = true),
                    launcher(visible = false),
                    emptyRootSplit,
                )
                listOf(fullScreen(spec, visible = true), launcher(visible = false), emptyRootSplit),
        )

    enum class Orientation {
        HORIZONTAL,
        VERTICAL,
    }

    internal fun Rect.splitLeft(margin: Int = 0) = Rect(left, top, centerX() - margin, bottom)

    internal fun Rect.splitRight(margin: Int = 0) = Rect(centerX() + margin, top, right, bottom)

    internal fun Rect.splitTop(margin: Int = 0) = Rect(left, top, right, centerY() - margin)

    internal fun Rect.splitBottom(margin: Int = 0) = Rect(left, centerY() + margin, right, bottom)

    fun splitScreenApps(
        top: TaskSpec,
        bottom: TaskSpec,
        displayId: Int = 0,
        parentBounds: Rect = FULL_SCREEN,
        taskMargin: Int = 0,
        orientation: Orientation = VERTICAL,
        first: TaskSpec,
        second: TaskSpec,
        focusedTaskId: Int,
        parentTaskId: Int = 2,
        shadeExpanded: Boolean = false,
    ): DisplayContentModel {
        val topBounds = SPLIT_TOP
        val bottomBounds = SPLIT_BOTTOM

        val firstBounds =
            when (orientation) {
                VERTICAL -> parentBounds.splitTop(taskMargin)
                HORIZONTAL -> parentBounds.splitLeft(taskMargin)
            }
        val secondBounds =
            when (orientation) {
                VERTICAL -> parentBounds.splitBottom(taskMargin)
                HORIZONTAL -> parentBounds.splitRight(taskMargin)
            }

        return DisplayContentModel(
            displayId = 0,
            displayId = displayId,
            systemUiState = SystemUiState(shadeExpanded = shadeExpanded),
            rootTasks =
                listOf(
                    newRootTaskInfo(
                        taskId = 2,
                        taskId = parentTaskId,
                        userId = TestUserIds.PERSONAL,
                        bounds = FULL_SCREEN,
                        bounds = parentBounds,
                        topActivity =
                            ComponentName.unflattenFromString(
                                if (top.taskId == focusedTaskId) top.name else bottom.name
                                if (first.taskId == focusedTaskId) first.name else second.name
                            ),
                    ) {
                        listOf(
                                newChildTask(
                                    taskId = top.taskId,
                                    bounds = topBounds,
                                    userId = top.userId,
                                    name = top.name
                                    taskId = first.taskId,
                                    bounds = firstBounds,
                                    userId = first.userId,
                                    name = first.name,
                                ),
                                newChildTask(
                                    taskId = bottom.taskId,
                                    bounds = bottomBounds,
                                    userId = bottom.userId,
                                    name = bottom.name
                                )
                                    taskId = second.taskId,
                                    bounds = secondBounds,
                                    userId = second.userId,
                                    name = second.name,
                                ),
                            )
                            // Child tasks are ordered bottom-up in RootTaskInfo.
                            // Sort 'focusedTaskId' last.
@@ -106,7 +128,7 @@ object DisplayContentScenarios {
                            .sortedBy { it.id == focusedTaskId }
                    },
                    launcher(visible = false),
                )
                ),
        )
    }

@@ -124,7 +146,7 @@ object DisplayContentScenarios {
                    fullScreen?.also { add(fullScreen(it, visible = true)) }
                    add(launcher(visible = (fullScreen == null)))
                    add(emptyRootSplit)
                }
                },
        )
    }

@@ -142,7 +164,7 @@ object DisplayContentScenarios {
        return DisplayContentModel(
            displayId = 0,
            systemUiState = SystemUiState(shadeExpanded = shadeExpanded),
            rootTasks = freeFormTasks + launcher(visible = true) + emptyRootSplit
            rootTasks = freeFormTasks + launcher(visible = true) + emptyRootSplit,
        )
    }

@@ -153,11 +175,18 @@ object DisplayContentScenarios {
     * somewhat sensible in terms of logical position (Re: PIP, SPLIT, etc).
     */
    object Bounds {
        // "Phone" size
        val FULL_SCREEN = Rect(0, 0, 1080, 2400)
        val PIP = Rect(440, 1458, 1038, 1794)
        val SPLIT_TOP = Rect(0, 0, 1080, 1187)
        val SPLIT_BOTTOM = Rect(0, 1213, 1080, 2400)
        val FREE_FORM = Rect(119, 332, 1000, 1367)

        // "Tablet" size
        val FREEFORM_FULL_SCREEN = Rect(0, 0, 2560, 1600)
        val FREEFORM_MAXIMIZED = Rect(0, 48, 2560, 1480)
        val FREEFORM_SPLIT_LEFT = Rect(0, 0, 1270, 1600)
        val FREEFORM_SPLIT_RIGHT = Rect(1290, 0, 2560, 1600)
    }

    /** A collection of task names used in test scenarios */
@@ -177,6 +206,8 @@ object DisplayContentScenarios {
            "com.google.android.youtube/" +
                "com.google.android.apps.youtube.app.watchwhile.WatchWhileActivity"

        const val MESSAGES = "com.google.android.apps.messaging/.ui.ConversationListActivity"

        /** The NexusLauncher activity */
        const val LAUNCHER =
            "com.google.android.apps.nexuslauncher/" +
@@ -220,7 +251,7 @@ object DisplayContentScenarios {
            }

        /** NexusLauncher on the default display. Usually below all other visible tasks */
        fun launcher(visible: Boolean) =
        fun launcher(visible: Boolean, bounds: Rect = FULL_SCREEN) =
            newRootTaskInfo(
                taskId = 1,
                activityType = ActivityType.Home,
@@ -229,43 +260,63 @@ object DisplayContentScenarios {
                topActivity = ComponentName.unflattenFromString(ActivityNames.LAUNCHER),
                topActivityType = ActivityType.Home,
            ) {
                listOf(newChildTask(taskId = 1002, name = ActivityNames.LAUNCHER))
                listOf(newChildTask(taskId = 1002, name = ActivityNames.LAUNCHER, bounds = bounds))
            }

        /** A full screen Activity */
        fun fullScreen(task: TaskSpec, visible: Boolean) =
        fun fullScreen(task: TaskSpec, visible: Boolean, bounds: Rect = FULL_SCREEN) =
            newRootTaskInfo(
                taskId = task.taskId,
                userId = task.userId,
                visible = visible,
                bounds = FULL_SCREEN,
                bounds = bounds,
                topActivity = ComponentName.unflattenFromString(task.name),
            ) {
                listOf(newChildTask(taskId = task.taskId, userId = task.userId, name = task.name))
                listOf(
                    newChildTask(
                        taskId = task.taskId,
                        userId = task.userId,
                        name = task.name,
                        bounds = bounds,
                    )
                )
            }

        /** An activity in Picture-in-Picture mode */
        fun pictureInPicture(task: TaskSpec) =
        fun pictureInPicture(task: TaskSpec, bounds: Rect = PIP) =
            newRootTaskInfo(
                taskId = task.taskId,
                userId = task.userId,
                bounds = PIP,
                windowingMode = WindowingMode.PictureInPicture,
                topActivity = ComponentName.unflattenFromString(task.name),
            ) {
                listOf(newChildTask(taskId = task.taskId, userId = userId, name = task.name))
                listOf(
                    newChildTask(
                        taskId = task.taskId,
                        userId = userId,
                        name = task.name,
                        bounds = bounds,
                    )
                )
            }

        /** An activity in FreeForm mode */
        fun freeForm(task: TaskSpec) =
        fun freeForm(task: TaskSpec, bounds: Rect = FREE_FORM) =
            newRootTaskInfo(
                taskId = task.taskId,
                userId = task.userId,
                bounds = FREE_FORM,
                bounds = bounds,
                windowingMode = WindowingMode.Freeform,
                topActivity = ComponentName.unflattenFromString(task.name),
            ) {
                listOf(newChildTask(taskId = task.taskId, userId = userId, name = task.name))
                listOf(
                    newChildTask(
                        taskId = task.taskId,
                        userId = userId,
                        name = task.name,
                        bounds = bounds,
                    )
                )
            }
    }
}
+2 −2
Original line number Diff line number Diff line
@@ -69,7 +69,7 @@ fun RootTaskInfo.newChildTask(
    taskId: Int,
    name: String,
    bounds: Rect? = null,
    userId: Int? = null
    userId: Int? = null,
): ChildTaskModel {
    return ChildTaskModel(taskId, name, bounds ?: this.bounds, userId ?: this.userId)
}
@@ -83,7 +83,7 @@ fun newRootTaskInfo(
    running: Boolean = true,
    activityType: ActivityType = Standard,
    windowingMode: WindowingMode = FullScreen,
    bounds: Rect? = null,
    bounds: Rect = Rect(),
    topActivity: ComponentName? = null,
    topActivityType: ActivityType = Standard,
    numActivities: Int? = null,
+22 −27
Original line number Diff line number Diff line
@@ -17,8 +17,8 @@
package com.android.systemui.screenshot.policy

import android.content.ComponentName
import androidx.test.ext.junit.runners.AndroidJUnit4
import android.os.UserHandle
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.screenshot.data.model.DisplayContentModel
import com.android.systemui.screenshot.data.model.DisplayContentScenarios.ActivityNames.FILES
@@ -59,7 +59,7 @@ class PrivateProfilePolicyTest {
            policy.check(
                singleFullScreen(
                    spec = TaskSpec(taskId = 1002, name = YOUTUBE, userId = PRIVATE),
                    shadeExpanded = true
                    shadeExpanded = true,
                )
            )

@@ -93,8 +93,8 @@ class PrivateProfilePolicyTest {
                    CaptureParameters(
                        type = FullScreen(displayId = 0),
                        component = ComponentName.unflattenFromString(YOUTUBE),
                        owner = UserHandle.of(PRIVATE)
                    )
                        owner = UserHandle.of(PRIVATE),
                    ),
                )
            )
    }
@@ -110,25 +110,20 @@ class PrivateProfilePolicyTest {
                        listOf(
                            fullScreen(
                                TaskSpec(taskId = 1002, name = FILES, userId = PERSONAL),
                                visible = true
                                visible = true,
                            ),
                            fullScreen(
                                TaskSpec(taskId = 1003, name = YOUTUBE, userId = PRIVATE),
                                visible = false
                                visible = false,
                            ),
                            launcher(visible = false),
                            emptyRootSplit,
                        )
                        ),
                )
            )

        assertThat(result)
            .isEqualTo(
                NotMatched(
                    PrivateProfilePolicy.NAME,
                    PrivateProfilePolicy.NO_VISIBLE_TASKS,
                )
            )
            .isEqualTo(NotMatched(PrivateProfilePolicy.NAME, PrivateProfilePolicy.NO_VISIBLE_TASKS))
    }

    @Test
@@ -136,9 +131,9 @@ class PrivateProfilePolicyTest {
        val result =
            policy.check(
                splitScreenApps(
                    top = TaskSpec(taskId = 1002, name = FILES, userId = PERSONAL),
                    bottom = TaskSpec(taskId = 1003, name = YOUTUBE, userId = PRIVATE),
                    focusedTaskId = 1003
                    first = TaskSpec(taskId = 1002, name = FILES, userId = PERSONAL),
                    second = TaskSpec(taskId = 1003, name = YOUTUBE, userId = PRIVATE),
                    focusedTaskId = 1003,
                )
            )

@@ -150,8 +145,8 @@ class PrivateProfilePolicyTest {
                    CaptureParameters(
                        type = FullScreen(displayId = 0),
                        component = ComponentName.unflattenFromString(YOUTUBE),
                        owner = UserHandle.of(PRIVATE)
                    )
                        owner = UserHandle.of(PRIVATE),
                    ),
                )
            )
    }
@@ -161,9 +156,9 @@ class PrivateProfilePolicyTest {
        val result =
            policy.check(
                splitScreenApps(
                    top = TaskSpec(taskId = 1002, name = FILES, userId = PERSONAL),
                    bottom = TaskSpec(taskId = 1003, name = YOUTUBE, userId = PRIVATE),
                    focusedTaskId = 1002
                    first = TaskSpec(taskId = 1002, name = FILES, userId = PERSONAL),
                    second = TaskSpec(taskId = 1003, name = YOUTUBE, userId = PRIVATE),
                    focusedTaskId = 1002,
                )
            )

@@ -175,8 +170,8 @@ class PrivateProfilePolicyTest {
                    CaptureParameters(
                        type = FullScreen(displayId = 0),
                        component = ComponentName.unflattenFromString(FILES),
                        owner = UserHandle.of(PRIVATE)
                    )
                        owner = UserHandle.of(PRIVATE),
                    ),
                )
            )
    }
@@ -196,8 +191,8 @@ class PrivateProfilePolicyTest {
                    CaptureParameters(
                        type = FullScreen(displayId = 0),
                        component = ComponentName.unflattenFromString(YOUTUBE_PIP),
                        owner = UserHandle.of(PRIVATE)
                    )
                        owner = UserHandle.of(PRIVATE),
                    ),
                )
            )
    }
@@ -220,8 +215,8 @@ class PrivateProfilePolicyTest {
                    CaptureParameters(
                        type = FullScreen(displayId = 0),
                        component = ComponentName.unflattenFromString(YOUTUBE_PIP),
                        owner = UserHandle.of(PRIVATE)
                    )
                        owner = UserHandle.of(PRIVATE),
                    ),
                )
            )
    }
+351 −0

File added.

Preview size limit exceeded, changes collapsed.

+24 −51
Original line number Diff line number Diff line
@@ -31,13 +31,13 @@ import com.android.systemui.screenshot.data.model.DisplayContentScenarios.Activi
import com.android.systemui.screenshot.data.model.DisplayContentScenarios.ActivityNames.YOUTUBE
import com.android.systemui.screenshot.data.model.DisplayContentScenarios.Bounds.FREE_FORM
import com.android.systemui.screenshot.data.model.DisplayContentScenarios.Bounds.FULL_SCREEN
import com.android.systemui.screenshot.data.model.DisplayContentScenarios.Bounds.SPLIT_TOP
import com.android.systemui.screenshot.data.model.DisplayContentScenarios.RootTasks
import com.android.systemui.screenshot.data.model.DisplayContentScenarios.TaskSpec
import com.android.systemui.screenshot.data.model.DisplayContentScenarios.freeFormApps
import com.android.systemui.screenshot.data.model.DisplayContentScenarios.pictureInPictureApp
import com.android.systemui.screenshot.data.model.DisplayContentScenarios.singleFullScreen
import com.android.systemui.screenshot.data.model.DisplayContentScenarios.splitScreenApps
import com.android.systemui.screenshot.data.model.DisplayContentScenarios.splitTop
import com.android.systemui.screenshot.data.model.SystemUiState
import com.android.systemui.screenshot.data.repository.profileTypeRepository
import com.android.systemui.screenshot.policy.CapturePolicy.PolicyResult
@@ -69,6 +69,7 @@ class WorkProfilePolicyTest {
    @JvmField @Rule(order = 2) val mockitoRule: MockitoRule = MockitoJUnit.rule()

    @Mock lateinit var mContext: Context

    @Mock lateinit var mResources: Resources

    private val kosmos = Kosmos()
@@ -94,17 +95,11 @@ class WorkProfilePolicyTest {
                DisplayContentModel(
                    displayId = 0,
                    systemUiState = SystemUiState(shadeExpanded = false),
                    rootTasks = listOf(RootTasks.emptyWithNoChildTasks)
                    rootTasks = listOf(RootTasks.emptyWithNoChildTasks),
                )
            )

        assertThat(result)
            .isEqualTo(
                NotMatched(
                    WorkProfilePolicy.NAME,
                    WORK_TASK_NOT_TOP,
                )
            )
        assertThat(result).isEqualTo(NotMatched(WorkProfilePolicy.NAME, WORK_TASK_NOT_TOP))
    }

    @Test
@@ -114,13 +109,7 @@ class WorkProfilePolicyTest {
                singleFullScreen(TaskSpec(taskId = 1002, name = YOUTUBE, userId = PERSONAL))
            )

        assertThat(result)
            .isEqualTo(
                NotMatched(
                    WorkProfilePolicy.NAME,
                    WORK_TASK_NOT_TOP,
                )
            )
        assertThat(result).isEqualTo(NotMatched(WorkProfilePolicy.NAME, WORK_TASK_NOT_TOP))
    }

    @Test
@@ -129,17 +118,11 @@ class WorkProfilePolicyTest {
            policy.check(
                singleFullScreen(
                    TaskSpec(taskId = 1002, name = FILES, userId = WORK),
                    shadeExpanded = true
                    shadeExpanded = true,
                )
            )

        assertThat(result)
            .isEqualTo(
                NotMatched(
                    WorkProfilePolicy.NAME,
                    SHADE_EXPANDED,
                )
            )
        assertThat(result).isEqualTo(NotMatched(WorkProfilePolicy.NAME, SHADE_EXPANDED))
    }

    @Test
@@ -156,7 +139,7 @@ class WorkProfilePolicyTest {
                        type = IsolatedTask(taskId = 1002, taskBounds = FULL_SCREEN),
                        component = ComponentName.unflattenFromString(FILES),
                        owner = UserHandle.of(WORK),
                    )
                    ),
                )
            )
    }
@@ -166,9 +149,11 @@ class WorkProfilePolicyTest {
        val result =
            policy.check(
                splitScreenApps(
                    top = TaskSpec(taskId = 1002, name = FILES, userId = WORK),
                    bottom = TaskSpec(taskId = 1003, name = YOUTUBE, userId = PERSONAL),
                    focusedTaskId = 1002
                    parentBounds = FULL_SCREEN,
                    taskMargin = 20,
                    first = TaskSpec(taskId = 1002, name = FILES, userId = WORK),
                    second = TaskSpec(taskId = 1003, name = YOUTUBE, userId = PERSONAL),
                    focusedTaskId = 1002,
                )
            )

@@ -178,10 +163,10 @@ class WorkProfilePolicyTest {
                    policy = WorkProfilePolicy.NAME,
                    reason = WORK_TASK_IS_TOP,
                    CaptureParameters(
                        type = IsolatedTask(taskId = 1002, taskBounds = SPLIT_TOP),
                        type = IsolatedTask(taskId = 1002, taskBounds = FULL_SCREEN.splitTop(20)),
                        component = ComponentName.unflattenFromString(FILES),
                        owner = UserHandle.of(WORK),
                    )
                    ),
                )
            )
    }
@@ -191,19 +176,13 @@ class WorkProfilePolicyTest {
        val result =
            policy.check(
                splitScreenApps(
                    top = TaskSpec(taskId = 1002, name = FILES, userId = WORK),
                    bottom = TaskSpec(taskId = 1003, name = YOUTUBE, userId = PERSONAL),
                    focusedTaskId = 1003
                    first = TaskSpec(taskId = 1002, name = FILES, userId = WORK),
                    second = TaskSpec(taskId = 1003, name = YOUTUBE, userId = PERSONAL),
                    focusedTaskId = 1003,
                )
            )

        assertThat(result)
            .isEqualTo(
                NotMatched(
                    WorkProfilePolicy.NAME,
                    WORK_TASK_NOT_TOP,
                )
            )
        assertThat(result).isEqualTo(NotMatched(WorkProfilePolicy.NAME, WORK_TASK_NOT_TOP))
    }

    @Test
@@ -225,7 +204,7 @@ class WorkProfilePolicyTest {
                        type = IsolatedTask(taskId = 1003, taskBounds = FULL_SCREEN),
                        component = ComponentName.unflattenFromString(FILES),
                        owner = UserHandle.of(WORK),
                    )
                    ),
                )
            )
    }
@@ -238,7 +217,7 @@ class WorkProfilePolicyTest {
                freeFormApps(
                    TaskSpec(taskId = 1002, name = YOUTUBE, userId = PERSONAL),
                    TaskSpec(taskId = 1003, name = FILES, userId = WORK),
                    focusedTaskId = 1003
                    focusedTaskId = 1003,
                )
            )

@@ -251,7 +230,7 @@ class WorkProfilePolicyTest {
                        type = IsolatedTask(taskId = 1003, taskBounds = FREE_FORM),
                        component = ComponentName.unflattenFromString(FILES),
                        owner = UserHandle.of(WORK),
                    )
                    ),
                )
            )
    }
@@ -264,16 +243,10 @@ class WorkProfilePolicyTest {
                freeFormApps(
                    TaskSpec(taskId = 1002, name = YOUTUBE, userId = PERSONAL),
                    TaskSpec(taskId = 1003, name = FILES, userId = WORK),
                    focusedTaskId = 1003
                    focusedTaskId = 1003,
                )
            )

        assertThat(result)
            .isEqualTo(
                NotMatched(
                    WorkProfilePolicy.NAME,
                    DESKTOP_MODE_ENABLED,
                )
            )
        assertThat(result).isEqualTo(NotMatched(WorkProfilePolicy.NAME, DESKTOP_MODE_ENABLED))
    }
}
Loading