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)