Initial work profile screenshot policy handling
When a request is received, screen contents are inspected
to determine the most appropriate owner. If the primary
content belongs to a work profile app, the screenshot will
be isolated to the task content.
See: go/work-profile-screenshots for details.
This change also enables the foreground app package name to
be provided with uievent logging.
Additional functionality to follow.
To activate, enable the flag WORK_PROFILE_SCREENSHOT_POLICY
adb shell cmd statusbar flag 1301 on
This also requires the flag SCREENSHOTS_REQUEST_PROCESSOR
adb shell cmd statusbar flag 1300 on
Bug: 159422805
Bug: 231957192
Bug: 200294244
Test: atest RequestProcessorTest
manual; enable flags,
take screenshot of launcher
take screenshot of work profile app
observe logs and screenshot content
Change-Id: Idc74b04ddd449bfd9765ab9a456e9f9344c3aa2c
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -233,6 +233,7 @@ public class Flags {
|
||||
// 1300 - screenshots
|
||||
|
||||
public static final UnreleasedFlag SCREENSHOT_REQUEST_PROCESSOR = new UnreleasedFlag(1300);
|
||||
public static final UnreleasedFlag SCREENSHOT_WORK_PROFILE_POLICY = new UnreleasedFlag(1301);
|
||||
|
||||
// Pay no attention to the reflection behind the curtain.
|
||||
// ========================== Curtain ==========================
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -22,5 +22,5 @@ interface ImageCapture {
|
||||
|
||||
fun captureDisplay(displayId: Int, crop: Rect? = null): Bitmap?
|
||||
|
||||
fun captureTask(taskId: Int): Bitmap?
|
||||
suspend fun captureTask(taskId: Int): Bitmap?
|
||||
}
|
||||
|
||||
@@ -27,13 +27,19 @@ import android.view.SurfaceControl
|
||||
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 @@ open class ImageCaptureImpl @Inject constructor(
|
||||
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 @@ open class ImageCaptureImpl @Inject constructor(
|
||||
}
|
||||
|
||||
@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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import static android.content.Intent.ACTION_CLOSE_SYSTEM_DIALOGS;
|
||||
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 @@ public class TakeScreenshotService extends Service {
|
||||
};
|
||||
|
||||
/** 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 @@ public class TakeScreenshotService extends Service {
|
||||
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 @@ public class TakeScreenshotService extends Service {
|
||||
|
||||
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) {
|
||||
|
||||
@@ -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 android.app.Service;
|
||||
|
||||
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 @@ import dagger.multibindings.IntoMap;
|
||||
@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);
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,8 @@ import android.view.SurfaceControl.ScreenshotHardwareBuffer
|
||||
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 @@ import org.junit.runner.RunWith
|
||||
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 ImageCaptureImplTest : SysuiTestCase() {
|
||||
|
||||
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 @@ class ImageCaptureImplTest : SysuiTestCase() {
|
||||
return ScreenshotHardwareBuffer(null, null, false, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,86 +22,253 @@ import android.graphics.ColorSpace
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user