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

Commit e234e53b authored by Mark Renouf's avatar Mark Renouf Committed by Android (Google) Code Review
Browse files

Merge changes I777cd7e3,I6d6dea8e,I7c9095ce into main

* changes:
  Handle RootTaskInfo with no child tasks
  Disable WorkProfilePolicy for multiwindow droidfood
  Test coverage for Work and Private profile policies
parents 711ad1c6 d7f84600
Loading
Loading
Loading
Loading
+8 −6
Original line number Diff line number Diff line
@@ -39,11 +39,11 @@ constructor(
    override suspend fun check(content: DisplayContentModel): PolicyResult {
        // The systemUI notification shade isn't a private profile app, skip.
        if (content.systemUiState.shadeExpanded) {
            return NotMatched(policy = NAME, reason = "Notification shade is expanded")
            return NotMatched(policy = NAME, reason = SHADE_EXPANDED)
        }

        // Find the first visible rootTaskInfo with a child task owned by a private user
        val (rootTask, childTask) =
        val childTask =
            content.rootTasks
                .filter { it.isVisible }
                .firstNotNullOfOrNull { root ->
@@ -52,22 +52,24 @@ constructor(
                        .firstOrNull {
                            profileTypes.getProfileType(it.userId) == ProfileType.PRIVATE
                        }
                        ?.let { root to it }
                }
                ?: return NotMatched(policy = NAME, reason = "No private profile tasks are visible")
                ?: return NotMatched(policy = NAME, reason = NO_VISIBLE_TASKS)

        // If matched, return parameters needed to modify the request.
        return Matched(
            policy = NAME,
            reason = "At least one private profile task is visible",
            reason = PRIVATE_TASK_VISIBLE,
            CaptureParameters(
                type = FullScreen(content.displayId),
                component = childTask.componentName ?: rootTask.topActivity,
                component = content.rootTasks.first { it.isVisible }.topActivity,
                owner = UserHandle.of(childTask.userId),
            )
        )
    }
    companion object {
        const val NAME = "PrivateProfile"
        const val SHADE_EXPANDED = "Notification shade is expanded"
        const val NO_VISIBLE_TASKS = "No private profile tasks are visible"
        const val PRIVATE_TASK_VISIBLE = "At least one private profile task is visible"
    }
}
+2 −0
Original line number Diff line number Diff line
@@ -30,3 +30,5 @@ internal fun RootTaskInfo.childTasksTopDown(): Sequence<ChildTaskModel> {
        )
    }
}

internal fun RootTaskInfo.hasChildTasks() = childTaskUserIds.isNotEmpty()
+24 −5
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.systemui.screenshot.policy

import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
import android.app.WindowConfiguration.WINDOWING_MODE_PINNED
import android.os.UserHandle
import com.android.systemui.screenshot.data.model.DisplayContentModel
@@ -24,6 +25,7 @@ import com.android.systemui.screenshot.data.repository.ProfileTypeRepository
import com.android.systemui.screenshot.policy.CapturePolicy.PolicyResult
import com.android.systemui.screenshot.policy.CapturePolicy.PolicyResult.NotMatched
import com.android.systemui.screenshot.policy.CaptureType.IsolatedTask
import com.android.window.flags.Flags
import javax.inject.Inject
import kotlinx.coroutines.flow.first

@@ -41,26 +43,36 @@ constructor(
    override suspend fun check(content: DisplayContentModel): PolicyResult {
        // The systemUI notification shade isn't a work app, skip.
        if (content.systemUiState.shadeExpanded) {
            return NotMatched(policy = NAME, reason = "Notification shade is expanded")
            return NotMatched(policy = NAME, reason = SHADE_EXPANDED)
        }

        if (Flags.enableDesktopWindowingMode()) {
            content.rootTasks.firstOrNull()?.also {
                if (it.windowingMode == WINDOWING_MODE_FREEFORM) {
                    return NotMatched(policy = NAME, reason = DESKTOP_MODE_ENABLED)
                }
            }
        }

        // Find the first non PiP rootTask with a top child task owned by a work user
        val (rootTask, childTask) =
            content.rootTasks
                .filter { it.isVisible && it.windowingMode != WINDOWING_MODE_PINNED }
                .filter {
                    it.isVisible && it.windowingMode != WINDOWING_MODE_PINNED && it.hasChildTasks()
                }
                .map { it to it.childTasksTopDown().first() }
                .firstOrNull { (_, child) ->
                    profileTypes.getProfileType(child.userId) == ProfileType.WORK
                }
                ?: return NotMatched(
                    policy = NAME,
                    reason = "The top-most non-PINNED task does not belong to a work profile user"
                    reason = WORK_TASK_NOT_TOP,
                )

        // If matched, return parameters needed to modify the request.
        return PolicyResult.Matched(
            policy = NAME,
            reason = "The top-most non-PINNED task ($childTask) belongs to a work profile user",
            reason = WORK_TASK_IS_TOP,
            CaptureParameters(
                type = IsolatedTask(taskId = childTask.id, taskBounds = childTask.bounds),
                component = childTask.componentName ?: rootTask.topActivity,
@@ -70,6 +82,13 @@ constructor(
    }

    companion object {
        val NAME = "WorkProfile"
        const val NAME = "WorkProfile"
        const val SHADE_EXPANDED = "Notification shade is expanded"
        const val WORK_TASK_NOT_TOP =
            "The top-most non-PINNED task does not belong to a work profile user"
        const val WORK_TASK_IS_TOP = "The top-most non-PINNED task belongs to a work profile user"
        const val DESKTOP_MODE_ENABLED =
            "enable_desktop_windowing_mode is enabled and top " +
                "RootTask has WINDOWING_MODE_FREEFORM"
    }
}
+12 −0
Original line number Diff line number Diff line
@@ -188,6 +188,18 @@ object DisplayContentScenarios {
     * actual values returned by ActivityTaskManager
     */
    object RootTasks {
        /** An empty RootTaskInfo with no child tasks. */
        val emptyWithNoChildTasks =
            newRootTaskInfo(
                taskId = 2,
                visible = true,
                running = true,
                numActivities = 0,
                bounds = FULL_SCREEN,
            ) {
                emptyList()
            }

        /**
         * The empty RootTaskInfo that is always at the end of a list from ActivityTaskManager when
         * no other visible activities are in split mode
+225 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.systemui.screenshot.policy

import android.content.ComponentName
import android.os.UserHandle
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.screenshot.data.model.DisplayContentModel
import com.android.systemui.screenshot.data.model.DisplayContentScenarios.ActivityNames.FILES
import com.android.systemui.screenshot.data.model.DisplayContentScenarios.ActivityNames.YOUTUBE
import com.android.systemui.screenshot.data.model.DisplayContentScenarios.ActivityNames.YOUTUBE_PIP
import com.android.systemui.screenshot.data.model.DisplayContentScenarios.RootTasks.emptyRootSplit
import com.android.systemui.screenshot.data.model.DisplayContentScenarios.RootTasks.fullScreen
import com.android.systemui.screenshot.data.model.DisplayContentScenarios.RootTasks.launcher
import com.android.systemui.screenshot.data.model.DisplayContentScenarios.TaskSpec
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.SystemUiState
import com.android.systemui.screenshot.data.repository.profileTypeRepository
import com.android.systemui.screenshot.policy.CapturePolicy.PolicyResult.Matched
import com.android.systemui.screenshot.policy.CapturePolicy.PolicyResult.NotMatched
import com.android.systemui.screenshot.policy.CaptureType.FullScreen
import com.android.systemui.screenshot.policy.TestUserIds.PERSONAL
import com.android.systemui.screenshot.policy.TestUserIds.PRIVATE
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
import org.junit.Test

class PrivateProfilePolicyTest {
    private val kosmos = Kosmos()
    private val policy = PrivateProfilePolicy(kosmos.profileTypeRepository)

    // TODO:
    // private app in PIP
    // private app below personal PIP app
    // Freeform windows

    @Test
    fun shadeExpanded_notMatched() = runTest {
        val result =
            policy.check(
                singleFullScreen(
                    spec = TaskSpec(taskId = 1002, name = YOUTUBE, userId = PRIVATE),
                    shadeExpanded = true
                )
            )

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

    @Test
    fun noPrivate_notMatched() = runTest {
        val result =
            policy.check(
                singleFullScreen(TaskSpec(taskId = 1002, name = YOUTUBE, userId = PERSONAL))
            )

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

    @Test
    fun withPrivateFullScreen_isMatched() = runTest {
        val result =
            policy.check(
                singleFullScreen(TaskSpec(taskId = 1002, name = YOUTUBE, userId = PRIVATE))
            )

        assertThat(result)
            .isEqualTo(
                Matched(
                    PrivateProfilePolicy.NAME,
                    PrivateProfilePolicy.PRIVATE_TASK_VISIBLE,
                    CaptureParameters(
                        type = FullScreen(displayId = 0),
                        component = ComponentName.unflattenFromString(YOUTUBE),
                        owner = UserHandle.of(PRIVATE)
                    )
                )
            )
    }

    @Test
    fun withPrivateNotVisible_notMatched() = runTest {
        val result =
            policy.check(
                DisplayContentModel(
                    displayId = 0,
                    systemUiState = SystemUiState(shadeExpanded = false),
                    rootTasks =
                        listOf(
                            fullScreen(
                                TaskSpec(taskId = 1002, name = FILES, userId = PERSONAL),
                                visible = true
                            ),
                            fullScreen(
                                TaskSpec(taskId = 1003, name = YOUTUBE, userId = PRIVATE),
                                visible = false
                            ),
                            launcher(visible = false),
                            emptyRootSplit,
                        )
                )
            )

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

    @Test
    fun withPrivateFocusedInSplitScreen_isMatched() = runTest {
        val result =
            policy.check(
                splitScreenApps(
                    top = TaskSpec(taskId = 1002, name = FILES, userId = PERSONAL),
                    bottom = TaskSpec(taskId = 1003, name = YOUTUBE, userId = PRIVATE),
                    focusedTaskId = 1003
                )
            )

        assertThat(result)
            .isEqualTo(
                Matched(
                    PrivateProfilePolicy.NAME,
                    PrivateProfilePolicy.PRIVATE_TASK_VISIBLE,
                    CaptureParameters(
                        type = FullScreen(displayId = 0),
                        component = ComponentName.unflattenFromString(YOUTUBE),
                        owner = UserHandle.of(PRIVATE)
                    )
                )
            )
    }

    @Test
    fun withPrivateNotFocusedInSplitScreen_isMatched() = runTest {
        val result =
            policy.check(
                splitScreenApps(
                    top = TaskSpec(taskId = 1002, name = FILES, userId = PERSONAL),
                    bottom = TaskSpec(taskId = 1003, name = YOUTUBE, userId = PRIVATE),
                    focusedTaskId = 1002
                )
            )

        assertThat(result)
            .isEqualTo(
                Matched(
                    PrivateProfilePolicy.NAME,
                    PrivateProfilePolicy.PRIVATE_TASK_VISIBLE,
                    CaptureParameters(
                        type = FullScreen(displayId = 0),
                        component = ComponentName.unflattenFromString(FILES),
                        owner = UserHandle.of(PRIVATE)
                    )
                )
            )
    }

    @Test
    fun withPrivatePictureInPictureApp_isMatched() = runTest {
        val result =
            policy.check(
                pictureInPictureApp(TaskSpec(taskId = 1002, name = YOUTUBE_PIP, userId = PRIVATE))
            )

        assertThat(result)
            .isEqualTo(
                Matched(
                    PrivateProfilePolicy.NAME,
                    PrivateProfilePolicy.PRIVATE_TASK_VISIBLE,
                    CaptureParameters(
                        type = FullScreen(displayId = 0),
                        component = ComponentName.unflattenFromString(YOUTUBE_PIP),
                        owner = UserHandle.of(PRIVATE)
                    )
                )
            )
    }

    @Test
    fun withPrivateAppBelowPictureInPictureApp_isMatched() = runTest {
        val result =
            policy.check(
                pictureInPictureApp(
                    pip = TaskSpec(taskId = 1002, name = YOUTUBE_PIP, userId = PERSONAL),
                    fullScreen = TaskSpec(taskId = 1003, name = FILES, userId = PRIVATE),
                )
            )

        assertThat(result)
            .isEqualTo(
                Matched(
                    PrivateProfilePolicy.NAME,
                    PrivateProfilePolicy.PRIVATE_TASK_VISIBLE,
                    CaptureParameters(
                        type = FullScreen(displayId = 0),
                        component = ComponentName.unflattenFromString(YOUTUBE_PIP),
                        owner = UserHandle.of(PRIVATE)
                    )
                )
            )
    }
}
Loading