diff options
6 files changed, 524 insertions, 11 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/policy/PrivateProfilePolicy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/policy/PrivateProfilePolicy.kt index d62ab8574799..1945c2575655 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/policy/PrivateProfilePolicy.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/policy/PrivateProfilePolicy.kt @@ -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" } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/policy/RootTaskInfoExt.kt b/packages/SystemUI/src/com/android/systemui/screenshot/policy/RootTaskInfoExt.kt index 3789371d7c33..f768cfb2ceb5 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/policy/RootTaskInfoExt.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/policy/RootTaskInfoExt.kt @@ -30,3 +30,5 @@ internal fun RootTaskInfo.childTasksTopDown(): Sequence<ChildTaskModel> { ) } } + +internal fun RootTaskInfo.hasChildTasks() = childTaskUserIds.isNotEmpty() diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/policy/WorkProfilePolicy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/policy/WorkProfilePolicy.kt index b781ae99a4de..fdf16aa9d081 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/policy/WorkProfilePolicy.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/policy/WorkProfilePolicy.kt @@ -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" } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/data/model/DisplayContentScenarios.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/data/model/DisplayContentScenarios.kt index 621b0582c538..254f1e1efe13 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/data/model/DisplayContentScenarios.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/data/model/DisplayContentScenarios.kt @@ -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 diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/policy/PrivateProfilePolicyTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/policy/PrivateProfilePolicyTest.kt new file mode 100644 index 000000000000..9e3ae054d31b --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/policy/PrivateProfilePolicyTest.kt @@ -0,0 +1,225 @@ +/* + * 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) + ) + ) + ) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/policy/WorkProfilePolicyTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/policy/WorkProfilePolicyTest.kt new file mode 100644 index 000000000000..5d35528b0cf0 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/policy/WorkProfilePolicyTest.kt @@ -0,0 +1,253 @@ +/* + * 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 android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +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.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.SystemUiState +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.systemui.screenshot.policy.TestUserIds.PERSONAL +import com.android.systemui.screenshot.policy.TestUserIds.WORK +import com.android.systemui.screenshot.policy.WorkProfilePolicy.Companion.DESKTOP_MODE_ENABLED +import com.android.systemui.screenshot.policy.WorkProfilePolicy.Companion.SHADE_EXPANDED +import com.android.systemui.screenshot.policy.WorkProfilePolicy.Companion.WORK_TASK_IS_TOP +import com.android.systemui.screenshot.policy.WorkProfilePolicy.Companion.WORK_TASK_NOT_TOP +import com.android.window.flags.Flags +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class WorkProfilePolicyTest { + @JvmField @Rule val setFlagsRule = SetFlagsRule() + + private val kosmos = Kosmos() + private val policy = WorkProfilePolicy(kosmos.profileTypeRepository) + + /** + * There is no guarantee that every RootTaskInfo contains a non-empty list of child tasks. Test + * the case where the RootTaskInfo would match but child tasks are empty. + */ + @Test + fun withEmptyChildTasks_notMatched() = runTest { + val result = + policy.check( + DisplayContentModel( + displayId = 0, + systemUiState = SystemUiState(shadeExpanded = false), + rootTasks = listOf(RootTasks.emptyWithNoChildTasks) + ) + ) + + assertThat(result) + .isEqualTo( + NotMatched( + WorkProfilePolicy.NAME, + WORK_TASK_NOT_TOP, + ) + ) + } + + @Test + fun noWorkApp_notMatched() = runTest { + val result = + policy.check( + singleFullScreen(TaskSpec(taskId = 1002, name = YOUTUBE, userId = PERSONAL)) + ) + + assertThat(result) + .isEqualTo( + NotMatched( + WorkProfilePolicy.NAME, + WORK_TASK_NOT_TOP, + ) + ) + } + + @Test + fun withWorkFullScreen_shadeExpanded_notMatched() = runTest { + val result = + policy.check( + singleFullScreen( + TaskSpec(taskId = 1002, name = FILES, userId = WORK), + shadeExpanded = true + ) + ) + + assertThat(result) + .isEqualTo( + NotMatched( + WorkProfilePolicy.NAME, + SHADE_EXPANDED, + ) + ) + } + + @Test + fun withWorkFullScreen_matched() = runTest { + val result = + policy.check(singleFullScreen(TaskSpec(taskId = 1002, name = FILES, userId = WORK))) + + assertThat(result) + .isEqualTo( + PolicyResult.Matched( + policy = WorkProfilePolicy.NAME, + reason = WORK_TASK_IS_TOP, + CaptureParameters( + type = IsolatedTask(taskId = 1002, taskBounds = FULL_SCREEN), + component = ComponentName.unflattenFromString(FILES), + owner = UserHandle.of(WORK), + ) + ) + ) + } + + @Test + fun withWorkFocusedInSplitScreen_matched() = runTest { + val result = + policy.check( + splitScreenApps( + top = TaskSpec(taskId = 1002, name = FILES, userId = WORK), + bottom = TaskSpec(taskId = 1003, name = YOUTUBE, userId = PERSONAL), + focusedTaskId = 1002 + ) + ) + + assertThat(result) + .isEqualTo( + PolicyResult.Matched( + policy = WorkProfilePolicy.NAME, + reason = WORK_TASK_IS_TOP, + CaptureParameters( + type = IsolatedTask(taskId = 1002, taskBounds = SPLIT_TOP), + component = ComponentName.unflattenFromString(FILES), + owner = UserHandle.of(WORK), + ) + ) + ) + } + + @Test + fun withWorkNotFocusedInSplitScreen_notMatched() = runTest { + val result = + policy.check( + splitScreenApps( + top = TaskSpec(taskId = 1002, name = FILES, userId = WORK), + bottom = TaskSpec(taskId = 1003, name = YOUTUBE, userId = PERSONAL), + focusedTaskId = 1003 + ) + ) + + assertThat(result) + .isEqualTo( + NotMatched( + WorkProfilePolicy.NAME, + WORK_TASK_NOT_TOP, + ) + ) + } + + @Test + fun withWorkBelowPersonalPictureInPicture_matched() = runTest { + val result = + policy.check( + pictureInPictureApp( + pip = TaskSpec(taskId = 1002, name = YOUTUBE, userId = PERSONAL), + fullScreen = TaskSpec(taskId = 1003, name = FILES, userId = WORK), + ) + ) + + assertThat(result) + .isEqualTo( + PolicyResult.Matched( + policy = WorkProfilePolicy.NAME, + reason = WORK_TASK_IS_TOP, + CaptureParameters( + type = IsolatedTask(taskId = 1003, taskBounds = FULL_SCREEN), + component = ComponentName.unflattenFromString(FILES), + owner = UserHandle.of(WORK), + ) + ) + ) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + fun withWorkFocusedInFreeForm_matched() = runTest { + val result = + policy.check( + freeFormApps( + TaskSpec(taskId = 1002, name = YOUTUBE, userId = PERSONAL), + TaskSpec(taskId = 1003, name = FILES, userId = WORK), + focusedTaskId = 1003 + ) + ) + + assertThat(result) + .isEqualTo( + PolicyResult.Matched( + policy = WorkProfilePolicy.NAME, + reason = WORK_TASK_IS_TOP, + CaptureParameters( + type = IsolatedTask(taskId = 1003, taskBounds = FREE_FORM), + component = ComponentName.unflattenFromString(FILES), + owner = UserHandle.of(WORK), + ) + ) + ) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + fun withWorkFocusedInFreeForm_desktopModeEnabled_notMatched() = runTest { + val result = + policy.check( + freeFormApps( + TaskSpec(taskId = 1002, name = YOUTUBE, userId = PERSONAL), + TaskSpec(taskId = 1003, name = FILES, userId = WORK), + focusedTaskId = 1003 + ) + ) + + assertThat(result) + .isEqualTo( + NotMatched( + WorkProfilePolicy.NAME, + DESKTOP_MODE_ENABLED, + ) + ) + } +} |