diff options
13 files changed, 467 insertions, 18 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotRequestProcessor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotRequestProcessor.kt index 796457d88cc2..3ad4075a2b89 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotRequestProcessor.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotRequestProcessor.kt @@ -17,13 +17,14 @@ package com.android.systemui.screenshot /** Processes a screenshot request sent from [ScreenshotHelper]. */ -interface ScreenshotRequestProcessor { +fun interface ScreenshotRequestProcessor { /** * Inspects the incoming ScreenshotData, potentially modifying it based upon policy. * - * @param screenshot the screenshot to process + * @param original the screenshot to process + * @return a potentially modified screenshot data */ - suspend fun process(screenshot: ScreenshotData): ScreenshotData + suspend fun process(original: ScreenshotData): ScreenshotData } /** Exception thrown by [RequestProcessor] if something goes wrong. */ diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt index 92d3e550dc0f..ec7707c83980 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt @@ -92,14 +92,14 @@ constructor( // Let's wait before logging "screenshot requested", as we should log the processed // ScreenshotData. val screenshotData = - try { - screenshotRequestProcessor.process(rawScreenshotData) - } catch (e: RequestProcessorException) { - Log.e(TAG, "Failed to process screenshot request!", e) - logScreenshotRequested(rawScreenshotData) - onFailedScreenshotRequest(rawScreenshotData, callback) - return - } + runCatching { screenshotRequestProcessor.process(rawScreenshotData) } + .onFailure { + Log.e(TAG, "Failed to process screenshot request!", it) + logScreenshotRequested(rawScreenshotData) + onFailedScreenshotRequest(rawScreenshotData, callback) + } + .getOrNull() + ?: return logScreenshotRequested(screenshotData) Log.d(TAG, "Screenshot request: $screenshotData") diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/data/model/ChildTaskModel.kt b/packages/SystemUI/src/com/android/systemui/screenshot/data/model/ChildTaskModel.kt new file mode 100644 index 000000000000..c380db0ca3a4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/data/model/ChildTaskModel.kt @@ -0,0 +1,35 @@ +/* + * 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.data.model + +import android.content.ComponentName +import android.graphics.Rect + +/** A child task within a RootTaskInfo */ +data class ChildTaskModel( + /** The task identifier */ + val id: Int, + /** The task name */ + val name: String, + /** The location and size of the task */ + val bounds: Rect, + /** The user which created the task. */ + val userId: Int, +) { + val componentName: ComponentName? + get() = ComponentName.unflattenFromString(name) +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/data/model/DisplayContentModel.kt b/packages/SystemUI/src/com/android/systemui/screenshot/data/model/DisplayContentModel.kt index 837a661230cb..2048b7c0c142 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/data/model/DisplayContentModel.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/data/model/DisplayContentModel.kt @@ -24,6 +24,6 @@ data class DisplayContentModel( val displayId: Int, /** Information about the current System UI state which can affect capture. */ val systemUiState: SystemUiState, - /** A list of root tasks on the display, ordered from bottom to top along the z-axis */ + /** A list of root tasks on the display, ordered from top to bottom along the z-axis */ val rootTasks: List<RootTaskInfo>, ) diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/data/repository/DisplayContentRepository.kt b/packages/SystemUI/src/com/android/systemui/screenshot/data/repository/DisplayContentRepository.kt index 9c81b322a2b7..48e813d89af7 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/data/repository/DisplayContentRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/data/repository/DisplayContentRepository.kt @@ -18,7 +18,7 @@ package com.android.systemui.screenshot.data.repository import com.android.systemui.screenshot.data.model.DisplayContentModel /** Provides information about tasks related to a display. */ -interface DisplayContentRepository { +fun interface DisplayContentRepository { /** Provides information about the tasks and content presented on a given display. */ suspend fun getDisplayContent(displayId: Int): DisplayContentModel } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/policy/CaptureParameters.kt b/packages/SystemUI/src/com/android/systemui/screenshot/policy/CaptureParameters.kt new file mode 100644 index 000000000000..5e2b57651de7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/policy/CaptureParameters.kt @@ -0,0 +1,30 @@ +/* + * 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 + +/** The parameters dictated by a [CapturePolicy], used to adjust alter screenshot request. */ +data class CaptureParameters( + /** How should the content be captured? */ + val type: CaptureType, + /** The focused or top component at the time of the screenshot. */ + val component: ComponentName?, + /** Which user should receive the screenshot file? */ + val owner: UserHandle, +) diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/policy/CapturePolicy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/policy/CapturePolicy.kt new file mode 100644 index 000000000000..4a88180d8f73 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/policy/CapturePolicy.kt @@ -0,0 +1,28 @@ +/* + * 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 com.android.systemui.screenshot.data.model.DisplayContentModel + +/** Contains logic to determine when and how an adjust to screenshot behavior applies. */ +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. + */ + suspend fun apply(content: DisplayContentModel): CaptureParameters? +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/policy/CaptureType.kt b/packages/SystemUI/src/com/android/systemui/screenshot/policy/CaptureType.kt new file mode 100644 index 000000000000..6ca2e9d6d5e0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/policy/CaptureType.kt @@ -0,0 +1,31 @@ +/* + * 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.graphics.Rect + +/** What to capture */ +sealed interface CaptureType { + /** Capture the entire screen contents. */ + class FullScreen(val displayId: Int) : CaptureType + + /** Capture the contents of the task only. */ + class IsolatedTask( + val taskId: Int, + val taskBounds: Rect?, + ) : CaptureType +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/policy/PolicyRequestProcessor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/policy/PolicyRequestProcessor.kt new file mode 100644 index 000000000000..2c0a0dbf8ea9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/policy/PolicyRequestProcessor.kt @@ -0,0 +1,134 @@ +/* + * 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.graphics.Bitmap +import android.graphics.Rect +import android.os.UserHandle +import android.util.Log +import android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN +import android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE +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.repository.DisplayContentRepository +import com.android.systemui.screenshot.policy.CaptureType.FullScreen +import com.android.systemui.screenshot.policy.CaptureType.IsolatedTask +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.withContext + +private const val TAG = "PolicyRequestProcessor" + +/** A [ScreenshotRequestProcessor] which supports general policy rule matching. */ +class PolicyRequestProcessor( + @Background private val background: CoroutineDispatcher, + private val capture: ImageCapture, + private val displayTasks: DisplayContentRepository, + private val policies: List<CapturePolicy>, +) : ScreenshotRequestProcessor { + override suspend fun process(original: ScreenshotData): ScreenshotData { + if (original.type == TAKE_SCREENSHOT_PROVIDED_IMAGE) { + // The request contains an already captured screenshot, accept it as is. + Log.i(TAG, "Screenshot bitmap provided. No modifications applied.") + return original + } + + val tasks = 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) + } + + // 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 + ) + } + + /** Produce a new [ScreenshotData] using [CaptureParameters] */ + suspend fun apply(updates: CaptureParameters, original: ScreenshotData): ScreenshotData { + // Update and apply bitmap capture depending on the parameters. + val updated = + when (val type = updates.type) { + is IsolatedTask -> + replaceWithTaskSnapshot( + original, + updates.component, + updates.owner, + type.taskId, + type.taskBounds + ) + is FullScreen -> + replaceWithScreenshot( + original, + updates.component, + updates.owner, + type.displayId + ) + } + return updated + } + + suspend fun replaceWithTaskSnapshot( + original: ScreenshotData, + componentName: ComponentName?, + owner: UserHandle, + taskId: Int, + taskBounds: Rect?, + ): ScreenshotData { + val taskSnapshot = capture.captureTask(taskId) + return original.copy( + type = TAKE_SCREENSHOT_PROVIDED_IMAGE, + bitmap = taskSnapshot, + userHandle = owner, + taskId = taskId, + topComponent = componentName, + screenBounds = taskBounds + ) + } + + suspend fun replaceWithScreenshot( + original: ScreenshotData, + componentName: ComponentName?, + owner: UserHandle?, + displayId: Int, + ): ScreenshotData { + val screenshot = captureDisplay(displayId) + return original.copy( + type = TAKE_SCREENSHOT_FULLSCREEN, + bitmap = screenshot, + userHandle = owner, + topComponent = componentName, + screenBounds = Rect(0, 0, screenshot?.width ?: 0, screenshot?.height ?: 0) + ) + } + + /** TODO: Move to ImageCapture (existing function is non-suspending) */ + private suspend fun captureDisplay(displayId: Int): Bitmap? { + return withContext(background) { capture.captureDisplay(displayId) } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/policy/PrivateProfilePolicy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/policy/PrivateProfilePolicy.kt new file mode 100644 index 000000000000..221e64782894 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/policy/PrivateProfilePolicy.kt @@ -0,0 +1,60 @@ +/* + * 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.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.CaptureType.FullScreen +import javax.inject.Inject +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.firstOrNull + +/** + * Condition: When any visible task belongs to a private user. + * + * Parameters: Capture the whole screen, owned by the private user. + */ +class PrivateProfilePolicy +@Inject +constructor( + private val profileTypes: ProfileTypeRepository, +) : CapturePolicy { + override suspend fun apply(content: DisplayContentModel): CaptureParameters? { + // Find the first visible rootTaskInfo with a child task owned by a private user + val (rootTask, childTask) = + content.rootTasks + .filter { it.isVisible } + .firstNotNullOfOrNull { root -> + root + .childTasksTopDown() + .firstOrNull { + profileTypes.getProfileType(it.userId) == ProfileType.PRIVATE + } + ?.let { root to it } + } + ?: return null + + // If matched, return parameters needed to modify the request. + return CaptureParameters( + type = FullScreen(content.displayId), + component = childTask.componentName ?: rootTask.topActivity, + owner = UserHandle.of(childTask.userId), + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/policy/RootTaskInfoExt.kt b/packages/SystemUI/src/com/android/systemui/screenshot/policy/RootTaskInfoExt.kt new file mode 100644 index 000000000000..d2f4d9e039f6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/policy/RootTaskInfoExt.kt @@ -0,0 +1,47 @@ +/* + * 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.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 -> + ChildTaskModel( + childTaskIds[index], + childTaskNames[index], + childTaskBounds[index], + childTaskUserIds[index] + ) + } +} + +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 +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/policy/ScreenshotPolicyModule.kt b/packages/SystemUI/src/com/android/systemui/screenshot/policy/ScreenshotPolicyModule.kt index bc71ab71b626..63d15087d12c 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/policy/ScreenshotPolicyModule.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/policy/ScreenshotPolicyModule.kt @@ -16,7 +16,9 @@ package com.android.systemui.screenshot.policy +import com.android.systemui.Flags.screenshotPrivateProfile import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.screenshot.ImageCapture import com.android.systemui.screenshot.RequestProcessor import com.android.systemui.screenshot.ScreenshotPolicy @@ -29,6 +31,7 @@ import dagger.Binds import dagger.Module import dagger.Provides import javax.inject.Provider +import kotlinx.coroutines.CoroutineDispatcher @Module interface ScreenshotPolicyModule { @@ -37,18 +40,42 @@ interface ScreenshotPolicyModule { @SysUISingleton fun bindProfileTypeRepository(impl: ProfileTypeRepositoryImpl): ProfileTypeRepository + @Binds + @SysUISingleton + fun bindDisplayContentRepository(impl: DisplayContentRepositoryImpl): DisplayContentRepository + companion object { + @JvmStatic + @Provides + @SysUISingleton + fun bindCapturePolicyList( + privateProfilePolicy: PrivateProfilePolicy, + workProfilePolicy: WorkProfilePolicy, + ): List<CapturePolicy> { + // In order of priority. The first matching policy applies. + return listOf(workProfilePolicy, privateProfilePolicy) + } + + @JvmStatic @Provides @SysUISingleton fun bindScreenshotRequestProcessor( + @Background background: CoroutineDispatcher, imageCapture: ImageCapture, policyProvider: Provider<ScreenshotPolicy>, + displayContentRepoProvider: Provider<DisplayContentRepository>, + policyListProvider: Provider<List<CapturePolicy>>, ): ScreenshotRequestProcessor { - return RequestProcessor(imageCapture, policyProvider.get()) + return if (screenshotPrivateProfile()) { + PolicyRequestProcessor( + background, + imageCapture, + displayContentRepoProvider.get(), + policyListProvider.get() + ) + } else { + RequestProcessor(imageCapture, policyProvider.get()) + } } } - - @Binds - @SysUISingleton - fun bindDisplayContentRepository(impl: DisplayContentRepositoryImpl): DisplayContentRepository } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/policy/WorkProfilePolicy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/policy/WorkProfilePolicy.kt new file mode 100644 index 000000000000..d6b5d6dfda25 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/policy/WorkProfilePolicy.kt @@ -0,0 +1,56 @@ +/* + * 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.app.WindowConfiguration.WINDOWING_MODE_PINNED +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.CaptureType.IsolatedTask +import javax.inject.Inject +import kotlinx.coroutines.flow.first + +/** + * Condition: When the top visible task (excluding PIP mode) belongs to a work user. + * + * Parameters: Capture only the foreground task, owned by the work user. + */ +class WorkProfilePolicy +@Inject +constructor( + private val profileTypes: ProfileTypeRepository, +) : CapturePolicy { + override suspend fun apply(content: DisplayContentModel): CaptureParameters? { + // 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 } + .map { it to it.childTasksTopDown().first() } + .firstOrNull { (_, child) -> + profileTypes.getProfileType(child.userId) == ProfileType.WORK + } + ?: return null + + // If matched, return parameters needed to modify the request. + return CaptureParameters( + type = IsolatedTask(taskId = childTask.id, taskBounds = childTask.bounds), + component = childTask.componentName ?: rootTask.topActivity, + owner = UserHandle.of(childTask.userId), + ) + } +} |