diff options
15 files changed, 733 insertions, 107 deletions
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 6edf13addbca..652e281e67e9 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 5849d6a95e68..6d83c059bed8 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.java @@ -234,6 +234,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 ========================== 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 000000000000..f7c4dadc6605 --- /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 39f35a59ff42..77797601ca5a 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 @@ interface ImageCapture { 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 258c4360922d..246265b2c202 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 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) } - } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt index 4397d3d85d62..a918e5d9e106 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!") - 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}") + // 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) + } } + + 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 000000000000..3580010cc1e8 --- /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 000000000000..ba809f676f1e --- /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 000000000000..9654e03e506e --- /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 7bf3217e5f15..a8993bc274e4 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 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) { 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 3e44258744f2..fdb01000b837 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 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); + } 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 000000000000..447e28cd9527 --- /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 000000000000..28d53c72640f --- /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 ce3f20d4d39d..00f38081c5c9 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 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) } } -}
\ 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 024d3bd8eb0e..48fbd354b98d 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.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 testProcessAsync() { + flags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, false) + + val request = ScreenshotRequest(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_CHORD) + val processor = RequestProcessor(imageCapture, policy, flags, scope) + + var result: ScreenshotRequest? = null + var callbackCount = 0 + val callback: (ScreenshotRequest) -> Unit = { processedRequest: ScreenshotRequest -> + result = processedRequest + callbackCount++ + } + + // 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 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() { + 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 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).takeScreenshotFullscreen(/* topComponent */ isNull(), - eq(onSavedListener), eq(callback)) + // Request has topComponent added, but otherwise unchanged. + assertThat(processedRequest.type).isEqualTo(TAKE_SCREENSHOT_FULLSCREEN) + assertThat(processedRequest.topComponent).isEqualTo(component) } @Test - fun testSelectedRegionScreenshot() { + 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 processor = RequestProcessor(imageCapture, policy, flags, scope) + + val processedRequest = processor.process(request) + + // 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 onSavedListener = mock<Consumer<Uri>>() - val callback = mock<RequestCallback>() - val processor = RequestProcessor(controller) + val processor = RequestProcessor(imageCapture, policy, flags, scope) - processor.processRequest(request, onSavedListener, callback) + policy.setManagedProfile(USER_ID, false) + policy.setDisplayContentInfo(policy.getDefaultDisplayId(), + DisplayContentInfo(component, bounds, USER_ID, TASK_ID)) - verify(controller).takeScreenshotPartial(/* topComponent */ isNull(), - eq(onSavedListener), eq(callback)) + 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 testSelectedRegionScreenshot_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 + + 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() { - val taskId = 1111 - val userId = 2222 + fun testProvidedImageScreenshot_workProfilePolicyDisabled() = runBlocking { + flags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, false) + val bounds = Rect(50, 50, 150, 150) - val topComponent = ComponentName("test", "test") - val processor = RequestProcessor(controller) + val processor = RequestProcessor(imageCapture, policy, flags, scope) - 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))!! + 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) + // No changes + assertThat(processedRequest).isEqualTo(request) + } + + @Test + fun testProvidedImageScreenshot() = runBlocking { + flags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, true) - verify(controller).handleImageAsScreenshot( - bitmapCaptor.capture(), eq(bounds), eq(Insets.NONE), eq(taskId), eq(userId), - eq(topComponent), eq(onSavedListener), eq(callback) - ) + 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) - assertThat(bitmapCaptor.value.equalsHardwareBitmap(bitmap)).isTrue() + 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.equalsHardwareBitmap(bitmap: Bitmap): Boolean { - return bitmap.hardwareBuffer == this.hardwareBuffer && - bitmap.colorSpace == this.colorSpace + private fun Bitmap.equalsHardwareBitmapBundle(bundle: Bundle): Boolean { + val provided = bundleToHardwareBitmap(bundle) + return provided.hardwareBuffer == this.hardwareBuffer && + provided.colorSpace == this.colorSpace } } |