Merge "Initial work profile screenshot policy handling" into tm-qpr-dev am: 8dfa3f09b8 am: 47d70ae41c
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/19432252
Change-Id: I806af3705591be6cfe2f4f176edbaa6128c5868e
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 6edf13a..652e281 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -394,6 +394,11 @@
android:label="@string/screenshot_scroll_label"
android:finishOnTaskLaunch="true" />
+ <service android:name=".screenshot.ScreenshotProxyService"
+ android:permission="com.android.systemui.permission.SELF"
+ android:exported="false" />
+
+
<service android:name=".screenrecord.RecordingService" />
<receiver android:name=".SysuiRestartReceiver"
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
index 4b409f5..1f356cb 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
@@ -237,6 +237,7 @@
// 1300 - screenshots
public static final UnreleasedFlag SCREENSHOT_REQUEST_PROCESSOR = new UnreleasedFlag(1300);
+ public static final UnreleasedFlag SCREENSHOT_WORK_PROFILE_POLICY = new UnreleasedFlag(1301);
// 1400 - columbus, b/242800729
public static final UnreleasedFlag QUICK_TAP_IN_PCC = new UnreleasedFlag(1400);
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/IScreenshotProxy.aidl b/packages/SystemUI/src/com/android/systemui/screenshot/IScreenshotProxy.aidl
new file mode 100644
index 0000000..f7c4dad
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/IScreenshotProxy.aidl
@@ -0,0 +1,24 @@
+/**
+ * Copyright (c) 2009, 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;
+
+/** Interface implemented by ScreenshotProxyService */
+interface IScreenshotProxy {
+
+ /** Is the notification shade currently exanded? */
+ boolean isNotificationShadeExpanded();
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ImageCapture.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ImageCapture.kt
index 39f35a5..7779760 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ImageCapture.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ImageCapture.kt
@@ -22,5 +22,5 @@
fun captureDisplay(displayId: Int, crop: Rect? = null): Bitmap?
- fun captureTask(taskId: Int): Bitmap?
+ suspend fun captureTask(taskId: Int): Bitmap?
}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ImageCaptureImpl.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ImageCaptureImpl.kt
index 258c436..246265b 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ImageCaptureImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ImageCaptureImpl.kt
@@ -27,13 +27,19 @@
import android.view.SurfaceControl.DisplayCaptureArgs
import android.view.SurfaceControl.ScreenshotHardwareBuffer
import androidx.annotation.VisibleForTesting
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
private const val TAG = "ImageCaptureImpl"
+@SysUISingleton
open class ImageCaptureImpl @Inject constructor(
private val displayManager: DisplayManager,
- private val atmService: IActivityTaskManager
+ private val atmService: IActivityTaskManager,
+ @Background private val bgContext: CoroutineDispatcher
) : ImageCapture {
override fun captureDisplay(displayId: Int, crop: Rect?): Bitmap? {
@@ -46,8 +52,8 @@
return buffer?.asBitmap()
}
- override fun captureTask(taskId: Int): Bitmap? {
- val snapshot = atmService.takeTaskSnapshot(taskId)
+ override suspend fun captureTask(taskId: Int): Bitmap? {
+ val snapshot = withContext(bgContext) { atmService.takeTaskSnapshot(taskId) } ?: return null
return Bitmap.wrapHardwareBuffer(snapshot.hardwareBuffer, snapshot.colorSpace)
}
@@ -67,12 +73,17 @@
}
@VisibleForTesting
- open fun captureDisplay(displayToken: IBinder, width: Int, height: Int, crop: Rect): ScreenshotHardwareBuffer? {
- val captureArgs = DisplayCaptureArgs.Builder(displayToken)
- .setSize(width, height)
- .setSourceCrop(crop)
- .build()
+ open fun captureDisplay(
+ displayToken: IBinder,
+ width: Int,
+ height: Int,
+ crop: Rect
+ ): ScreenshotHardwareBuffer? {
+ val captureArgs =
+ DisplayCaptureArgs.Builder(displayToken)
+ .setSize(width, height)
+ .setSourceCrop(crop)
+ .build()
return SurfaceControl.captureDisplay(captureArgs)
}
-
}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt
index 4397d3d..a918e5d 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt
@@ -16,51 +16,84 @@
package com.android.systemui.screenshot
-import android.net.Uri
-import android.util.Log
-import android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN
+import android.graphics.Insets
import android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE
-import android.view.WindowManager.TAKE_SCREENSHOT_SELECTED_REGION
import com.android.internal.util.ScreenshotHelper.HardwareBitmapBundler
import com.android.internal.util.ScreenshotHelper.ScreenshotRequest
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.screenshot.TakeScreenshotService.RequestCallback
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags.SCREENSHOT_WORK_PROFILE_POLICY
import java.util.function.Consumer
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
/**
* Processes a screenshot request sent from {@link ScreenshotHelper}.
*/
@SysUISingleton
-internal class RequestProcessor @Inject constructor(
- private val controller: ScreenshotController,
+class RequestProcessor @Inject constructor(
+ private val capture: ImageCapture,
+ private val policy: ScreenshotPolicy,
+ private val flags: FeatureFlags,
+ /** For the Java Async version, to invoke the callback. */
+ @Application private val mainScope: CoroutineScope
) {
- fun processRequest(
- request: ScreenshotRequest,
- onSavedListener: Consumer<Uri>,
- callback: RequestCallback
- ) {
+ /**
+ * Inspects the incoming request, returning a potentially modified request depending on policy.
+ *
+ * @param request the request to process
+ */
+ suspend fun process(request: ScreenshotRequest): ScreenshotRequest {
+ var result = request
- if (request.type == TAKE_SCREENSHOT_PROVIDED_IMAGE) {
- val image = HardwareBitmapBundler.bundleToHardwareBitmap(request.bitmapBundle)
+ // Apply work profile screenshots policy:
+ //
+ // If the focused app belongs to a work profile, transforms a full screen
+ // (or partial) screenshot request to a task snapshot (provided image) screenshot.
- controller.handleImageAsScreenshot(
- image, request.boundsInScreen, request.insets,
- request.taskId, request.userId, request.topComponent, onSavedListener, callback
- )
- return
+ // Whenever displayContentInfo is fetched, the topComponent is also populated
+ // regardless of the managed profile status.
+
+ if (request.type != TAKE_SCREENSHOT_PROVIDED_IMAGE &&
+ flags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)
+ ) {
+
+ val info = policy.findPrimaryContent(policy.getDefaultDisplayId())
+
+ result = if (policy.isManagedProfile(info.userId)) {
+ val image = capture.captureTask(info.taskId)
+ ?: error("Task snapshot returned a null Bitmap!")
+
+ // Provide the task snapshot as the screenshot
+ ScreenshotRequest(
+ TAKE_SCREENSHOT_PROVIDED_IMAGE, request.source,
+ HardwareBitmapBundler.hardwareBitmapToBundle(image),
+ info.bounds, Insets.NONE, info.taskId, info.userId, info.component
+ )
+ } else {
+ // Create a new request of the same type which includes the top component
+ ScreenshotRequest(request.source, request.type, info.component)
+ }
}
- when (request.type) {
- TAKE_SCREENSHOT_FULLSCREEN ->
- controller.takeScreenshotFullscreen(null, onSavedListener, callback)
- TAKE_SCREENSHOT_SELECTED_REGION ->
- controller.takeScreenshotPartial(null, onSavedListener, callback)
- else -> Log.w(TAG, "Invalid screenshot option: ${request.type}")
- }
+ return result
}
- companion object {
- const val TAG: String = "RequestProcessor"
+ /**
+ * Note: This is for compatibility with existing Java. Prefer the suspending function when
+ * calling from a Coroutine context.
+ *
+ * @param request the request to process
+ * @param callback the callback to provide the processed request, invoked from the main thread
+ */
+ fun processAsync(request: ScreenshotRequest, callback: Consumer<ScreenshotRequest>) {
+ mainScope.launch {
+ val result = process(request)
+ callback.accept(result)
+ }
}
}
+
+private const val TAG = "RequestProcessor"
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicy.kt
new file mode 100644
index 0000000..3580010
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicy.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2022 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
+
+import android.annotation.UserIdInt
+import android.content.ComponentName
+import android.graphics.Rect
+import android.view.Display
+
+/**
+ * Provides policy decision-making information to screenshot request handling.
+ */
+interface ScreenshotPolicy {
+
+ /** @return true if the user is a managed profile (a.k.a. work profile) */
+ suspend fun isManagedProfile(@UserIdInt userId: Int): Boolean
+
+ /**
+ * Requests information about the owner of display content which occupies a majority of the
+ * screenshot and/or has most recently been interacted with at the time the screenshot was
+ * requested.
+ *
+ * @param displayId the id of the display to inspect
+ * @return content info for the primary content on the display
+ */
+ suspend fun findPrimaryContent(displayId: Int): DisplayContentInfo
+
+ data class DisplayContentInfo(
+ val component: ComponentName,
+ val bounds: Rect,
+ @UserIdInt val userId: Int,
+ val taskId: Int,
+ )
+
+ fun getDefaultDisplayId(): Int = Display.DEFAULT_DISPLAY
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicyImpl.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicyImpl.kt
new file mode 100644
index 0000000..ba809f6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicyImpl.kt
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2022 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
+
+import android.annotation.UserIdInt
+import android.app.ActivityTaskManager
+import android.app.ActivityTaskManager.RootTaskInfo
+import android.app.IActivityTaskManager
+import android.app.WindowConfiguration
+import android.app.WindowConfiguration.activityTypeToString
+import android.app.WindowConfiguration.windowingModeToString
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.graphics.Rect
+import android.os.Process
+import android.os.RemoteException
+import android.os.UserManager
+import android.util.Log
+import android.view.Display.DEFAULT_DISPLAY
+import com.android.internal.infra.ServiceConnector
+import com.android.systemui.SystemUIService
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.screenshot.ScreenshotPolicy.DisplayContentInfo
+import java.util.Arrays
+import javax.inject.Inject
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
+
+@SysUISingleton
+internal class ScreenshotPolicyImpl @Inject constructor(
+ context: Context,
+ private val userMgr: UserManager,
+ private val atmService: IActivityTaskManager,
+ @Background val bgDispatcher: CoroutineDispatcher,
+) : ScreenshotPolicy {
+
+ private val systemUiContent =
+ DisplayContentInfo(
+ ComponentName(context, SystemUIService::class.java),
+ Rect(),
+ ActivityTaskManager.INVALID_TASK_ID,
+ Process.myUserHandle().identifier,
+ )
+
+ private val proxyConnector: ServiceConnector<IScreenshotProxy> =
+ ServiceConnector.Impl(
+ context,
+ Intent(context, ScreenshotProxyService::class.java),
+ Context.BIND_AUTO_CREATE or Context.BIND_WAIVE_PRIORITY or Context.BIND_NOT_VISIBLE,
+ context.userId,
+ IScreenshotProxy.Stub::asInterface
+ )
+
+ override fun getDefaultDisplayId(): Int {
+ return DEFAULT_DISPLAY
+ }
+
+ override suspend fun isManagedProfile(@UserIdInt userId: Int): Boolean {
+ return withContext(bgDispatcher) { userMgr.isManagedProfile(userId) }
+ }
+
+ 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()
+ }
+
+ /**
+ * Uses RootTaskInfo from ActivityTaskManager to guess at the primary focused task within a
+ * display. If no task is visible or the top task is covered by a system window, the info
+ * reported will reference a SystemUI component instead.
+ */
+ override suspend fun findPrimaryContent(displayId: Int): DisplayContentInfo {
+ // Determine if the notification shade is expanded. If so, task windows are not
+ // visible behind it, so the screenshot should instead be associated with SystemUI.
+ if (isNotificationShadeExpanded()) {
+ return systemUiContent
+ }
+
+ val taskInfoList = getAllRootTaskInfosOnDisplay(displayId)
+ if (DEBUG) {
+ debugLogRootTaskInfos(taskInfoList)
+ }
+
+ // If no visible task is located, then report SystemUI as the foreground content
+ val target = taskInfoList.firstOrNull(::nonPipVisibleTask) ?: return systemUiContent
+
+ val topActivity: ComponentName = target.topActivity ?: error("should not be null")
+ val topChildTask = target.childTaskIds.size - 1
+ val childTaskId = target.childTaskIds[topChildTask]
+ val childTaskUserId = target.childTaskUserIds[topChildTask]
+ val childTaskBounds = target.childTaskBounds[topChildTask]
+
+ return DisplayContentInfo(topActivity, childTaskBounds, childTaskId, childTaskUserId)
+ }
+
+ private fun debugLogRootTaskInfos(taskInfoList: List<RootTaskInfo>) {
+ for (info in taskInfoList) {
+ Log.d(
+ TAG,
+ "[root task info] " +
+ "taskId=${info.taskId} " +
+ "parentTaskId=${info.parentTaskId} " +
+ "position=${info.position} " +
+ "positionInParent=${info.positionInParent} " +
+ "isVisible=${info.isVisible()} " +
+ "visible=${info.visible} " +
+ "isFocused=${info.isFocused} " +
+ "isSleeping=${info.isSleeping} " +
+ "isRunning=${info.isRunning} " +
+ "windowMode=${windowingModeToString(info.windowingMode)} " +
+ "activityType=${activityTypeToString(info.activityType)} " +
+ "topActivity=${info.topActivity} " +
+ "topActivityInfo=${info.topActivityInfo} " +
+ "numActivities=${info.numActivities} " +
+ "childTaskIds=${Arrays.toString(info.childTaskIds)} " +
+ "childUserIds=${Arrays.toString(info.childTaskUserIds)} " +
+ "childTaskBounds=${Arrays.toString(info.childTaskBounds)} " +
+ "childTaskNames=${Arrays.toString(info.childTaskNames)}"
+ )
+
+ for (j in 0 until info.childTaskIds.size) {
+ Log.d(TAG, " *** [$j] ******")
+ Log.d(TAG, " *** childTaskIds[$j]: ${info.childTaskIds[j]}")
+ Log.d(TAG, " *** childTaskUserIds[$j]: ${info.childTaskUserIds[j]}")
+ Log.d(TAG, " *** childTaskBounds[$j]: ${info.childTaskBounds[j]}")
+ Log.d(TAG, " *** childTaskNames[$j]: ${info.childTaskNames[j]}")
+ }
+ }
+ }
+
+ private suspend fun getAllRootTaskInfosOnDisplay(displayId: Int): List<RootTaskInfo> =
+ withContext(bgDispatcher) {
+ try {
+ atmService.getAllRootTaskInfosOnDisplay(displayId)
+ } catch (e: RemoteException) {
+ Log.e(TAG, "getAllRootTaskInfosOnDisplay", e)
+ listOf()
+ }
+ }
+
+ private suspend fun isNotificationShadeExpanded(): Boolean = suspendCoroutine { k ->
+ proxyConnector
+ .postForResult { it.isNotificationShadeExpanded }
+ .whenComplete { expanded, error ->
+ if (error != null) {
+ Log.e(TAG, "isNotificationShadeExpanded", error)
+ }
+ k.resume(expanded ?: false)
+ }
+ }
+
+ companion object {
+ const val TAG: String = "ScreenshotPolicyImpl"
+ const val DEBUG: Boolean = false
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotProxyService.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotProxyService.kt
new file mode 100644
index 0000000..9654e03
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotProxyService.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2022 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
+
+import android.app.Service
+import android.content.Intent
+import android.os.IBinder
+import android.util.Log
+import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager
+import javax.inject.Inject
+
+/**
+ * Provides state from the main SystemUI process on behalf of the Screenshot process.
+ */
+internal class ScreenshotProxyService @Inject constructor(
+ private val mExpansionMgr: PanelExpansionStateManager
+) : Service() {
+
+ private val mBinder: IBinder = object : IScreenshotProxy.Stub() {
+ /**
+ * @return true when the notification shade is partially or fully expanded.
+ */
+ override fun isNotificationShadeExpanded(): Boolean {
+ val expanded = !mExpansionMgr.isClosed()
+ Log.d(TAG, "isNotificationShadeExpanded(): $expanded")
+ return expanded
+ }
+ }
+
+ override fun onBind(intent: Intent): IBinder? {
+ Log.d(TAG, "onBind: $intent")
+ return mBinder
+ }
+
+ companion object {
+ const val TAG = "ScreenshotProxyService"
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java
index 7bf3217..a8993bc 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java
@@ -22,6 +22,7 @@
import static com.android.internal.util.ScreenshotHelper.SCREENSHOT_MSG_PROCESS_COMPLETE;
import static com.android.internal.util.ScreenshotHelper.SCREENSHOT_MSG_URI;
import static com.android.systemui.flags.Flags.SCREENSHOT_REQUEST_PROCESSOR;
+import static com.android.systemui.flags.Flags.SCREENSHOT_WORK_PROFILE_POLICY;
import static com.android.systemui.screenshot.LogConfig.DEBUG_CALLBACK;
import static com.android.systemui.screenshot.LogConfig.DEBUG_DISMISS;
import static com.android.systemui.screenshot.LogConfig.DEBUG_SERVICE;
@@ -97,7 +98,7 @@
};
/** Informs about coarse grained state of the Controller. */
- interface RequestCallback {
+ public interface RequestCallback {
/** Respond to the current request indicating the screenshot request failed. */
void reportError();
@@ -124,6 +125,7 @@
mBgExecutor = bgExecutor;
mFeatureFlags = featureFlags;
mFeatureFlags.addListener(SCREENSHOT_REQUEST_PROCESSOR, FlagEvent::requestNoRestart);
+ mFeatureFlags.addListener(SCREENSHOT_WORK_PROFILE_POLICY, FlagEvent::requestNoRestart);
mProcessor = processor;
}
@@ -229,49 +231,57 @@
if (mFeatureFlags.isEnabled(SCREENSHOT_REQUEST_PROCESSOR)) {
Log.d(TAG, "handleMessage: Using request processor");
- mProcessor.processRequest(screenshotRequest, uriConsumer, requestCallback);
+ mProcessor.processAsync(screenshotRequest,
+ (request) -> dispatchToController(request, uriConsumer, requestCallback));
return true;
}
- switch (screenshotRequest.getType()) {
+ dispatchToController(screenshotRequest, uriConsumer, requestCallback);
+ return true;
+ }
+
+ private void dispatchToController(ScreenshotHelper.ScreenshotRequest request,
+ Consumer<Uri> uriConsumer, RequestCallback callback) {
+
+ ComponentName topComponent = request.getTopComponent();
+
+ switch (request.getType()) {
case WindowManager.TAKE_SCREENSHOT_FULLSCREEN:
if (DEBUG_SERVICE) {
Log.d(TAG, "handleMessage: TAKE_SCREENSHOT_FULLSCREEN");
}
- mScreenshot.takeScreenshotFullscreen(topComponent, uriConsumer, requestCallback);
+ mScreenshot.takeScreenshotFullscreen(topComponent, uriConsumer, callback);
break;
case WindowManager.TAKE_SCREENSHOT_SELECTED_REGION:
if (DEBUG_SERVICE) {
Log.d(TAG, "handleMessage: TAKE_SCREENSHOT_SELECTED_REGION");
}
- mScreenshot.takeScreenshotPartial(topComponent, uriConsumer, requestCallback);
+ mScreenshot.takeScreenshotPartial(topComponent, uriConsumer, callback);
break;
case WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE:
if (DEBUG_SERVICE) {
Log.d(TAG, "handleMessage: TAKE_SCREENSHOT_PROVIDED_IMAGE");
}
Bitmap screenshot = ScreenshotHelper.HardwareBitmapBundler.bundleToHardwareBitmap(
- screenshotRequest.getBitmapBundle());
- Rect screenBounds = screenshotRequest.getBoundsInScreen();
- Insets insets = screenshotRequest.getInsets();
- int taskId = screenshotRequest.getTaskId();
- int userId = screenshotRequest.getUserId();
+ request.getBitmapBundle());
+ Rect screenBounds = request.getBoundsInScreen();
+ Insets insets = request.getInsets();
+ int taskId = request.getTaskId();
+ int userId = request.getUserId();
if (screenshot == null) {
Log.e(TAG, "Got null bitmap from screenshot message");
mNotificationsController.notifyScreenshotError(
R.string.screenshot_failed_to_capture_text);
- requestCallback.reportError();
+ callback.reportError();
} else {
mScreenshot.handleImageAsScreenshot(screenshot, screenBounds, insets,
- taskId, userId, topComponent, uriConsumer, requestCallback);
+ taskId, userId, topComponent, uriConsumer, callback);
}
break;
default:
- Log.w(TAG, "Invalid screenshot option: " + msg.what);
- return false;
+ Log.w(TAG, "Invalid screenshot option: " + request.getType());
}
- return true;
}
private static void sendComplete(Messenger target) {
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java b/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java
index 3e442587..fdb0100 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 The Android Open Source Project
+ * Copyright (C) 2022 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.
@@ -20,6 +20,9 @@
import com.android.systemui.screenshot.ImageCapture;
import com.android.systemui.screenshot.ImageCaptureImpl;
+import com.android.systemui.screenshot.ScreenshotPolicy;
+import com.android.systemui.screenshot.ScreenshotPolicyImpl;
+import com.android.systemui.screenshot.ScreenshotProxyService;
import com.android.systemui.screenshot.TakeScreenshotService;
import dagger.Binds;
@@ -33,12 +36,20 @@
@Module
public abstract class ScreenshotModule {
- /** */
@Binds
@IntoMap
@ClassKey(TakeScreenshotService.class)
- public abstract Service bindTakeScreenshotService(TakeScreenshotService service);
+ abstract Service bindTakeScreenshotService(TakeScreenshotService service);
@Binds
- public abstract ImageCapture bindImageCapture(ImageCaptureImpl capture);
+ @IntoMap
+ @ClassKey(ScreenshotProxyService.class)
+ abstract Service bindScreenshotProxyService(ScreenshotProxyService service);
+
+ @Binds
+ abstract ScreenshotPolicy bindScreenshotPolicyImpl(ScreenshotPolicyImpl impl);
+
+ @Binds
+ abstract ImageCapture bindImageCaptureImpl(ImageCaptureImpl capture);
+
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/FakeImageCapture.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/FakeImageCapture.kt
new file mode 100644
index 0000000..447e28c
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/FakeImageCapture.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2022 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
+
+import android.graphics.Bitmap
+import android.graphics.Rect
+
+internal class FakeImageCapture : ImageCapture {
+
+ var requestedDisplayId: Int? = null
+ var requestedDisplayCrop: Rect? = null
+ var requestedTaskId: Int? = null
+
+ var image: Bitmap? = null
+
+ override fun captureDisplay(displayId: Int, crop: Rect?): Bitmap? {
+ requestedDisplayId = displayId
+ requestedDisplayCrop = crop
+ return image
+ }
+
+ override suspend fun captureTask(taskId: Int): Bitmap? {
+ requestedTaskId = taskId
+ return image
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/FakeScreenshotPolicy.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/FakeScreenshotPolicy.kt
new file mode 100644
index 0000000..28d53c7
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/FakeScreenshotPolicy.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2022 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
+
+import com.android.systemui.screenshot.ScreenshotPolicy.DisplayContentInfo
+
+internal class FakeScreenshotPolicy : ScreenshotPolicy {
+
+ private val userTypes = mutableMapOf<Int, Boolean>()
+ private val contentInfo = mutableMapOf<Int, DisplayContentInfo?>()
+
+ fun setManagedProfile(userId: Int, managedUser: Boolean) {
+ userTypes[userId] = managedUser
+ }
+ override suspend fun isManagedProfile(userId: Int): Boolean {
+ return userTypes[userId] ?: error("No managedProfile value set for userId $userId")
+ }
+
+ fun setDisplayContentInfo(userId: Int, contentInfo: DisplayContentInfo) {
+ this.contentInfo[userId] = contentInfo
+ }
+
+ override suspend fun findPrimaryContent(displayId: Int): DisplayContentInfo {
+ return contentInfo[displayId] ?: error("No DisplayContentInfo set for displayId $displayId")
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ImageCaptureImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ImageCaptureImplTest.kt
index ce3f20d..00f3808 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ImageCaptureImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ImageCaptureImplTest.kt
@@ -27,6 +27,8 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.util.mockito.mock
import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Dispatchers
import org.junit.Test
import org.junit.runner.RunWith
@@ -37,7 +39,10 @@
class ImageCaptureImplTest : SysuiTestCase() {
private val displayManager = mock<DisplayManager>()
private val atmService = mock<IActivityTaskManager>()
- private val capture = TestableImageCaptureImpl(displayManager, atmService)
+ private val capture = TestableImageCaptureImpl(
+ displayManager,
+ atmService,
+ Dispatchers.Unconfined)
@Test
fun captureDisplayWithCrop() {
@@ -59,9 +64,10 @@
class TestableImageCaptureImpl(
displayManager: DisplayManager,
- atmService: IActivityTaskManager
+ atmService: IActivityTaskManager,
+ bgDispatcher: CoroutineDispatcher
) :
- ImageCaptureImpl(displayManager, atmService) {
+ ImageCaptureImpl(displayManager, atmService, bgDispatcher) {
var token: IBinder? = null
var width: Int? = null
@@ -81,4 +87,4 @@
return ScreenshotHardwareBuffer(null, null, false, false)
}
}
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt
index 024d3bd..48fbd35 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt
@@ -22,86 +22,253 @@
import android.graphics.Insets
import android.graphics.Rect
import android.hardware.HardwareBuffer
-import android.net.Uri
+import android.os.Bundle
import android.view.WindowManager.ScreenshotSource.SCREENSHOT_KEY_CHORD
import android.view.WindowManager.ScreenshotSource.SCREENSHOT_OTHER
import android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN
-import android.view.WindowManager.TAKE_SCREENSHOT_SELECTED_REGION
import android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE
-
+import android.view.WindowManager.TAKE_SCREENSHOT_SELECTED_REGION
import com.android.internal.util.ScreenshotHelper.HardwareBitmapBundler
+import com.android.internal.util.ScreenshotHelper.HardwareBitmapBundler.bundleToHardwareBitmap
import com.android.internal.util.ScreenshotHelper.ScreenshotRequest
-import com.android.systemui.screenshot.TakeScreenshotService.RequestCallback
-import com.android.systemui.util.mockito.argumentCaptor
-import com.android.systemui.util.mockito.mock
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.screenshot.ScreenshotPolicy.DisplayContentInfo
import com.google.common.truth.Truth.assertThat
-import java.util.function.Consumer
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
import org.junit.Test
-import org.mockito.Mockito.eq
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.isNull
+
+private const val USER_ID = 1
+private const val TASK_ID = 1
class RequestProcessorTest {
- private val controller = mock<ScreenshotController>()
- private val bitmapCaptor = argumentCaptor<Bitmap>()
+ private val imageCapture = FakeImageCapture()
+ private val component = ComponentName("android.test", "android.test.Component")
+ private val bounds = Rect(25, 25, 75, 75)
+ private val scope = CoroutineScope(Dispatchers.Unconfined)
+ private val dispatcher = Dispatchers.Unconfined
+ private val policy = FakeScreenshotPolicy()
+ private val flags = FakeFeatureFlags()
+
+ /** Tests the Java-compatible function wrapper, ensures callback is invoked. */
@Test
- fun testFullScreenshot() {
+ fun testProcessAsync() {
+ flags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, false)
+
val request = ScreenshotRequest(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_CHORD)
- val onSavedListener = mock<Consumer<Uri>>()
- val callback = mock<RequestCallback>()
- val processor = RequestProcessor(controller)
+ val processor = RequestProcessor(imageCapture, policy, flags, scope)
- processor.processRequest(request, onSavedListener, callback)
+ var result: ScreenshotRequest? = null
+ var callbackCount = 0
+ val callback: (ScreenshotRequest) -> Unit = { processedRequest: ScreenshotRequest ->
+ result = processedRequest
+ callbackCount++
+ }
- verify(controller).takeScreenshotFullscreen(/* topComponent */ isNull(),
- eq(onSavedListener), eq(callback))
+ // runs synchronously, using Unconfined Dispatcher
+ processor.processAsync(request, callback)
+
+ // Callback invoked once returning the same request (no changes)
+ assertThat(callbackCount).isEqualTo(1)
+ assertThat(result).isEqualTo(request)
}
@Test
- fun testSelectedRegionScreenshot() {
+ fun testFullScreenshot_workProfilePolicyDisabled() = runBlocking {
+ flags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, false)
+
+ val request = ScreenshotRequest(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_CHORD)
+ val processor = RequestProcessor(imageCapture, policy, flags, scope)
+
+ val processedRequest = processor.process(request)
+
+ // No changes
+ assertThat(processedRequest).isEqualTo(request)
+ }
+
+ @Test
+ fun testFullScreenshot() = runBlocking {
+ flags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, true)
+
+ // Indicate that the primary content belongs to a normal user
+ policy.setManagedProfile(USER_ID, false)
+ policy.setDisplayContentInfo(
+ policy.getDefaultDisplayId(),
+ DisplayContentInfo(component, bounds, USER_ID, TASK_ID))
+
+ val request = ScreenshotRequest(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_CHORD)
+ val processor = RequestProcessor(imageCapture, policy, flags, scope)
+
+ val processedRequest = processor.process(request)
+
+ // Request has topComponent added, but otherwise unchanged.
+ assertThat(processedRequest.type).isEqualTo(TAKE_SCREENSHOT_FULLSCREEN)
+ assertThat(processedRequest.topComponent).isEqualTo(component)
+ }
+
+ @Test
+ fun testFullScreenshot_managedProfile() = runBlocking {
+ flags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, true)
+
+ // Provide a fake task bitmap when asked
+ val bitmap = makeHardwareBitmap(100, 100)
+ imageCapture.image = bitmap
+
+ // Indicate that the primary content belongs to a manged profile
+ policy.setManagedProfile(USER_ID, true)
+ policy.setDisplayContentInfo(policy.getDefaultDisplayId(),
+ DisplayContentInfo(component, bounds, USER_ID, TASK_ID))
+
+ val request = ScreenshotRequest(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_CHORD)
+ val processor = RequestProcessor(imageCapture, policy, flags, scope)
+
+ val processedRequest = processor.process(request)
+
+ // Expect a task snapshot is taken, overriding the full screen mode
+ assertThat(processedRequest.type).isEqualTo(TAKE_SCREENSHOT_PROVIDED_IMAGE)
+ assertThat(bitmap.equalsHardwareBitmapBundle(processedRequest.bitmapBundle)).isTrue()
+ assertThat(processedRequest.boundsInScreen).isEqualTo(bounds)
+ assertThat(processedRequest.insets).isEqualTo(Insets.NONE)
+ assertThat(processedRequest.taskId).isEqualTo(TASK_ID)
+ assertThat(imageCapture.requestedTaskId).isEqualTo(TASK_ID)
+ assertThat(processedRequest.userId).isEqualTo(USER_ID)
+ assertThat(processedRequest.topComponent).isEqualTo(component)
+ }
+
+ @Test
+ fun testSelectedRegionScreenshot_workProfilePolicyDisabled() = runBlocking {
+ flags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, false)
+
val request = ScreenshotRequest(TAKE_SCREENSHOT_SELECTED_REGION, SCREENSHOT_KEY_CHORD)
- val onSavedListener = mock<Consumer<Uri>>()
- val callback = mock<RequestCallback>()
- val processor = RequestProcessor(controller)
+ val processor = RequestProcessor(imageCapture, policy, flags, scope)
- processor.processRequest(request, onSavedListener, callback)
+ val processedRequest = processor.process(request)
- verify(controller).takeScreenshotPartial(/* topComponent */ isNull(),
- eq(onSavedListener), eq(callback))
+ // No changes
+ assertThat(processedRequest).isEqualTo(request)
+ }
+
+ @Test
+ fun testSelectedRegionScreenshot() = runBlocking {
+ flags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, true)
+
+ val request = ScreenshotRequest(TAKE_SCREENSHOT_SELECTED_REGION, SCREENSHOT_KEY_CHORD)
+ val processor = RequestProcessor(imageCapture, policy, flags, scope)
+
+ policy.setManagedProfile(USER_ID, false)
+ policy.setDisplayContentInfo(policy.getDefaultDisplayId(),
+ DisplayContentInfo(component, bounds, USER_ID, TASK_ID))
+
+ val processedRequest = processor.process(request)
+
+ // Request has topComponent added, but otherwise unchanged.
+ assertThat(processedRequest.type).isEqualTo(TAKE_SCREENSHOT_FULLSCREEN)
+ assertThat(processedRequest.topComponent).isEqualTo(component)
}
@Test
- fun testProvidedImageScreenshot() {
- val taskId = 1111
- val userId = 2222
- val bounds = Rect(50, 50, 150, 150)
- val topComponent = ComponentName("test", "test")
- val processor = RequestProcessor(controller)
+ fun testSelectedRegionScreenshot_managedProfile() = runBlocking {
+ flags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, true)
- val buffer = HardwareBuffer.create(100, 100, HardwareBuffer.RGBA_8888, 1,
- HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE)
- val bitmap = Bitmap.wrapHardwareBuffer(buffer, ColorSpace.get(ColorSpace.Named.SRGB))!!
+ // Provide a fake task bitmap when asked
+ val bitmap = makeHardwareBitmap(100, 100)
+ imageCapture.image = bitmap
+
+ val request = ScreenshotRequest(TAKE_SCREENSHOT_SELECTED_REGION, SCREENSHOT_KEY_CHORD)
+ val processor = RequestProcessor(imageCapture, policy, flags, scope)
+
+ // Indicate that the primary content belongs to a manged profile
+ policy.setManagedProfile(USER_ID, true)
+ policy.setDisplayContentInfo(policy.getDefaultDisplayId(),
+ DisplayContentInfo(component, bounds, USER_ID, TASK_ID))
+
+ val processedRequest = processor.process(request)
+
+ // Expect a task snapshot is taken, overriding the selected region mode
+ assertThat(processedRequest.type).isEqualTo(TAKE_SCREENSHOT_PROVIDED_IMAGE)
+ assertThat(bitmap.equalsHardwareBitmapBundle(processedRequest.bitmapBundle)).isTrue()
+ assertThat(processedRequest.boundsInScreen).isEqualTo(bounds)
+ assertThat(processedRequest.insets).isEqualTo(Insets.NONE)
+ assertThat(processedRequest.taskId).isEqualTo(TASK_ID)
+ assertThat(imageCapture.requestedTaskId).isEqualTo(TASK_ID)
+ assertThat(processedRequest.userId).isEqualTo(USER_ID)
+ assertThat(processedRequest.topComponent).isEqualTo(component)
+ }
+
+ @Test
+ fun testProvidedImageScreenshot_workProfilePolicyDisabled() = runBlocking {
+ flags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, false)
+
+ val bounds = Rect(50, 50, 150, 150)
+ val processor = RequestProcessor(imageCapture, policy, flags, scope)
+
+ val bitmap = makeHardwareBitmap(100, 100)
val bitmapBundle = HardwareBitmapBundler.hardwareBitmapToBundle(bitmap)
val request = ScreenshotRequest(TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_OTHER,
- bitmapBundle, bounds, Insets.NONE, taskId, userId, topComponent)
+ bitmapBundle, bounds, Insets.NONE, TASK_ID, USER_ID, component)
- val onSavedListener = mock<Consumer<Uri>>()
- val callback = mock<RequestCallback>()
+ val processedRequest = processor.process(request)
- processor.processRequest(request, onSavedListener, callback)
-
- verify(controller).handleImageAsScreenshot(
- bitmapCaptor.capture(), eq(bounds), eq(Insets.NONE), eq(taskId), eq(userId),
- eq(topComponent), eq(onSavedListener), eq(callback)
- )
-
- assertThat(bitmapCaptor.value.equalsHardwareBitmap(bitmap)).isTrue()
+ // No changes
+ assertThat(processedRequest).isEqualTo(request)
}
- private fun Bitmap.equalsHardwareBitmap(bitmap: Bitmap): Boolean {
- return bitmap.hardwareBuffer == this.hardwareBuffer &&
- bitmap.colorSpace == this.colorSpace
+ @Test
+ fun testProvidedImageScreenshot() = runBlocking {
+ flags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, true)
+
+ val bounds = Rect(50, 50, 150, 150)
+ val processor = RequestProcessor(imageCapture, policy, flags, scope)
+
+ policy.setManagedProfile(USER_ID, false)
+
+ val bitmap = makeHardwareBitmap(100, 100)
+ val bitmapBundle = HardwareBitmapBundler.hardwareBitmapToBundle(bitmap)
+
+ val request = ScreenshotRequest(TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_OTHER,
+ bitmapBundle, bounds, Insets.NONE, TASK_ID, USER_ID, component)
+
+ val processedRequest = processor.process(request)
+
+ // No changes
+ assertThat(processedRequest).isEqualTo(request)
+ }
+
+ @Test
+ fun testProvidedImageScreenshot_managedProfile() = runBlocking {
+ flags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, true)
+
+ val bounds = Rect(50, 50, 150, 150)
+ val processor = RequestProcessor(imageCapture, policy, flags, scope)
+
+ // Indicate that the screenshot belongs to a manged profile
+ policy.setManagedProfile(USER_ID, true)
+
+ val bitmap = makeHardwareBitmap(100, 100)
+ val bitmapBundle = HardwareBitmapBundler.hardwareBitmapToBundle(bitmap)
+
+ val request = ScreenshotRequest(TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_OTHER,
+ bitmapBundle, bounds, Insets.NONE, TASK_ID, USER_ID, component)
+
+ val processedRequest = processor.process(request)
+
+ // Work profile, but already a task snapshot, so no changes
+ assertThat(processedRequest).isEqualTo(request)
+ }
+
+ private fun makeHardwareBitmap(width: Int, height: Int): Bitmap {
+ val buffer = HardwareBuffer.create(width, height, HardwareBuffer.RGBA_8888, 1,
+ HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE)
+ return Bitmap.wrapHardwareBuffer(buffer, ColorSpace.get(ColorSpace.Named.SRGB))!!
+ }
+
+ private fun Bitmap.equalsHardwareBitmapBundle(bundle: Bundle): Boolean {
+ val provided = bundleToHardwareBitmap(bundle)
+ return provided.hardwareBuffer == this.hardwareBuffer &&
+ provided.colorSpace == this.colorSpace
}
}