1 /*
<lambda>null2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.screenshot.policy
18 
19 import android.app.ActivityTaskManager.RootTaskInfo
20 import android.app.WindowConfiguration
21 import android.content.ComponentName
22 import android.graphics.Bitmap
23 import android.graphics.Rect
24 import android.os.Process.myUserHandle
25 import android.os.UserHandle
26 import android.util.Log
27 import android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN
28 import android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE
29 import com.android.systemui.dagger.qualifiers.Background
30 import com.android.systemui.screenshot.ImageCapture
31 import com.android.systemui.screenshot.ScreenshotData
32 import com.android.systemui.screenshot.ScreenshotRequestProcessor
33 import com.android.systemui.screenshot.data.model.DisplayContentModel
34 import com.android.systemui.screenshot.data.repository.DisplayContentRepository
35 import com.android.systemui.screenshot.policy.CapturePolicy.PolicyResult.Matched
36 import com.android.systemui.screenshot.policy.CapturePolicy.PolicyResult.NotMatched
37 import com.android.systemui.screenshot.policy.CaptureType.FullScreen
38 import com.android.systemui.screenshot.policy.CaptureType.IsolatedTask
39 import kotlinx.coroutines.CoroutineDispatcher
40 import kotlinx.coroutines.withContext
41 
42 private const val TAG = "PolicyRequestProcessor"
43 
44 /** A [ScreenshotRequestProcessor] which supports general policy rule matching. */
45 class PolicyRequestProcessor(
46     @Background private val background: CoroutineDispatcher,
47     private val capture: ImageCapture,
48     /** Provides information about the tasks on a given display */
49     private val displayTasks: DisplayContentRepository,
50     /** The list of policies to apply, in order of priority */
51     private val policies: List<CapturePolicy>,
52     /** The owner to assign for screenshot when a focused task isn't visible */
53     private val defaultOwner: UserHandle = myUserHandle(),
54     /** The assigned component when no application has focus, or not visible */
55     private val defaultComponent: ComponentName,
56 ) : ScreenshotRequestProcessor {
57     override suspend fun process(original: ScreenshotData): ScreenshotData {
58         if (original.type == TAKE_SCREENSHOT_PROVIDED_IMAGE) {
59             // The request contains an already captured screenshot, accept it as is.
60             Log.i(TAG, "Screenshot bitmap provided. No modifications applied.")
61             return original
62         }
63         val displayContent = displayTasks.getDisplayContent(original.displayId)
64 
65         // If policies yield explicit modifications, apply them and return the result
66         Log.i(TAG, "Applying policy checks....")
67         policies.map { policy ->
68             when (val result = policy.check(displayContent)) {
69                 is Matched -> {
70                     Log.i(TAG, "$result")
71                     return modify(original, result.parameters)
72                 }
73                 is NotMatched -> Log.i(TAG, "$result")
74             }
75         }
76 
77         // Otherwise capture normally, filling in additional information as needed.
78         return captureScreenshot(original, displayContent)
79     }
80 
81     /** Produce a new [ScreenshotData] using [CaptureParameters] */
82     private suspend fun modify(
83         original: ScreenshotData,
84         updates: CaptureParameters,
85     ): ScreenshotData {
86         // Update and apply bitmap capture depending on the parameters.
87         val updated =
88             when (val type = updates.type) {
89                 is IsolatedTask ->
90                     replaceWithTaskSnapshot(
91                         original,
92                         updates.component,
93                         updates.owner,
94                         type.taskId,
95                         type.taskBounds
96                     )
97                 is FullScreen ->
98                     replaceWithScreenshot(
99                         original,
100                         updates.component,
101                         updates.owner,
102                         type.displayId,
103                     )
104             }
105         return updated
106     }
107 
108     private suspend fun captureScreenshot(
109         original: ScreenshotData,
110         displayContent: DisplayContentModel,
111     ): ScreenshotData {
112         // The first root task on the display, excluding Picture-in-Picture
113         val topMainRootTask =
114             if (!displayContent.systemUiState.shadeExpanded) {
115                 displayContent.rootTasks.firstOrNull(::nonPipVisibleTask)
116             } else {
117                 null // Otherwise attributed to SystemUI / current user
118             }
119 
120         return replaceWithScreenshot(
121             original = original,
122             componentName = topMainRootTask?.topActivity ?: defaultComponent,
123             taskId = topMainRootTask?.taskId,
124             owner = defaultOwner,
125             displayId = original.displayId
126         )
127     }
128 
129     suspend fun replaceWithTaskSnapshot(
130         original: ScreenshotData,
131         componentName: ComponentName?,
132         owner: UserHandle,
133         taskId: Int,
134         taskBounds: Rect?,
135     ): ScreenshotData {
136         Log.i(TAG, "Capturing task snapshot: $componentName / $owner")
137         val taskSnapshot = capture.captureTask(taskId)
138         return original.copy(
139             type = TAKE_SCREENSHOT_PROVIDED_IMAGE,
140             bitmap = taskSnapshot,
141             userHandle = owner,
142             taskId = taskId,
143             topComponent = componentName,
144             screenBounds = taskBounds
145         )
146     }
147 
148     private suspend fun replaceWithScreenshot(
149         original: ScreenshotData,
150         componentName: ComponentName?,
151         owner: UserHandle?,
152         displayId: Int,
153         taskId: Int? = null,
154     ): ScreenshotData {
155         Log.i(TAG, "Capturing screenshot: $componentName / $owner")
156         val screenshot = captureDisplay(displayId)
157         return original.copy(
158             type = TAKE_SCREENSHOT_FULLSCREEN,
159             bitmap = screenshot,
160             userHandle = owner,
161             topComponent = componentName,
162             screenBounds = Rect(0, 0, screenshot?.width ?: 0, screenshot?.height ?: 0),
163             taskId = taskId ?: -1,
164         )
165     }
166 
167     /** Filter for the task used to attribute a full screen capture to an owner */
168     private fun nonPipVisibleTask(info: RootTaskInfo): Boolean {
169         return info.windowingMode != WindowConfiguration.WINDOWING_MODE_PINNED &&
170             info.isVisible &&
171             info.isRunning &&
172             info.numActivities > 0 &&
173             info.topActivity != null &&
174             info.childTaskIds.isNotEmpty()
175     }
176 
177     /** TODO: Move to ImageCapture (existing function is non-suspending) */
178     private suspend fun captureDisplay(displayId: Int): Bitmap? {
179         return withContext(background) { capture.captureDisplay(displayId) }
180     }
181 }
182