diff options
6 files changed, 692 insertions, 434 deletions
diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index 3d505315fc95..df2b2a395ea7 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -295,6 +295,12 @@ "group": "WM_DEBUG_STARTING_WINDOW", "at": "com\/android\/server\/wm\/ActivityRecord.java" }, + "-1781861035": { + "message": "Display %d has content (%b) so pause recording", + "level": "VERBOSE", + "group": "WM_DEBUG_CONTENT_RECORDING", + "at": "com\/android\/server\/wm\/ContentRecorder.java" + }, "-1777196134": { "message": "goodToGo(): No apps to animate, mPendingAnimations=%d", "level": "DEBUG", @@ -343,12 +349,6 @@ "group": "WM_DEBUG_STATES", "at": "com\/android\/server\/wm\/ActivityTaskSupervisor.java" }, - "-1734445525": { - "message": "Display %d has content (%b) so pause recording", - "level": "VERBOSE", - "group": "WM_DEBUG_CONTENT_RECORDING", - "at": "com\/android\/server\/wm\/DisplayContent.java" - }, "-1730156332": { "message": "Display id=%d rotation changed to %d from %d, lastOrientation=%d", "level": "VERBOSE", @@ -445,6 +445,12 @@ "group": "WM_DEBUG_LOCKTASK", "at": "com\/android\/server\/wm\/LockTaskController.java" }, + "-1605829532": { + "message": "Unable to start recording due to null token for display %d", + "level": "VERBOSE", + "group": "WM_DEBUG_CONTENT_RECORDING", + "at": "com\/android\/server\/wm\/ContentRecorder.java" + }, "-1598452494": { "message": "activityDestroyedLocked: r=%s", "level": "DEBUG", @@ -511,12 +517,6 @@ "group": "WM_ERROR", "at": "com\/android\/server\/wm\/WindowManagerService.java" }, - "-1542296596": { - "message": "Going ahead with updating recording for display %d to new bounds %s and\/or orientation %d.", - "level": "VERBOSE", - "group": "WM_DEBUG_CONTENT_RECORDING", - "at": "com\/android\/server\/wm\/DisplayContent.java" - }, "-1539974875": { "message": "removeAppToken: %s delayed=%b Callers=%s", "level": "VERBOSE", @@ -715,6 +715,12 @@ "group": "WM_DEBUG_TASKS", "at": "com\/android\/server\/wm\/RootWindowContainer.java" }, + "-1373875178": { + "message": "Going ahead with updating recording for display %d to new bounds %s and\/or orientation %d.", + "level": "VERBOSE", + "group": "WM_DEBUG_CONTENT_RECORDING", + "at": "com\/android\/server\/wm\/ContentRecorder.java" + }, "-1364754753": { "message": "Task vanished taskId=%d", "level": "VERBOSE", @@ -739,11 +745,11 @@ "group": "WM_DEBUG_STARTING_WINDOW", "at": "com\/android\/server\/wm\/ActivityRecord.java" }, - "-1323003327": { - "message": "Unexpectedly null window container; unable to update recording for display %d", + "-1326876381": { + "message": "Provided surface for recording on display %d is not present, so do not update the surface", "level": "VERBOSE", "group": "WM_DEBUG_CONTENT_RECORDING", - "at": "com\/android\/server\/wm\/DisplayContent.java" + "at": "com\/android\/server\/wm\/ContentRecorder.java" }, "-1311436264": { "message": "Unregister task fragment organizer=%s uid=%d pid=%d", @@ -859,12 +865,6 @@ "group": "WM_DEBUG_WINDOW_INSETS", "at": "com\/android\/server\/wm\/InsetsSourceProvider.java" }, - "-1179559337": { - "message": "Unable to start recording due to invalid region for display %d", - "level": "VERBOSE", - "group": "WM_DEBUG_CONTENT_RECORDING", - "at": "com\/android\/server\/wm\/DisplayContent.java" - }, "-1176488860": { "message": "SURFACE isSecure=%b: %s", "level": "INFO", @@ -1045,12 +1045,6 @@ "group": "WM_DEBUG_DRAW", "at": "com\/android\/server\/wm\/WindowStateAnimator.java" }, - "-992111757": { - "message": "Unable to retrieve window container to start recording for display %d", - "level": "VERBOSE", - "group": "WM_DEBUG_CONTENT_RECORDING", - "at": "com\/android\/server\/wm\/DisplayContent.java" - }, "-986746907": { "message": "Starting window removed %s", "level": "DEBUG", @@ -1111,12 +1105,6 @@ "group": "WM_DEBUG_STATES", "at": "com\/android\/server\/wm\/ActivityRecord.java" }, - "-922507769": { - "message": "Display %d has no content and is on, so start recording for state %d", - "level": "VERBOSE", - "group": "WM_DEBUG_CONTENT_RECORDING", - "at": "com\/android\/server\/wm\/DisplayContent.java" - }, "-917215012": { "message": "%s: caller %d is using old GET_TASKS but privileged; allowing", "level": "WARN", @@ -1291,6 +1279,12 @@ "group": "WM_DEBUG_SCREEN_ON", "at": "com\/android\/server\/wm\/WindowManagerService.java" }, + "-751255162": { + "message": "Unable to update recording for display %d to new bounds %s and\/or orientation %d, since the surface is not available.", + "level": "VERBOSE", + "group": "WM_DEBUG_CONTENT_RECORDING", + "at": "com\/android\/server\/wm\/ContentRecorder.java" + }, "-743856570": { "message": "shouldWaitAnimatingExit: isAnimating: %s", "level": "DEBUG", @@ -1303,6 +1297,12 @@ "group": "WM_DEBUG_CONFIGURATION", "at": "com\/android\/server\/wm\/ActivityRecord.java" }, + "-732715767": { + "message": "Unable to retrieve window container to start recording for display %d", + "level": "VERBOSE", + "group": "WM_DEBUG_CONTENT_RECORDING", + "at": "com\/android\/server\/wm\/ContentRecorder.java" + }, "-729530161": { "message": "Moving to DESTROYED: %s (no app)", "level": "VERBOSE", @@ -1549,18 +1549,6 @@ "group": "WM_DEBUG_KEEP_SCREEN_ON", "at": "com\/android\/server\/wm\/RootWindowContainer.java" }, - "-479990726": { - "message": "Unable to start recording due to null token for display %d", - "level": "VERBOSE", - "group": "WM_DEBUG_CONTENT_RECORDING", - "at": "com\/android\/server\/wm\/DisplayContent.java" - }, - "-473911359": { - "message": "Display %d was already recording, so apply transformations if necessary", - "level": "VERBOSE", - "group": "WM_DEBUG_CONTENT_RECORDING", - "at": "com\/android\/server\/wm\/DisplayContent.java" - }, "-463348344": { "message": "Removing and adding activity %s to root task at top callers=%s", "level": "INFO", @@ -1669,12 +1657,6 @@ "group": "WM_DEBUG_TASKS", "at": "com\/android\/server\/wm\/RootWindowContainer.java" }, - "-370641936": { - "message": "Unable to update recording for display %d to new bounds %s and\/or orientation %d, since the surface is not available.", - "level": "VERBOSE", - "group": "WM_DEBUG_CONTENT_RECORDING", - "at": "com\/android\/server\/wm\/DisplayContent.java" - }, "-360208282": { "message": "Animating wallpapers: old: %s hidden=%b new: %s hidden=%b", "level": "VERBOSE", @@ -1747,6 +1729,12 @@ "group": "WM_DEBUG_RECENTS_ANIMATIONS", "at": "com\/android\/server\/wm\/RecentsAnimation.java" }, + "-302468137": { + "message": "Display %d was already recording, so apply transformations if necessary", + "level": "VERBOSE", + "group": "WM_DEBUG_CONTENT_RECORDING", + "at": "com\/android\/server\/wm\/ContentRecorder.java" + }, "-292790591": { "message": "Attempted to set IME policy to a display that does not exist: %d", "level": "WARN", @@ -1873,6 +1861,12 @@ "group": "WM_DEBUG_STATES", "at": "com\/android\/server\/wm\/Task.java" }, + "-142844021": { + "message": "Unable to start recording for display %d since the surface is not available.", + "level": "VERBOSE", + "group": "WM_DEBUG_CONTENT_RECORDING", + "at": "com\/android\/server\/wm\/ContentRecorder.java" + }, "-134091882": { "message": "Screenshotting Activity %s", "level": "VERBOSE", @@ -2605,6 +2599,12 @@ "group": "WM_SHOW_TRANSACTIONS", "at": "com\/android\/server\/wm\/Session.java" }, + "609880497": { + "message": "Display %d has no content and is on, so start recording for state %d", + "level": "VERBOSE", + "group": "WM_DEBUG_CONTENT_RECORDING", + "at": "com\/android\/server\/wm\/ContentRecorder.java" + }, "620368427": { "message": "******* TELLING SURFACE FLINGER WE ARE BOOTED!", "level": "INFO", @@ -3319,6 +3319,12 @@ "group": "WM_ERROR", "at": "com\/android\/server\/wm\/WindowManagerService.java" }, + "1444064727": { + "message": "Unexpectedly null window container; unable to update recording for display %d", + "level": "VERBOSE", + "group": "WM_DEBUG_CONTENT_RECORDING", + "at": "com\/android\/server\/wm\/ContentRecorder.java" + }, "1448683958": { "message": "Override pending remote transitionSet=%b adapter=%s", "level": "INFO", @@ -3445,6 +3451,12 @@ "group": "WM_DEBUG_APP_TRANSITIONS_ANIM", "at": "com\/android\/server\/wm\/AppTransition.java" }, + "1608402305": { + "message": "Unable to start recording due to invalid region for display %d", + "level": "VERBOSE", + "group": "WM_DEBUG_CONTENT_RECORDING", + "at": "com\/android\/server\/wm\/ContentRecorder.java" + }, "1610646518": { "message": "Enqueueing pending finish: %s", "level": "VERBOSE", @@ -3547,12 +3559,6 @@ "group": "WM_DEBUG_WINDOW_ORGANIZER", "at": "com\/android\/server\/wm\/DisplayAreaOrganizerController.java" }, - "1707558369": { - "message": "Unable to start recording for display %d since the surface is not available.", - "level": "VERBOSE", - "group": "WM_DEBUG_CONTENT_RECORDING", - "at": "com\/android\/server\/wm\/DisplayContent.java" - }, "1720229827": { "message": "Creating animation bounds layer", "level": "INFO", @@ -3685,12 +3691,6 @@ "group": "WM_DEBUG_STARTING_WINDOW", "at": "com\/android\/server\/wm\/ActivityRecord.java" }, - "1854279309": { - "message": "Provided surface for recording on display %d is not present, so do not update the surface", - "level": "VERBOSE", - "group": "WM_DEBUG_CONTENT_RECORDING", - "at": "com\/android\/server\/wm\/DisplayContent.java" - }, "1856211951": { "message": "moveFocusableActivityToTop: already on top, activity=%s", "level": "DEBUG", diff --git a/services/core/java/com/android/server/wm/ContentRecorder.java b/services/core/java/com/android/server/wm/ContentRecorder.java new file mode 100644 index 000000000000..07a0c372778b --- /dev/null +++ b/services/core/java/com/android/server/wm/ContentRecorder.java @@ -0,0 +1,368 @@ +/* + * 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.server.wm; + +import static android.view.ContentRecordingSession.RECORD_CONTENT_DISPLAY; + +import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_CONTENT_RECORDING; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.res.Configuration; +import android.graphics.Point; +import android.graphics.Rect; +import android.os.IBinder; +import android.view.ContentRecordingSession; +import android.view.Display; +import android.view.SurfaceControl; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.protolog.common.ProtoLog; + +/** + * Manages content recording for a particular {@link DisplayContent}. + */ +final class ContentRecorder { + + /** + * The display content this class is handling recording for. + */ + @NonNull + private final DisplayContent mDisplayContent; + + /** + * The session for content recording, or null if this DisplayContent is not being used for + * recording. + */ + @VisibleForTesting private ContentRecordingSession mContentRecordingSession = null; + + /** + * The WindowContainer for the level of the hierarchy to record. + */ + @Nullable private WindowContainer mRecordedWindowContainer = null; + + /** + * The surface for recording the contents of this hierarchy, or null if content recording is + * temporarily disabled. + */ + @Nullable private SurfaceControl mRecordedSurface = null; + + /** + * The last bounds of the region to record. + */ + @Nullable private Rect mLastRecordedBounds = null; + + ContentRecorder(@NonNull DisplayContent displayContent) { + mDisplayContent = displayContent; + } + + /** + * Sets the incoming recording session. Should only be used when starting to record on + * this display; stopping recording is handled separately when the display is destroyed. + * + * @param session the new session indicating recording will begin on this display. + */ + void setContentRecordingSession(@Nullable ContentRecordingSession session) { + mContentRecordingSession = session; + } + + /** + * Returns {@code true} if this DisplayContent is currently recording. + */ + boolean isCurrentlyRecording() { + return mContentRecordingSession != null && mRecordedSurface != null; + } + + /** + * Start recording if this DisplayContent no longer has content. Stop recording if it now + * has content or the display is not on. + */ + @VisibleForTesting void updateRecording() { + if (isCurrentlyRecording() && (mDisplayContent.getLastHasContent() + || mDisplayContent.getDisplay().getState() == Display.STATE_OFF)) { + pauseRecording(); + } else { + // Display no longer has content, or now has a surface to write to, so try to start + // recording. + startRecordingIfNeeded(); + } + } + + /** + * Handle a configuration change on the display content, and resize recording if needed. + * @param lastOrientation the prior orientation of the configuration + */ + void onConfigurationChanged(@Configuration.Orientation int lastOrientation) { + // Update surface for MediaProjection, if this DisplayContent is being used for recording. + if (isCurrentlyRecording() && mLastRecordedBounds != null) { + // Recording has already begun, but update recording since the display is now on. + if (mRecordedWindowContainer == null) { + ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, + "Unexpectedly null window container; unable to update recording for " + + "display %d", + mDisplayContent.getDisplayId()); + return; + } + + ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, + "Display %d was already recording, so apply transformations if necessary", + mDisplayContent.getDisplayId()); + // Retrieve the size of the region to record, and continue with the update + // if the bounds or orientation has changed. + final Rect recordedContentBounds = mRecordedWindowContainer.getBounds(); + int recordedContentOrientation = mRecordedWindowContainer.getOrientation(); + if (!mLastRecordedBounds.equals(recordedContentBounds) + || lastOrientation != recordedContentOrientation) { + Point surfaceSize = fetchSurfaceSizeIfPresent(); + if (surfaceSize != null) { + ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, + "Going ahead with updating recording for display %d to new " + + "bounds %s and/or orientation %d.", + mDisplayContent.getDisplayId(), recordedContentBounds, + recordedContentOrientation); + updateMirroredSurface(mDisplayContent.mWmService.mTransactionFactory.get(), + recordedContentBounds, surfaceSize); + } else { + // If the surface removed, do nothing. We will handle this via onDisplayChanged + // (the display will be off if the surface is removed). + ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, + "Unable to update recording for display %d to new bounds %s" + + " and/or orientation %d, since the surface is not available.", + mDisplayContent.getDisplayId(), recordedContentBounds, + recordedContentOrientation); + } + } + } + } + + /** + * Pauses recording on this display content. Note the session does not need to be updated, + * since recording can be resumed still. + */ + void pauseRecording() { + if (mRecordedSurface == null) { + return; + } + ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, + "Display %d has content (%b) so pause recording", mDisplayContent.getDisplayId(), + mDisplayContent.getLastHasContent()); + // If the display is not on and it is a virtual display, then it no longer has an + // associated surface to write output to. + // If the display now has content, stop mirroring to it. + mDisplayContent.mWmService.mTransactionFactory.get() + // Remove the reference to mMirroredSurface, to clean up associated memory. + .remove(mRecordedSurface) + // Reparent the SurfaceControl of this DisplayContent back to mSurfaceControl, + // to allow content to be added to it. This allows this DisplayContent to stop + // mirroring and show content normally. + .reparent(mDisplayContent.getWindowingLayer(), mDisplayContent.getSurfaceControl()) + .reparent(mDisplayContent.getOverlayLayer(), mDisplayContent.getSurfaceControl()) + .apply(); + // Pause mirroring by destroying the reference to the mirrored layer. + mRecordedSurface = null; + // Do not un-set the token, in case content is removed and recording should begin again. + } + + /** + * Stops recording on this DisplayContent, and updates the session details. + */ + void remove() { + if (mRecordedSurface != null) { + // Do not wait for the mirrored surface to be garbage collected, but clean up + // immediately. + mDisplayContent.mWmService.mTransactionFactory.get().remove(mRecordedSurface).apply(); + mRecordedSurface = null; + clearContentRecordingSession(); + } + } + + /** + * Removes both the local cache and WM Service view of the current session, to stop the session + * on this display. + */ + private void clearContentRecordingSession() { + // Update the cached session state first, since updating the service will result in always + // returning to this instance to update recording state. + mContentRecordingSession = null; + mDisplayContent.mWmService.setContentRecordingSession(null); + } + + /** + * Start recording to this DisplayContent if it does not have its own content. Captures the + * content of a WindowContainer indicated by a WindowToken. If unable to start recording, falls + * back to original MediaProjection approach. + */ + private void startRecordingIfNeeded() { + // Only record if this display does not have its own content, is not recording already, + // and if this display is on (it has a surface to write output to). + if (mDisplayContent.getLastHasContent() || isCurrentlyRecording() + || mDisplayContent.getDisplay().getState() == Display.STATE_OFF + || mContentRecordingSession == null) { + return; + } + + final int contentToRecord = mContentRecordingSession.getContentToRecord(); + if (contentToRecord != RECORD_CONTENT_DISPLAY) { + // TODO(b/216625226) handle task-based recording + // Not a valid region, or recording is disabled, so fall back to prior MediaProjection + // approach. + clearContentRecordingSession(); + ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, + "Unable to start recording due to invalid region for display %d", + mDisplayContent.getDisplayId()); + return; + } + // Given the WindowToken of the DisplayArea to record, retrieve the associated + // SurfaceControl. + IBinder tokenToRecord = mContentRecordingSession.getTokenToRecord(); + if (tokenToRecord == null) { + // Unexpectedly missing token. Fall back to prior MediaProjection approach. + clearContentRecordingSession(); + ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, + "Unable to start recording due to null token for display %d", + mDisplayContent.getDisplayId()); + return; + } + + final WindowContainer wc = + mDisplayContent.mWmService.mWindowContextListenerController.getContainer( + tokenToRecord); + if (wc == null) { + // Un-set the window token to record for this VirtualDisplay. Fall back to the + // original MediaProjection approach. + mDisplayContent.mWmService.mDisplayManagerInternal.setWindowManagerMirroring( + mDisplayContent.getDisplayId(), false); + clearContentRecordingSession(); + ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, + "Unable to retrieve window container to start recording for " + + "display %d", + mDisplayContent.getDisplayId()); + return; + } + // TODO(206461622) Migrate to using the RootDisplayArea + mRecordedWindowContainer = wc.getDisplayContent(); + + final Point surfaceSize = fetchSurfaceSizeIfPresent(); + if (surfaceSize == null) { + ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, + "Unable to start recording for display %d since the surface is not " + + "available.", + mDisplayContent.getDisplayId()); + return; + } + ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, + "Display %d has no content and is on, so start recording for state %d", + mDisplayContent.getDisplayId(), mDisplayContent.getDisplay().getState()); + + // Create a mirrored hierarchy for the SurfaceControl of the DisplayArea to capture. + mRecordedSurface = SurfaceControl.mirrorSurface( + mRecordedWindowContainer.getSurfaceControl()); + SurfaceControl.Transaction transaction = + mDisplayContent.mWmService.mTransactionFactory.get() + // Set the mMirroredSurface's parent to the root SurfaceControl for this + // DisplayContent. This brings the new mirrored hierarchy under this + // DisplayContent, + // so SurfaceControl will write the layers of this hierarchy to the + // output surface + // provided by the app. + .reparent(mRecordedSurface, mDisplayContent.getSurfaceControl()) + // Reparent the SurfaceControl of this DisplayContent to null, to prevent + // content + // being added to it. This ensures that no app launched explicitly on the + // VirtualDisplay will show up as part of the mirrored content. + .reparent(mDisplayContent.getWindowingLayer(), null) + .reparent(mDisplayContent.getOverlayLayer(), null); + // Retrieve the size of the DisplayArea to mirror. + updateMirroredSurface(transaction, mRecordedWindowContainer.getBounds(), surfaceSize); + + // No need to clean up. In SurfaceFlinger, parents hold references to their children. The + // mirrored SurfaceControl is alive since the parent DisplayContent SurfaceControl is + // holding a reference to it. Therefore, the mirrored SurfaceControl will be cleaned up + // when the VirtualDisplay is destroyed - which will clean up this DisplayContent. + } + + /** + * Apply transformations to the mirrored surface to ensure the captured contents are scaled to + * fit and centred in the output surface. + * + * @param transaction the transaction to include transformations of mMirroredSurface + * to. Transaction is not applied before returning. + * @param recordedContentBounds bounds of the content to record to the surface provided by + * the app. + * @param surfaceSize the default size of the surface to write the display area + * content to + */ + @VisibleForTesting void updateMirroredSurface(SurfaceControl.Transaction transaction, + Rect recordedContentBounds, Point surfaceSize) { + // Calculate the scale to apply to the root mirror SurfaceControl to fit the size of the + // output surface. + float scaleX = surfaceSize.x / (float) recordedContentBounds.width(); + float scaleY = surfaceSize.y / (float) recordedContentBounds.height(); + float scale = Math.min(scaleX, scaleY); + int scaledWidth = Math.round(scale * (float) recordedContentBounds.width()); + int scaledHeight = Math.round(scale * (float) recordedContentBounds.height()); + + // Calculate the shift to apply to the root mirror SurfaceControl to centre the mirrored + // contents in the output surface. + int shiftedX = 0; + if (scaledWidth != surfaceSize.x) { + shiftedX = (surfaceSize.x - scaledWidth) / 2; + } + int shiftedY = 0; + if (scaledHeight != surfaceSize.y) { + shiftedY = (surfaceSize.y - scaledHeight) / 2; + } + + transaction + // Crop the area to capture to exclude the 'extra' wallpaper that is used + // for parallax (b/189930234). + .setWindowCrop(mRecordedSurface, recordedContentBounds.width(), + recordedContentBounds.height()) + // Scale the root mirror SurfaceControl, based upon the size difference between the + // source (DisplayArea to capture) and output (surface the app reads images from). + .setMatrix(mRecordedSurface, scale, 0 /* dtdx */, 0 /* dtdy */, scale) + // Position needs to be updated when the mirrored DisplayArea has changed, since + // the content will no longer be centered in the output surface. + .setPosition(mRecordedSurface, shiftedX /* x */, shiftedY /* y */) + .apply(); + mLastRecordedBounds = new Rect(recordedContentBounds); + } + + /** + * Returns a non-null {@link Point} if the surface is present, or null otherwise + */ + private Point fetchSurfaceSizeIfPresent() { + // Retrieve the default size of the surface the app provided to + // MediaProjection#createVirtualDisplay. Note the app is the consumer of the surface, + // since it reads out buffers from the surface, and SurfaceFlinger is the producer since + // it writes the mirrored layers to the buffers. + Point surfaceSize = + mDisplayContent.mWmService.mDisplayManagerInternal.getDisplaySurfaceDefaultSize( + mDisplayContent.getDisplayId()); + if (surfaceSize == null) { + // Layer mirroring started with a null surface, so do not apply any transformations yet. + // State of virtual display will change to 'ON' when the surface is set. + // will get event DISPLAY_DEVICE_EVENT_CHANGED + ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, + "Provided surface for recording on display %d is not present, so do not" + + " update the surface", + mDisplayContent.getDisplayId()); + return null; + } + return surfaceSize; + } +} diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index eb2c01adc79e..77b9b225a6a0 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -37,7 +37,6 @@ import static android.os.Process.SYSTEM_UID; import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; import static android.util.DisplayMetrics.DENSITY_DEFAULT; import static android.util.RotationUtils.deltaRotation; -import static android.view.ContentRecordingSession.RECORD_CONTENT_DISPLAY; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.FLAG_CAN_SHOW_WITH_INSECURE_KEYGUARD; import static android.view.Display.FLAG_PRIVATE; @@ -306,28 +305,12 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp */ private SurfaceControl mWindowingLayer; - // TODO(216756854) move all recording fields to the controller /** - * The session for content recording, or null if this DisplayContent is not being used for - * recording. + * Delegate for handling all logic around content recording; decides if this DisplayContent is + * recording, and if so, applies necessary updates to SurfaceFlinger. */ - @VisibleForTesting private ContentRecordingSession mContentRecordingSession = null; - - /** - * The WindowContainer for the level of the hierarchy to record. - */ - @Nullable private DisplayContent mRecordedWindowContainer = null; - - /** - * The surface for recording the contents of this hierarchy, or null if content recording is - * temporarily disabled. - */ - @Nullable private SurfaceControl mRecordedSurface = null; - - /** - * The last bounds of the region to record. - */ - @Nullable private Rect mLastRecordedBounds = null; + @Nullable + private ContentRecorder mContentRecorder; /** * The default per Display minimal size of tasks. Calculated at construction. @@ -2544,42 +2527,8 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp updateImeParent(); // Update surface for MediaProjection, if this DisplayContent is being used for recording. - if (isCurrentlyRecording() && mLastRecordedBounds != null) { - // Recording has already begun, but update recording since the display is now on. - if (mRecordedWindowContainer == null) { - ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, - "Unexpectedly null window container; unable to update recording for " - + "display %d", - mDisplayId); - return; - } - - ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, - "Display %d was already recording, so apply transformations if necessary", - mDisplayId); - // Retrieve the size of the region to record, and continue with the update - // if the bounds or orientation has changed. - final Rect recordedContentBounds = mRecordedWindowContainer.getBounds(); - int recordedContentOrientation = mRecordedWindowContainer.getOrientation(); - if (!mLastRecordedBounds.equals(recordedContentBounds) - || lastOrientation != recordedContentOrientation) { - Point surfaceSize = fetchSurfaceSizeIfPresent(); - if (surfaceSize != null) { - ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, - "Going ahead with updating recording for display %d to new " - + "bounds %s and/or orientation %d.", - mDisplayId, recordedContentBounds, recordedContentOrientation); - updateMirroredSurface(mWmService.mTransactionFactory.get(), - recordedContentBounds, surfaceSize); - } else { - // If the surface removed, do nothing. We will handle this via onDisplayChanged - // (the display will be off if the surface is removed). - ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, - "Unable to update recording for display %d to new bounds %s" - + " and/or orientation %d, since the surface is not available.", - mDisplayId, recordedContentBounds, recordedContentOrientation); - } - } + if (mContentRecorder != null) { + mContentRecorder.onConfigurationChanged(lastOrientation); } if (lastOrientation != getConfiguration().orientation) { @@ -5872,12 +5821,8 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp } mRemoved = true; - if (mRecordedSurface != null) { - // Do not wait for the mirrored surface to be garbage collected, but clean up - // immediately. - mWmService.mTransactionFactory.get().remove(mRecordedSurface).apply(); - mRecordedSurface = null; - clearContentRecordingSession(); + if (mContentRecorder != null) { + mContentRecorder.remove(); } // Only update focus/visibility for the last one because there may be many root tasks are @@ -6117,137 +6062,30 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp return mSandboxDisplayApis; } - /** - * Start recording to this DisplayContent if it does not have its own content. Captures the - * content of a WindowContainer indicated by a WindowToken. If unable to start recording, falls - * back to original MediaProjection approach. - */ - private void startRecordingIfNeeded() { - // Only record if this display does not have its own content, is not recording already, - // and if this display is on (it has a surface to write output to). - if (mLastHasContent || isCurrentlyRecording() || mDisplay.getState() == Display.STATE_OFF - || mContentRecordingSession == null) { - return; - } - - final int contentToRecord = mContentRecordingSession.getContentToRecord(); - if (contentToRecord != RECORD_CONTENT_DISPLAY) { - // TODO(b/216625226) handle task-based recording - // Not a valid region, or recording is disabled, so fall back to prior MediaProjection - // approach. - clearContentRecordingSession(); - ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, - "Unable to start recording due to invalid region for display %d", - mDisplayId); - return; - } - // Given the WindowToken of the DisplayArea to record, retrieve the associated - // SurfaceControl. - IBinder tokenToRecord = mContentRecordingSession.getTokenToRecord(); - if (tokenToRecord == null) { - // Unexpectedly missing token. Fall back to prior MediaProjection approach. - clearContentRecordingSession(); - ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, - "Unable to start recording due to null token for display %d", mDisplayId); - return; - } - - final WindowContainer wc = mWmService.mWindowContextListenerController.getContainer( - tokenToRecord); - if (wc == null) { - // Un-set the window token to record for this VirtualDisplay. Fall back to the - // original MediaProjection approach. - mWmService.mDisplayManagerInternal.setWindowManagerMirroring(mDisplayId, false); - clearContentRecordingSession(); - ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, - "Unable to retrieve window container to start recording for " - + "display %d", - mDisplayId); - return; - } - // TODO(206461622) Migrate to the RootDisplayArea - mRecordedWindowContainer = wc.getDisplayContent(); - - final Point surfaceSize = fetchSurfaceSizeIfPresent(); - if (surfaceSize == null) { - ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, - "Unable to start recording for display %d since the surface is not " - + "available.", - mDisplayId); - return; + private ContentRecorder getContentRecorder() { + if (mContentRecorder == null) { + mContentRecorder = new ContentRecorder(this); } - ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, - "Display %d has no content and is on, so start recording for state %d", - mDisplayId, mDisplay.getState()); - - // Create a mirrored hierarchy for the SurfaceControl of the DisplayArea to capture. - mRecordedSurface = SurfaceControl.mirrorSurface( - mRecordedWindowContainer.getSurfaceControl()); - SurfaceControl.Transaction transaction = mWmService.mTransactionFactory.get() - // Set the mMirroredSurface's parent to the root SurfaceControl for this - // DisplayContent. This brings the new mirrored hierarchy under this DisplayContent, - // so SurfaceControl will write the layers of this hierarchy to the output surface - // provided by the app. - .reparent(mRecordedSurface, mSurfaceControl) - // Reparent the SurfaceControl of this DisplayContent to null, to prevent content - // being added to it. This ensures that no app launched explicitly on the - // VirtualDisplay will show up as part of the mirrored content. - .reparent(mWindowingLayer, null) - .reparent(mOverlayLayer, null); - // Retrieve the size of the DisplayArea to mirror. - updateMirroredSurface(transaction, mRecordedWindowContainer.getBounds(), surfaceSize); - - // No need to clean up. In SurfaceFlinger, parents hold references to their children. The - // mirrored SurfaceControl is alive since the parent DisplayContent SurfaceControl is - // holding a reference to it. Therefore, the mirrored SurfaceControl will be cleaned up - // when the VirtualDisplay is destroyed - which will clean up this DisplayContent. + return mContentRecorder; } /** * Pause the recording session. */ - public void pauseRecording() { - if (mRecordedSurface == null) { - return; + @VisibleForTesting void pauseRecording() { + if (mContentRecorder != null) { + mContentRecorder.pauseRecording(); } - ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, - "Display %d has content (%b) so pause recording", mDisplayId, - mLastHasContent); - // If the display is not on and it is a virtual display, then it no longer has an - // associated surface to write output to. - // If the display now has content, stop mirroring to it. - mWmService.mTransactionFactory.get() - // Remove the reference to mMirroredSurface, to clean up associated memory. - .remove(mRecordedSurface) - // Reparent the SurfaceControl of this DisplayContent back to mSurfaceControl, - // to allow content to be added to it. This allows this DisplayContent to stop - // mirroring and show content normally. - .reparent(mWindowingLayer, mSurfaceControl) - .reparent(mOverlayLayer, mSurfaceControl) - .apply(); - // Pause mirroring by destroying the reference to the mirrored layer. - mRecordedSurface = null; - // Do not un-set the token, in case content is removed and recording should begin again. } /** * Sets the incoming recording session. Should only be used when starting to record on * this display; stopping recording is handled separately when the display is destroyed. + * * @param session the new session indicating recording will begin on this display. */ - public void setContentRecordingSession(@Nullable ContentRecordingSession session) { - mContentRecordingSession = session; - } - - /** - * Removes both the local cache and WM Service view of the current session, to stop the session - * on this display. - */ - private void clearContentRecordingSession() { - // Update the cached session state first, since updating the service will result in always - // returning to this instance to update recording state. - mContentRecordingSession = null; - mWmService.setContentRecordingSession(null); + void setContentRecordingSession(@Nullable ContentRecordingSession session) { + getContentRecorder().setContentRecordingSession(session); } /** @@ -6255,97 +6093,14 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp * has content or the display is not on. */ @VisibleForTesting void updateRecording() { - if (isCurrentlyRecording() && (mLastHasContent - || mDisplay.getState() == Display.STATE_OFF)) { - pauseRecording(); - } else { - // Display no longer has content, or now has a surface to write to, so try to start - // recording. - startRecordingIfNeeded(); - } - } - - /** - * Apply transformations to the mirrored surface to ensure the captured contents are scaled to - * fit and centred in the output surface. - * - * @param transaction the transaction to include transformations of mMirroredSurface - * to. Transaction is not applied before returning. - * @param recordedContentBounds bounds of the content to record to the surface provided by - * the app. - * @param surfaceSize the default size of the surface to write the display area - * content to - */ - @VisibleForTesting - void updateMirroredSurface(SurfaceControl.Transaction transaction, - Rect recordedContentBounds, Point surfaceSize) { - // Calculate the scale to apply to the root mirror SurfaceControl to fit the size of the - // output surface. - float scaleX = surfaceSize.x / (float) recordedContentBounds.width(); - float scaleY = surfaceSize.y / (float) recordedContentBounds.height(); - float scale = Math.min(scaleX, scaleY); - int scaledWidth = Math.round(scale * (float) recordedContentBounds.width()); - int scaledHeight = Math.round(scale * (float) recordedContentBounds.height()); - - // Calculate the shift to apply to the root mirror SurfaceControl to centre the mirrored - // contents in the output surface. - int shiftedX = 0; - if (scaledWidth != surfaceSize.x) { - shiftedX = (surfaceSize.x - scaledWidth) / 2; - } - int shiftedY = 0; - if (scaledHeight != surfaceSize.y) { - shiftedY = (surfaceSize.y - scaledHeight) / 2; - } - - transaction - // Crop the area to capture to exclude the 'extra' wallpaper that is used - // for parallax (b/189930234). - .setWindowCrop(mRecordedSurface, recordedContentBounds.width(), - recordedContentBounds.height()) - // Scale the root mirror SurfaceControl, based upon the size difference between the - // source (DisplayArea to capture) and output (surface the app reads images from). - .setMatrix(mRecordedSurface, scale, 0 /* dtdx */, 0 /* dtdy */, scale) - // Position needs to be updated when the mirrored DisplayArea has changed, since - // the content will no longer be centered in the output surface. - .setPosition(mRecordedSurface, shiftedX /* x */, shiftedY /* y */) - .apply(); - mLastRecordedBounds = new Rect(recordedContentBounds); - } - - /** - * Returns a non-null {@link Point} if the surface is present, or null otherwise - */ - Point fetchSurfaceSizeIfPresent() { - // Retrieve the default size of the surface the app provided to - // MediaProjection#createVirtualDisplay. Note the app is the consumer of the surface, - // since it reads out buffers from the surface, and SurfaceFlinger is the producer since - // it writes the mirrored layers to the buffers. - Point surfaceSize = mWmService.mDisplayManagerInternal.getDisplaySurfaceDefaultSize( - mDisplayId); - if (surfaceSize == null) { - // Layer mirroring started with a null surface, so do not apply any transformations yet. - // State of virtual display will change to 'ON' when the surface is set. - // will get event DISPLAY_DEVICE_EVENT_CHANGED - ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, - "Provided surface for recording on display %d is not present, so do not" - + " update the surface", - mDisplayId); - return null; - } - return surfaceSize; + getContentRecorder().updateRecording(); } /** * Returns {@code true} if this DisplayContent is currently recording. */ boolean isCurrentlyRecording() { - return mContentRecordingSession != null && mRecordedSurface != null; - } - - @VisibleForTesting - @Nullable ContentRecordingSession getContentRecordingSession() { - return mContentRecordingSession; + return mContentRecorder != null && mContentRecorder.isCurrentlyRecording(); } /** The entry for proceeding to handle {@link #mFixedRotationLaunchingApp}. */ diff --git a/services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java b/services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java new file mode 100644 index 000000000000..50eefa066a45 --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java @@ -0,0 +1,239 @@ +/* + * 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.server.wm; + + +import static android.content.res.Configuration.ORIENTATION_PORTRAIT; +import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR; +import static android.view.ContentRecordingSession.RECORD_CONTENT_TASK; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.any; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.anyFloat; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.never; + +import android.graphics.Point; +import android.graphics.Rect; +import android.hardware.display.VirtualDisplay; +import android.os.Binder; +import android.os.IBinder; +import android.platform.test.annotations.Presubmit; +import android.util.DisplayMetrics; +import android.view.ContentRecordingSession; +import android.view.Surface; +import android.view.SurfaceControl; + +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Tests for the {@link ContentRecorder} class. + * + * Build/Install/Run: + * atest WmTests:ContentRecorderTests + */ +@SmallTest +@Presubmit +@RunWith(WindowTestRunner.class) +public class ContentRecorderTests extends WindowTestsBase { + private static final IBinder TEST_TOKEN = new RecordingTestToken(); + private final ContentRecordingSession mDefaultSession = + ContentRecordingSession.createDisplaySession(TEST_TOKEN); + private static Point sSurfaceSize; + private ContentRecorder mContentRecorder; + private SurfaceControl mRecordedSurface; + + @Before public void setUp() { + // GIVEN MediaProjection has already initialized the WindowToken of the DisplayArea to + // mirror. + setUpDefaultTaskDisplayAreaWindowToken(); + + // GIVEN SurfaceControl can successfully mirror the provided surface. + sSurfaceSize = new Point( + mDefaultDisplay.getDefaultTaskDisplayArea().getBounds().width(), + mDefaultDisplay.getDefaultTaskDisplayArea().getBounds().height()); + mRecordedSurface = surfaceControlMirrors(sSurfaceSize); + + // GIVEN the VirtualDisplay associated with the session (so the display has state ON). + VirtualDisplay virtualDisplay = mWm.mDisplayManager.createVirtualDisplay("VirtualDisplay", + sSurfaceSize.x, sSurfaceSize.y, + DisplayMetrics.DENSITY_140, new Surface(), VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR); + final int displayId = virtualDisplay.getDisplay().getDisplayId(); + mDefaultSession.setDisplayId(displayId); + + mWm.mRoot.onDisplayAdded(displayId); + final DisplayContent mVirtualDisplayContent = mWm.mRoot.getDisplayContent(displayId); + mContentRecorder = new ContentRecorder(mVirtualDisplayContent); + spyOn(mVirtualDisplayContent); + } + + @Test + public void testIsCurrentlyRecording() { + assertThat(mContentRecorder.isCurrentlyRecording()).isFalse(); + + mContentRecorder.updateRecording(); + assertThat(mContentRecorder.isCurrentlyRecording()).isFalse(); + } + + @Test + public void testUpdateRecording_display() { + mContentRecorder.setContentRecordingSession(mDefaultSession); + mContentRecorder.updateRecording(); + assertThat(mContentRecorder.isCurrentlyRecording()).isTrue(); + } + + @Test + public void testUpdateRecording_task() { + mDefaultSession.setContentToRecord(RECORD_CONTENT_TASK); + mContentRecorder.setContentRecordingSession(mDefaultSession); + mContentRecorder.updateRecording(); + assertThat(mContentRecorder.isCurrentlyRecording()).isFalse(); + } + + @Test + public void testUpdateRecording_wasPaused() { + mContentRecorder.setContentRecordingSession(mDefaultSession); + mContentRecorder.updateRecording(); + + mContentRecorder.pauseRecording(); + mContentRecorder.updateRecording(); + assertThat(mContentRecorder.isCurrentlyRecording()).isTrue(); + } + + @Test + public void testUpdateRecording_wasStopped() { + mContentRecorder.setContentRecordingSession(mDefaultSession); + mContentRecorder.updateRecording(); + + mContentRecorder.remove(); + mContentRecorder.updateRecording(); + assertThat(mContentRecorder.isCurrentlyRecording()).isFalse(); + } + + @Test + public void testOnConfigurationChanged_neverRecording() { + mContentRecorder.onConfigurationChanged(ORIENTATION_PORTRAIT); + + verify(mTransaction, never()).setPosition(eq(mRecordedSurface), anyFloat(), anyFloat()); + verify(mTransaction, never()).setMatrix(eq(mRecordedSurface), anyFloat(), anyFloat(), + anyFloat(), anyFloat()); + } + + @Test + public void testOnConfigurationChanged_resizesSurface() { + mContentRecorder.setContentRecordingSession(mDefaultSession); + mContentRecorder.updateRecording(); + mContentRecorder.onConfigurationChanged(ORIENTATION_PORTRAIT); + + verify(mTransaction, atLeastOnce()).setPosition(eq(mRecordedSurface), anyFloat(), + anyFloat()); + verify(mTransaction, atLeastOnce()).setMatrix(eq(mRecordedSurface), anyFloat(), anyFloat(), + anyFloat(), anyFloat()); + } + + @Test + public void testPauseRecording_pausesRecording() { + mContentRecorder.setContentRecordingSession(mDefaultSession); + mContentRecorder.updateRecording(); + + mContentRecorder.pauseRecording(); + assertThat(mContentRecorder.isCurrentlyRecording()).isFalse(); + } + + @Test + public void testPauseRecording_neverRecording() { + mContentRecorder.pauseRecording(); + assertThat(mContentRecorder.isCurrentlyRecording()).isFalse(); + } + + @Test + public void testRemove_stopsRecording() { + mContentRecorder.setContentRecordingSession(mDefaultSession); + mContentRecorder.updateRecording(); + + mContentRecorder.remove(); + assertThat(mContentRecorder.isCurrentlyRecording()).isFalse(); + } + + @Test + public void testRemove_neverRecording() { + mContentRecorder.remove(); + assertThat(mContentRecorder.isCurrentlyRecording()).isFalse(); + } + + @Test + public void testUpdateMirroredSurface_capturedAreaResized() { + mContentRecorder.setContentRecordingSession(mDefaultSession); + mContentRecorder.updateRecording(); + + // WHEN attempting to mirror on the virtual display, and the captured content is resized. + float xScale = 0.7f; + float yScale = 2f; + Rect displayAreaBounds = new Rect(0, 0, Math.round(sSurfaceSize.x * xScale), + Math.round(sSurfaceSize.y * yScale)); + mContentRecorder.updateMirroredSurface(mTransaction, displayAreaBounds, sSurfaceSize); + + // THEN content in the captured DisplayArea is scaled to fit the surface size. + verify(mTransaction, atLeastOnce()).setMatrix(mRecordedSurface, 1.0f / yScale, 0, 0, + 1.0f / yScale); + // THEN captured content is positioned in the centre of the output surface. + float scaledWidth = displayAreaBounds.width() / xScale; + float xInset = (sSurfaceSize.x - scaledWidth) / 2; + verify(mTransaction, atLeastOnce()).setPosition(mRecordedSurface, xInset, 0); + } + + private static class RecordingTestToken extends Binder { + } + + /** + * Creates a WindowToken associated with the default task DisplayArea, in order for that + * DisplayArea to be mirrored. + */ + private void setUpDefaultTaskDisplayAreaWindowToken() { + // GIVEN the default task display area is represented by the WindowToken. + spyOn(mWm.mWindowContextListenerController); + doReturn(mDefaultDisplay.getDefaultTaskDisplayArea()).when( + mWm.mWindowContextListenerController).getContainer(any()); + } + + /** + * SurfaceControl successfully creates a mirrored surface of the given size. + */ + private SurfaceControl surfaceControlMirrors(Point surfaceSize) { + // Do not set the parent, since the mirrored surface is the root of a new surface hierarchy. + SurfaceControl mirroredSurface = new SurfaceControl.Builder() + .setName("mirroredSurface") + .setBufferSize(surfaceSize.x, surfaceSize.y) + .setCallsite("mirrorSurface") + .build(); + doReturn(mirroredSurface).when(() -> SurfaceControl.mirrorSurface(any())); + doReturn(surfaceSize).when(mWm.mDisplayManagerInternal).getDisplaySurfaceDefaultSize( + anyInt()); + return mirroredSurface; + } +} diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java index b7be932c0fd3..ed5ea9c15734 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java @@ -68,7 +68,6 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyBoolean; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; import static com.android.dx.mockito.inline.extended.ExtendedMockito.never; import static com.android.dx.mockito.inline.extended.ExtendedMockito.reset; import static com.android.dx.mockito.inline.extended.ExtendedMockito.same; @@ -97,7 +96,6 @@ import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeast; -import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doCallRealMethod; @@ -150,8 +148,6 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; -import org.mockito.MockitoSession; -import org.mockito.quality.Strictness; import java.util.ArrayList; import java.util.Arrays; @@ -2281,98 +2277,7 @@ public class DisplayContentTests extends WindowTestsBase { } @Test - public void testVirtualDisplayContent() { - MockitoSession mockSession = mockitoSession() - .initMocks(this) - .spyStatic(SurfaceControl.class) - .strictness(Strictness.LENIENT) - .startMocking(); - - // GIVEN MediaProjection has already initialized the WindowToken of the DisplayArea to - // mirror. - final IBinder tokenToMirror = setUpDefaultTaskDisplayAreaWindowToken(); - - // GIVEN SurfaceControl can successfully mirror the provided surface. - Point surfaceSize = new Point( - mDefaultDisplay.getDefaultTaskDisplayArea().getBounds().width(), - mDefaultDisplay.getDefaultTaskDisplayArea().getBounds().height()); - surfaceControlMirrors(surfaceSize); - - // WHEN creating the DisplayContent for a new virtual display. - final DisplayContent virtualDisplay = new TestDisplayContent.Builder(mAtm, - mDisplayInfo).build(); - - // GIVEN a session is set up to capture a DisplayContent. - ContentRecordingSession session = ContentRecordingSession.createDisplaySession( - tokenToMirror); - session.setDisplayId(virtualDisplay.getDisplayId()); - mWm.mContentRecordingController.setContentRecordingSessionLocked(session, mWm); - - // WHEN attempting to mirror on the virtual display. - virtualDisplay.updateRecording(); - - // THEN mirroring is initiated for the default display's DisplayArea. - assertThat(virtualDisplay.isCurrentlyRecording()).isTrue(); - assertThat(virtualDisplay.getContentRecordingSession()).isEqualTo(session); - - mockSession.finishMocking(); - } - - @Test - public void testVirtualDisplayContent_capturedAreaResized() { - MockitoSession mockSession = mockitoSession() - .initMocks(this) - .spyStatic(SurfaceControl.class) - .strictness(Strictness.LENIENT) - .startMocking(); - - // GIVEN MediaProjection has already initialized the WindowToken of the DisplayArea to - // mirror. - final IBinder tokenToMirror = setUpDefaultTaskDisplayAreaWindowToken(); - - // GIVEN SurfaceControl can successfully mirror the provided surface. - Point surfaceSize = new Point( - mDefaultDisplay.getDefaultTaskDisplayArea().getBounds().width(), - mDefaultDisplay.getDefaultTaskDisplayArea().getBounds().height()); - SurfaceControl mirroredSurface = surfaceControlMirrors(surfaceSize); - - // WHEN creating the DisplayContent for a new virtual display. - final DisplayContent virtualDisplay = new TestDisplayContent.Builder(mAtm, - mDisplayInfo).build(); - - // GIVEN a session is set up to capture a DisplayContent. - ContentRecordingSession session = ContentRecordingSession.createDisplaySession( - tokenToMirror); - session.setDisplayId(virtualDisplay.getDisplayId()); - mWm.mContentRecordingController.setContentRecordingSessionLocked(session, mWm); - - // WHEN attempting to mirror on the virtual display, and the captured content is resized. - virtualDisplay.updateRecording(); - float xScale = 0.7f; - float yScale = 2f; - Rect displayAreaBounds = new Rect(0, 0, Math.round(surfaceSize.x * xScale), - Math.round(surfaceSize.y * yScale)); - virtualDisplay.updateMirroredSurface(mTransaction, displayAreaBounds, surfaceSize); - - // THEN content in the captured DisplayArea is scaled to fit the surface size. - verify(mTransaction, atLeastOnce()).setMatrix(mirroredSurface, 1.0f / yScale, 0, 0, - 1.0f / yScale); - // THEN captured content is positioned in the centre of the output surface. - float scaledWidth = displayAreaBounds.width() / xScale; - float xInset = (surfaceSize.x - scaledWidth) / 2; - verify(mTransaction, atLeastOnce()).setPosition(mirroredSurface, xInset, 0); - - mockSession.finishMocking(); - } - - @Test public void testVirtualDisplayContent_withoutSurface() { - MockitoSession mockSession = mockitoSession() - .initMocks(this) - .spyStatic(SurfaceControl.class) - .strictness(Strictness.LENIENT) - .startMocking(); - // GIVEN MediaProjection has already initialized the WindowToken of the DisplayArea to // mirror. final IBinder tokenToMirror = setUpDefaultTaskDisplayAreaWindowToken(); @@ -2392,25 +2297,17 @@ public class DisplayContentTests extends WindowTestsBase { ContentRecordingSession session = ContentRecordingSession.createDisplaySession( tokenToMirror); session.setDisplayId(displayId); - mWm.mContentRecordingController.setContentRecordingSessionLocked(session, mWm); + mWm.setContentRecordingSession(session); actualDC.updateRecording(); // THEN mirroring is not started, since a null surface indicates the VirtualDisplay is off. assertThat(actualDC.isCurrentlyRecording()).isFalse(); - assertThat(actualDC.getContentRecordingSession()).isEqualTo(session); display.release(); - mockSession.finishMocking(); } @Test public void testVirtualDisplayContent_withSurface() { - MockitoSession mockSession = mockitoSession() - .initMocks(this) - .spyStatic(SurfaceControl.class) - .strictness(Strictness.LENIENT) - .startMocking(); - // GIVEN MediaProjection has already initialized the WindowToken of the DisplayArea to // mirror. final IBinder tokenToMirror = setUpDefaultTaskDisplayAreaWindowToken(); @@ -2438,10 +2335,8 @@ public class DisplayContentTests extends WindowTestsBase { // THEN mirroring is initiated for the default display's DisplayArea. assertThat(actualDC.isCurrentlyRecording()).isTrue(); - assertThat(actualDC.getContentRecordingSession()).isEqualTo(session); display.release(); - mockSession.finishMocking(); } private static class MirroringTestToken extends Binder { diff --git a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java index 9a33e23fcb30..d038fea4680b 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java +++ b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java @@ -148,6 +148,7 @@ public class SystemServicesTestRule implements TestRule { private void setUp() { mMockitoSession = mockitoSession() .spyStatic(LocalServices.class) + .spyStatic(SurfaceControl.class) .mockStatic(LockGuard.class) .mockStatic(Watchdog.class) .strictness(Strictness.LENIENT) |