Refactor content recording out of DisplayContent
Bug: 216756854
Test: atest WmTests:ContentRecorderTests
Test: atest WmTests:DisplayContentTests
Change-Id: Idf8f5b22986f889348ca187b2f1f005da163e5e2
diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json
index 3d50531..df2b2a3 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 0000000..07a0c37
--- /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 eb2c01ad..77b9b22 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.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 @@
*/
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 @@
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 @@
}
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 @@
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;
+ private ContentRecorder getContentRecorder() {
+ if (mContentRecorder == null) {
+ mContentRecorder = new ContentRecorder(this);
}
-
- 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;
- }
- 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 @@
* 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 0000000..50eefa0
--- /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 b7be932..ed5ea9c 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.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.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.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 @@
}
@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 @@
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 @@
// 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 9a33e23..d038fea 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 @@
private void setUp() {
mMockitoSession = mockitoSession()
.spyStatic(LocalServices.class)
+ .spyStatic(SurfaceControl.class)
.mockStatic(LockGuard.class)
.mockStatic(Watchdog.class)
.strictness(Strictness.LENIENT)