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

Commit 1d279790 authored by Mark Renouf's avatar Mark Renouf
Browse files

Complete PolicyRequestProcessor implementation

* Capture all screenshots regardless of policy match
* Apply attribution to all screenshots (top component)
* Pass default component (systemUI) into the processor
* Rename policy.apply to check, return PolicyResult
* Replace use of numActivities, vs. childTaskIds.size (bug)
* Better report on what is happening for triage/bug report

Bug: 327613051
Bug: 330329080
Test: TODO
Flag: ACONFIG com.android.systemui.screenshot_private_profile DISABLED
Change-Id: I7c7bd296bb71d9dc40df70758e21527436c2983d
parent 9f62abfc
Loading
Loading
Loading
Loading
+23 −2
Original line number Diff line number Diff line
@@ -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
    }
}
+2 −2
Original line number Diff line number Diff line
@@ -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
+55 −14
Original line number Diff line number Diff line
@@ -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
@@ -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
@@ -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) {
@@ -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) {
@@ -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?,
@@ -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,
@@ -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,
@@ -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) }
+23 −8
Original line number Diff line number Diff line
@@ -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.
@@ -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
@@ -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),
            )
        )
    }
}
+3 −18
Original line number Diff line number Diff line
@@ -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],
@@ -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