Loading packages/SystemUI/src/com/android/systemui/screenshot/policy/CapturePolicy.kt +23 −2 Original line number Diff line number Diff line Loading @@ -22,7 +22,28 @@ import com.android.systemui.screenshot.data.model.DisplayContentModel fun interface CapturePolicy { /** * Test the policy against the current display task state. If the policy applies, Returns a * non-null [CaptureParameters] describing how the screenshot request should be augmented. * [PolicyResult.Matched] containing [CaptureParameters] used to alter the request. */ suspend fun apply(content: DisplayContentModel): CaptureParameters? suspend fun check(content: DisplayContentModel): PolicyResult /** The result of a screen capture policy check. */ sealed interface PolicyResult { /** The policy rules matched the given display content and will be applied. */ data class Matched( /** The name of the policy rule which matched. */ val policy: String, /** Why the policy matched. */ val reason: String, /** Details on how to modify the screen capture request. */ val parameters: CaptureParameters, ) : PolicyResult /** The policy rules do not match the given display content and do not apply. */ data class NotMatched( /** The name of the policy rule which matched. */ val policy: String, /** Why the policy did not match. */ val reason: String ) : PolicyResult } } packages/SystemUI/src/com/android/systemui/screenshot/policy/CaptureType.kt +2 −2 Original line number Diff line number Diff line Loading @@ -21,10 +21,10 @@ import android.graphics.Rect /** What to capture */ sealed interface CaptureType { /** Capture the entire screen contents. */ class FullScreen(val displayId: Int) : CaptureType data class FullScreen(val displayId: Int) : CaptureType /** Capture the contents of the task only. */ class IsolatedTask( data class IsolatedTask( val taskId: Int, val taskBounds: Rect?, ) : CaptureType Loading packages/SystemUI/src/com/android/systemui/screenshot/policy/PolicyRequestProcessor.kt +55 −14 Original line number Diff line number Diff line Loading @@ -16,9 +16,12 @@ package com.android.systemui.screenshot.policy import android.app.ActivityTaskManager.RootTaskInfo import android.app.WindowConfiguration import android.content.ComponentName import android.graphics.Bitmap import android.graphics.Rect import android.os.Process.myUserHandle import android.os.UserHandle import android.util.Log import android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN Loading @@ -27,7 +30,10 @@ import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.screenshot.ImageCapture import com.android.systemui.screenshot.ScreenshotData import com.android.systemui.screenshot.ScreenshotRequestProcessor import com.android.systemui.screenshot.data.model.DisplayContentModel import com.android.systemui.screenshot.data.repository.DisplayContentRepository 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.CaptureType.IsolatedTask import kotlinx.coroutines.CoroutineDispatcher Loading @@ -39,8 +45,14 @@ private const val TAG = "PolicyRequestProcessor" class PolicyRequestProcessor( @Background private val background: CoroutineDispatcher, private val capture: ImageCapture, /** Provides information about the tasks on a given display */ private val displayTasks: DisplayContentRepository, /** The list of policies to apply, in order of priority */ private val policies: List<CapturePolicy>, /** The owner to assign for screenshot when a focused task isn't visible */ private val defaultOwner: UserHandle = myUserHandle(), /** The assigned component when no application has focus, or not visible */ private val defaultComponent: ComponentName, ) : ScreenshotRequestProcessor { override suspend fun process(original: ScreenshotData): ScreenshotData { if (original.type == TAKE_SCREENSHOT_PROVIDED_IMAGE) { Loading @@ -48,29 +60,26 @@ class PolicyRequestProcessor( Log.i(TAG, "Screenshot bitmap provided. No modifications applied.") return original } val tasks = displayTasks.getDisplayContent(original.displayId) val displayContent = displayTasks.getDisplayContent(original.displayId) // If policies yield explicit modifications, apply them and return the result Log.i(TAG, "Applying policy checks....") policies .firstNotNullOfOrNull { policy -> policy.apply(tasks) } ?.let { Log.i(TAG, "Modifying screenshot: $it") return apply(it, original) policies.map { policy -> when (val result = policy.check(displayContent)) { is Matched -> { Log.i(TAG, "$result") return modify(original, result.parameters) } is NotMatched -> Log.i(TAG, "$result") } } // Otherwise capture normally, filling in additional information as needed. return replaceWithScreenshot( original = original, componentName = original.topComponent ?: tasks.rootTasks.firstOrNull()?.topActivity, owner = original.userHandle, displayId = original.displayId ) return captureScreenshot(original, displayContent) } /** Produce a new [ScreenshotData] using [CaptureParameters] */ suspend fun apply(updates: CaptureParameters, original: ScreenshotData): ScreenshotData { suspend fun modify(original: ScreenshotData, updates: CaptureParameters): ScreenshotData { // Update and apply bitmap capture depending on the parameters. val updated = when (val type = updates.type) { Loading @@ -93,6 +102,26 @@ class PolicyRequestProcessor( return updated } private suspend fun captureScreenshot( original: ScreenshotData, displayContent: DisplayContentModel, ): ScreenshotData { // The first root task on the display, excluding Picture-in-Picture val topMainRootTask = if (!displayContent.systemUiState.shadeExpanded) { displayContent.rootTasks.firstOrNull(::nonPipVisibleTask) } else { null // Otherwise attributed to SystemUI / current user } return replaceWithScreenshot( original = original, componentName = topMainRootTask?.topActivity ?: defaultComponent, owner = topMainRootTask?.userId?.let { UserHandle.of(it) } ?: defaultOwner, displayId = original.displayId ) } suspend fun replaceWithTaskSnapshot( original: ScreenshotData, componentName: ComponentName?, Loading @@ -100,6 +129,7 @@ class PolicyRequestProcessor( taskId: Int, taskBounds: Rect?, ): ScreenshotData { Log.i(TAG, "Capturing task snapshot: $componentName / $owner") val taskSnapshot = capture.captureTask(taskId) return original.copy( type = TAKE_SCREENSHOT_PROVIDED_IMAGE, Loading @@ -117,6 +147,7 @@ class PolicyRequestProcessor( owner: UserHandle?, displayId: Int, ): ScreenshotData { Log.i(TAG, "Capturing screenshot: $componentName / $owner") val screenshot = captureDisplay(displayId) return original.copy( type = TAKE_SCREENSHOT_FULLSCREEN, Loading @@ -127,6 +158,16 @@ class PolicyRequestProcessor( ) } /** Filter for the task used to attribute a full screen capture to an owner */ private fun nonPipVisibleTask(info: RootTaskInfo): Boolean { return info.windowingMode != WindowConfiguration.WINDOWING_MODE_PINNED && info.isVisible && info.isRunning && info.numActivities > 0 && info.topActivity != null && info.childTaskIds.isNotEmpty() } /** TODO: Move to ImageCapture (existing function is non-suspending) */ private suspend fun captureDisplay(displayId: Int): Bitmap? { return withContext(background) { capture.captureDisplay(displayId) } Loading packages/SystemUI/src/com/android/systemui/screenshot/policy/PrivateProfilePolicy.kt +23 −8 Original line number Diff line number Diff line Loading @@ -20,10 +20,13 @@ import android.os.UserHandle import com.android.systemui.screenshot.data.model.DisplayContentModel import com.android.systemui.screenshot.data.model.ProfileType import com.android.systemui.screenshot.data.repository.ProfileTypeRepository import com.android.systemui.screenshot.policy.CapturePolicy.PolicyResult 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 javax.inject.Inject import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.firstOrNull private const val POLICY_NAME = "PrivateProfile" /** * Condition: When any visible task belongs to a private user. Loading @@ -35,7 +38,12 @@ class PrivateProfilePolicy constructor( private val profileTypes: ProfileTypeRepository, ) : CapturePolicy { override suspend fun apply(content: DisplayContentModel): CaptureParameters? { 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 = POLICY_NAME, reason = "Notification shade is expanded") } // Find the first visible rootTaskInfo with a child task owned by a private user val (rootTask, childTask) = content.rootTasks Loading @@ -48,13 +56,20 @@ constructor( } ?.let { root to it } } ?: return null ?: return NotMatched( policy = POLICY_NAME, reason = "No private profile tasks are visible" ) // If matched, return parameters needed to modify the request. return CaptureParameters( return Matched( policy = POLICY_NAME, reason = "At least one private profile task is visible", CaptureParameters( type = FullScreen(content.displayId), component = childTask.componentName ?: rootTask.topActivity, owner = UserHandle.of(childTask.userId), ) ) } } packages/SystemUI/src/com/android/systemui/screenshot/policy/RootTaskInfoExt.kt +3 −18 Original line number Diff line number Diff line Loading @@ -18,12 +18,10 @@ package com.android.systemui.screenshot.policy import android.app.ActivityTaskManager.RootTaskInfo import com.android.systemui.screenshot.data.model.ChildTaskModel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.map internal fun RootTaskInfo.childTasksTopDown(): Flow<ChildTaskModel> { return ((numActivities - 1) downTo 0).asFlow().map { index -> /** The child tasks of A RootTaskInfo as [ChildTaskModel] in top-down (z-index ascending) order. */ internal fun RootTaskInfo.childTasksTopDown(): Sequence<ChildTaskModel> { return ((childTaskIds.size - 1) downTo 0).asSequence().map { index -> ChildTaskModel( childTaskIds[index], childTaskNames[index], Loading @@ -32,16 +30,3 @@ internal fun RootTaskInfo.childTasksTopDown(): Flow<ChildTaskModel> { ) } } internal suspend fun RootTaskInfo.firstChildTaskOrNull( filter: suspend (Int) -> Boolean ): Pair<RootTaskInfo, Int>? { // Child tasks are provided in bottom-up order // Filtering is done top-down, so iterate backwards here. for (index in numActivities - 1 downTo 0) { if (filter(index)) { return (this to index) } } return null } Loading
packages/SystemUI/src/com/android/systemui/screenshot/policy/CapturePolicy.kt +23 −2 Original line number Diff line number Diff line Loading @@ -22,7 +22,28 @@ import com.android.systemui.screenshot.data.model.DisplayContentModel fun interface CapturePolicy { /** * Test the policy against the current display task state. If the policy applies, Returns a * non-null [CaptureParameters] describing how the screenshot request should be augmented. * [PolicyResult.Matched] containing [CaptureParameters] used to alter the request. */ suspend fun apply(content: DisplayContentModel): CaptureParameters? suspend fun check(content: DisplayContentModel): PolicyResult /** The result of a screen capture policy check. */ sealed interface PolicyResult { /** The policy rules matched the given display content and will be applied. */ data class Matched( /** The name of the policy rule which matched. */ val policy: String, /** Why the policy matched. */ val reason: String, /** Details on how to modify the screen capture request. */ val parameters: CaptureParameters, ) : PolicyResult /** The policy rules do not match the given display content and do not apply. */ data class NotMatched( /** The name of the policy rule which matched. */ val policy: String, /** Why the policy did not match. */ val reason: String ) : PolicyResult } }
packages/SystemUI/src/com/android/systemui/screenshot/policy/CaptureType.kt +2 −2 Original line number Diff line number Diff line Loading @@ -21,10 +21,10 @@ import android.graphics.Rect /** What to capture */ sealed interface CaptureType { /** Capture the entire screen contents. */ class FullScreen(val displayId: Int) : CaptureType data class FullScreen(val displayId: Int) : CaptureType /** Capture the contents of the task only. */ class IsolatedTask( data class IsolatedTask( val taskId: Int, val taskBounds: Rect?, ) : CaptureType Loading
packages/SystemUI/src/com/android/systemui/screenshot/policy/PolicyRequestProcessor.kt +55 −14 Original line number Diff line number Diff line Loading @@ -16,9 +16,12 @@ package com.android.systemui.screenshot.policy import android.app.ActivityTaskManager.RootTaskInfo import android.app.WindowConfiguration import android.content.ComponentName import android.graphics.Bitmap import android.graphics.Rect import android.os.Process.myUserHandle import android.os.UserHandle import android.util.Log import android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN Loading @@ -27,7 +30,10 @@ import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.screenshot.ImageCapture import com.android.systemui.screenshot.ScreenshotData import com.android.systemui.screenshot.ScreenshotRequestProcessor import com.android.systemui.screenshot.data.model.DisplayContentModel import com.android.systemui.screenshot.data.repository.DisplayContentRepository 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.CaptureType.IsolatedTask import kotlinx.coroutines.CoroutineDispatcher Loading @@ -39,8 +45,14 @@ private const val TAG = "PolicyRequestProcessor" class PolicyRequestProcessor( @Background private val background: CoroutineDispatcher, private val capture: ImageCapture, /** Provides information about the tasks on a given display */ private val displayTasks: DisplayContentRepository, /** The list of policies to apply, in order of priority */ private val policies: List<CapturePolicy>, /** The owner to assign for screenshot when a focused task isn't visible */ private val defaultOwner: UserHandle = myUserHandle(), /** The assigned component when no application has focus, or not visible */ private val defaultComponent: ComponentName, ) : ScreenshotRequestProcessor { override suspend fun process(original: ScreenshotData): ScreenshotData { if (original.type == TAKE_SCREENSHOT_PROVIDED_IMAGE) { Loading @@ -48,29 +60,26 @@ class PolicyRequestProcessor( Log.i(TAG, "Screenshot bitmap provided. No modifications applied.") return original } val tasks = displayTasks.getDisplayContent(original.displayId) val displayContent = displayTasks.getDisplayContent(original.displayId) // If policies yield explicit modifications, apply them and return the result Log.i(TAG, "Applying policy checks....") policies .firstNotNullOfOrNull { policy -> policy.apply(tasks) } ?.let { Log.i(TAG, "Modifying screenshot: $it") return apply(it, original) policies.map { policy -> when (val result = policy.check(displayContent)) { is Matched -> { Log.i(TAG, "$result") return modify(original, result.parameters) } is NotMatched -> Log.i(TAG, "$result") } } // Otherwise capture normally, filling in additional information as needed. return replaceWithScreenshot( original = original, componentName = original.topComponent ?: tasks.rootTasks.firstOrNull()?.topActivity, owner = original.userHandle, displayId = original.displayId ) return captureScreenshot(original, displayContent) } /** Produce a new [ScreenshotData] using [CaptureParameters] */ suspend fun apply(updates: CaptureParameters, original: ScreenshotData): ScreenshotData { suspend fun modify(original: ScreenshotData, updates: CaptureParameters): ScreenshotData { // Update and apply bitmap capture depending on the parameters. val updated = when (val type = updates.type) { Loading @@ -93,6 +102,26 @@ class PolicyRequestProcessor( return updated } private suspend fun captureScreenshot( original: ScreenshotData, displayContent: DisplayContentModel, ): ScreenshotData { // The first root task on the display, excluding Picture-in-Picture val topMainRootTask = if (!displayContent.systemUiState.shadeExpanded) { displayContent.rootTasks.firstOrNull(::nonPipVisibleTask) } else { null // Otherwise attributed to SystemUI / current user } return replaceWithScreenshot( original = original, componentName = topMainRootTask?.topActivity ?: defaultComponent, owner = topMainRootTask?.userId?.let { UserHandle.of(it) } ?: defaultOwner, displayId = original.displayId ) } suspend fun replaceWithTaskSnapshot( original: ScreenshotData, componentName: ComponentName?, Loading @@ -100,6 +129,7 @@ class PolicyRequestProcessor( taskId: Int, taskBounds: Rect?, ): ScreenshotData { Log.i(TAG, "Capturing task snapshot: $componentName / $owner") val taskSnapshot = capture.captureTask(taskId) return original.copy( type = TAKE_SCREENSHOT_PROVIDED_IMAGE, Loading @@ -117,6 +147,7 @@ class PolicyRequestProcessor( owner: UserHandle?, displayId: Int, ): ScreenshotData { Log.i(TAG, "Capturing screenshot: $componentName / $owner") val screenshot = captureDisplay(displayId) return original.copy( type = TAKE_SCREENSHOT_FULLSCREEN, Loading @@ -127,6 +158,16 @@ class PolicyRequestProcessor( ) } /** Filter for the task used to attribute a full screen capture to an owner */ private fun nonPipVisibleTask(info: RootTaskInfo): Boolean { return info.windowingMode != WindowConfiguration.WINDOWING_MODE_PINNED && info.isVisible && info.isRunning && info.numActivities > 0 && info.topActivity != null && info.childTaskIds.isNotEmpty() } /** TODO: Move to ImageCapture (existing function is non-suspending) */ private suspend fun captureDisplay(displayId: Int): Bitmap? { return withContext(background) { capture.captureDisplay(displayId) } Loading
packages/SystemUI/src/com/android/systemui/screenshot/policy/PrivateProfilePolicy.kt +23 −8 Original line number Diff line number Diff line Loading @@ -20,10 +20,13 @@ import android.os.UserHandle import com.android.systemui.screenshot.data.model.DisplayContentModel import com.android.systemui.screenshot.data.model.ProfileType import com.android.systemui.screenshot.data.repository.ProfileTypeRepository import com.android.systemui.screenshot.policy.CapturePolicy.PolicyResult 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 javax.inject.Inject import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.firstOrNull private const val POLICY_NAME = "PrivateProfile" /** * Condition: When any visible task belongs to a private user. Loading @@ -35,7 +38,12 @@ class PrivateProfilePolicy constructor( private val profileTypes: ProfileTypeRepository, ) : CapturePolicy { override suspend fun apply(content: DisplayContentModel): CaptureParameters? { 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 = POLICY_NAME, reason = "Notification shade is expanded") } // Find the first visible rootTaskInfo with a child task owned by a private user val (rootTask, childTask) = content.rootTasks Loading @@ -48,13 +56,20 @@ constructor( } ?.let { root to it } } ?: return null ?: return NotMatched( policy = POLICY_NAME, reason = "No private profile tasks are visible" ) // If matched, return parameters needed to modify the request. return CaptureParameters( return Matched( policy = POLICY_NAME, reason = "At least one private profile task is visible", CaptureParameters( type = FullScreen(content.displayId), component = childTask.componentName ?: rootTask.topActivity, owner = UserHandle.of(childTask.userId), ) ) } }
packages/SystemUI/src/com/android/systemui/screenshot/policy/RootTaskInfoExt.kt +3 −18 Original line number Diff line number Diff line Loading @@ -18,12 +18,10 @@ package com.android.systemui.screenshot.policy import android.app.ActivityTaskManager.RootTaskInfo import com.android.systemui.screenshot.data.model.ChildTaskModel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.map internal fun RootTaskInfo.childTasksTopDown(): Flow<ChildTaskModel> { return ((numActivities - 1) downTo 0).asFlow().map { index -> /** The child tasks of A RootTaskInfo as [ChildTaskModel] in top-down (z-index ascending) order. */ internal fun RootTaskInfo.childTasksTopDown(): Sequence<ChildTaskModel> { return ((childTaskIds.size - 1) downTo 0).asSequence().map { index -> ChildTaskModel( childTaskIds[index], childTaskNames[index], Loading @@ -32,16 +30,3 @@ internal fun RootTaskInfo.childTasksTopDown(): Flow<ChildTaskModel> { ) } } internal suspend fun RootTaskInfo.firstChildTaskOrNull( filter: suspend (Int) -> Boolean ): Pair<RootTaskInfo, Int>? { // Child tasks are provided in bottom-up order // Filtering is done top-down, so iterate backwards here. for (index in numActivities - 1 downTo 0) { if (filter(index)) { return (this to index) } } return null }