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