Refactor content recording out of DisplayContent

Bug: 216756854
Test: atest WmTests:ContentRecorderTests
Test: atest WmTests:DisplayContentTests
Change-Id: Idf8f5b22986f889348ca187b2f1f005da163e5e2
This commit is contained in:
Naomi Musgrave
2022-02-25 14:10:58 +00:00
parent 27655e1fc3
commit 472e00b0dc
6 changed files with 692 additions and 434 deletions

View File

@@ -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",

View File

@@ -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;
}
}

View File

@@ -37,7 +37,6 @@ import static android.os.Process.SYSTEM_UID;
import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
import static android.util.DisplayMetrics.DENSITY_DEFAULT;
import static android.util.RotationUtils.deltaRotation;
import static android.view.ContentRecordingSession.RECORD_CONTENT_DISPLAY;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.Display.FLAG_CAN_SHOW_WITH_INSECURE_KEYGUARD;
import static android.view.Display.FLAG_PRIVATE;
@@ -306,28 +305,12 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp
*/
private SurfaceControl mWindowingLayer;
// TODO(216756854) move all recording fields to the controller
/**
* The session for content recording, or null if this DisplayContent is not being used for
* recording.
* Delegate for handling all logic around content recording; decides if this DisplayContent is
* recording, and if so, applies necessary updates to SurfaceFlinger.
*/
@VisibleForTesting private ContentRecordingSession mContentRecordingSession = null;
/**
* The WindowContainer for the level of the hierarchy to record.
*/
@Nullable private DisplayContent mRecordedWindowContainer = null;
/**
* The surface for recording the contents of this hierarchy, or null if content recording is
* temporarily disabled.
*/
@Nullable private SurfaceControl mRecordedSurface = null;
/**
* The last bounds of the region to record.
*/
@Nullable private Rect mLastRecordedBounds = null;
@Nullable
private ContentRecorder mContentRecorder;
/**
* The default per Display minimal size of tasks. Calculated at construction.
@@ -2544,42 +2527,8 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp
updateImeParent();
// Update surface for MediaProjection, if this DisplayContent is being used for recording.
if (isCurrentlyRecording() && mLastRecordedBounds != null) {
// Recording has already begun, but update recording since the display is now on.
if (mRecordedWindowContainer == null) {
ProtoLog.v(WM_DEBUG_CONTENT_RECORDING,
"Unexpectedly null window container; unable to update recording for "
+ "display %d",
mDisplayId);
return;
}
ProtoLog.v(WM_DEBUG_CONTENT_RECORDING,
"Display %d was already recording, so apply transformations if necessary",
mDisplayId);
// Retrieve the size of the region to record, and continue with the update
// if the bounds or orientation has changed.
final Rect recordedContentBounds = mRecordedWindowContainer.getBounds();
int recordedContentOrientation = mRecordedWindowContainer.getOrientation();
if (!mLastRecordedBounds.equals(recordedContentBounds)
|| lastOrientation != recordedContentOrientation) {
Point surfaceSize = fetchSurfaceSizeIfPresent();
if (surfaceSize != null) {
ProtoLog.v(WM_DEBUG_CONTENT_RECORDING,
"Going ahead with updating recording for display %d to new "
+ "bounds %s and/or orientation %d.",
mDisplayId, recordedContentBounds, recordedContentOrientation);
updateMirroredSurface(mWmService.mTransactionFactory.get(),
recordedContentBounds, surfaceSize);
} else {
// If the surface removed, do nothing. We will handle this via onDisplayChanged
// (the display will be off if the surface is removed).
ProtoLog.v(WM_DEBUG_CONTENT_RECORDING,
"Unable to update recording for display %d to new bounds %s"
+ " and/or orientation %d, since the surface is not available.",
mDisplayId, recordedContentBounds, recordedContentOrientation);
}
}
if (mContentRecorder != null) {
mContentRecorder.onConfigurationChanged(lastOrientation);
}
if (lastOrientation != getConfiguration().orientation) {
@@ -5872,12 +5821,8 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp
}
mRemoved = true;
if (mRecordedSurface != null) {
// Do not wait for the mirrored surface to be garbage collected, but clean up
// immediately.
mWmService.mTransactionFactory.get().remove(mRecordedSurface).apply();
mRecordedSurface = null;
clearContentRecordingSession();
if (mContentRecorder != null) {
mContentRecorder.remove();
}
// Only update focus/visibility for the last one because there may be many root tasks are
@@ -6117,137 +6062,30 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp
return mSandboxDisplayApis;
}
/**
* Start recording to this DisplayContent if it does not have its own content. Captures the
* content of a WindowContainer indicated by a WindowToken. If unable to start recording, falls
* back to original MediaProjection approach.
*/
private void startRecordingIfNeeded() {
// Only record if this display does not have its own content, is not recording already,
// and if this display is on (it has a surface to write output to).
if (mLastHasContent || isCurrentlyRecording() || mDisplay.getState() == Display.STATE_OFF
|| mContentRecordingSession == null) {
return;
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 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp
* has content or the display is not on.
*/
@VisibleForTesting void updateRecording() {
if (isCurrentlyRecording() && (mLastHasContent
|| mDisplay.getState() == Display.STATE_OFF)) {
pauseRecording();
} else {
// Display no longer has content, or now has a surface to write to, so try to start
// recording.
startRecordingIfNeeded();
}
}
/**
* Apply transformations to the mirrored surface to ensure the captured contents are scaled to
* fit and centred in the output surface.
*
* @param transaction the transaction to include transformations of mMirroredSurface
* to. Transaction is not applied before returning.
* @param recordedContentBounds bounds of the content to record to the surface provided by
* the app.
* @param surfaceSize the default size of the surface to write the display area
* content to
*/
@VisibleForTesting
void updateMirroredSurface(SurfaceControl.Transaction transaction,
Rect recordedContentBounds, Point surfaceSize) {
// Calculate the scale to apply to the root mirror SurfaceControl to fit the size of the
// output surface.
float scaleX = surfaceSize.x / (float) recordedContentBounds.width();
float scaleY = surfaceSize.y / (float) recordedContentBounds.height();
float scale = Math.min(scaleX, scaleY);
int scaledWidth = Math.round(scale * (float) recordedContentBounds.width());
int scaledHeight = Math.round(scale * (float) recordedContentBounds.height());
// Calculate the shift to apply to the root mirror SurfaceControl to centre the mirrored
// contents in the output surface.
int shiftedX = 0;
if (scaledWidth != surfaceSize.x) {
shiftedX = (surfaceSize.x - scaledWidth) / 2;
}
int shiftedY = 0;
if (scaledHeight != surfaceSize.y) {
shiftedY = (surfaceSize.y - scaledHeight) / 2;
}
transaction
// Crop the area to capture to exclude the 'extra' wallpaper that is used
// for parallax (b/189930234).
.setWindowCrop(mRecordedSurface, recordedContentBounds.width(),
recordedContentBounds.height())
// Scale the root mirror SurfaceControl, based upon the size difference between the
// source (DisplayArea to capture) and output (surface the app reads images from).
.setMatrix(mRecordedSurface, scale, 0 /* dtdx */, 0 /* dtdy */, scale)
// Position needs to be updated when the mirrored DisplayArea has changed, since
// the content will no longer be centered in the output surface.
.setPosition(mRecordedSurface, shiftedX /* x */, shiftedY /* y */)
.apply();
mLastRecordedBounds = new Rect(recordedContentBounds);
}
/**
* Returns a non-null {@link Point} if the surface is present, or null otherwise
*/
Point fetchSurfaceSizeIfPresent() {
// Retrieve the default size of the surface the app provided to
// MediaProjection#createVirtualDisplay. Note the app is the consumer of the surface,
// since it reads out buffers from the surface, and SurfaceFlinger is the producer since
// it writes the mirrored layers to the buffers.
Point surfaceSize = mWmService.mDisplayManagerInternal.getDisplaySurfaceDefaultSize(
mDisplayId);
if (surfaceSize == null) {
// Layer mirroring started with a null surface, so do not apply any transformations yet.
// State of virtual display will change to 'ON' when the surface is set.
// will get event DISPLAY_DEVICE_EVENT_CHANGED
ProtoLog.v(WM_DEBUG_CONTENT_RECORDING,
"Provided surface for recording on display %d is not present, so do not"
+ " update the surface",
mDisplayId);
return null;
}
return surfaceSize;
getContentRecorder().updateRecording();
}
/**
* Returns {@code true} if this DisplayContent is currently recording.
*/
boolean isCurrentlyRecording() {
return mContentRecordingSession != null && mRecordedSurface != null;
}
@VisibleForTesting
@Nullable ContentRecordingSession getContentRecordingSession() {
return mContentRecordingSession;
return mContentRecorder != null && mContentRecorder.isCurrentlyRecording();
}
/** The entry for proceeding to handle {@link #mFixedRotationLaunchingApp}. */

View File

@@ -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;
}
}

View File

@@ -68,7 +68,6 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyBoolean;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.never;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.reset;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.same;
@@ -97,7 +96,6 @@ import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doCallRealMethod;
@@ -150,8 +148,6 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import org.mockito.MockitoSession;
import org.mockito.quality.Strictness;
import java.util.ArrayList;
import java.util.Arrays;
@@ -2280,99 +2276,8 @@ public class DisplayContentTests extends WindowTestsBase {
assertNotEquals(imeMenuDialog, mDisplayContent.findFocusedWindow());
}
@Test
public void testVirtualDisplayContent() {
MockitoSession mockSession = mockitoSession()
.initMocks(this)
.spyStatic(SurfaceControl.class)
.strictness(Strictness.LENIENT)
.startMocking();
// GIVEN MediaProjection has already initialized the WindowToken of the DisplayArea to
// mirror.
final IBinder tokenToMirror = setUpDefaultTaskDisplayAreaWindowToken();
// GIVEN SurfaceControl can successfully mirror the provided surface.
Point surfaceSize = new Point(
mDefaultDisplay.getDefaultTaskDisplayArea().getBounds().width(),
mDefaultDisplay.getDefaultTaskDisplayArea().getBounds().height());
surfaceControlMirrors(surfaceSize);
// WHEN creating the DisplayContent for a new virtual display.
final DisplayContent virtualDisplay = new TestDisplayContent.Builder(mAtm,
mDisplayInfo).build();
// GIVEN a session is set up to capture a DisplayContent.
ContentRecordingSession session = ContentRecordingSession.createDisplaySession(
tokenToMirror);
session.setDisplayId(virtualDisplay.getDisplayId());
mWm.mContentRecordingController.setContentRecordingSessionLocked(session, mWm);
// WHEN attempting to mirror on the virtual display.
virtualDisplay.updateRecording();
// THEN mirroring is initiated for the default display's DisplayArea.
assertThat(virtualDisplay.isCurrentlyRecording()).isTrue();
assertThat(virtualDisplay.getContentRecordingSession()).isEqualTo(session);
mockSession.finishMocking();
}
@Test
public void testVirtualDisplayContent_capturedAreaResized() {
MockitoSession mockSession = mockitoSession()
.initMocks(this)
.spyStatic(SurfaceControl.class)
.strictness(Strictness.LENIENT)
.startMocking();
// GIVEN MediaProjection has already initialized the WindowToken of the DisplayArea to
// mirror.
final IBinder tokenToMirror = setUpDefaultTaskDisplayAreaWindowToken();
// GIVEN SurfaceControl can successfully mirror the provided surface.
Point surfaceSize = new Point(
mDefaultDisplay.getDefaultTaskDisplayArea().getBounds().width(),
mDefaultDisplay.getDefaultTaskDisplayArea().getBounds().height());
SurfaceControl mirroredSurface = surfaceControlMirrors(surfaceSize);
// WHEN creating the DisplayContent for a new virtual display.
final DisplayContent virtualDisplay = new TestDisplayContent.Builder(mAtm,
mDisplayInfo).build();
// GIVEN a session is set up to capture a DisplayContent.
ContentRecordingSession session = ContentRecordingSession.createDisplaySession(
tokenToMirror);
session.setDisplayId(virtualDisplay.getDisplayId());
mWm.mContentRecordingController.setContentRecordingSessionLocked(session, mWm);
// WHEN attempting to mirror on the virtual display, and the captured content is resized.
virtualDisplay.updateRecording();
float xScale = 0.7f;
float yScale = 2f;
Rect displayAreaBounds = new Rect(0, 0, Math.round(surfaceSize.x * xScale),
Math.round(surfaceSize.y * yScale));
virtualDisplay.updateMirroredSurface(mTransaction, displayAreaBounds, surfaceSize);
// THEN content in the captured DisplayArea is scaled to fit the surface size.
verify(mTransaction, atLeastOnce()).setMatrix(mirroredSurface, 1.0f / yScale, 0, 0,
1.0f / yScale);
// THEN captured content is positioned in the centre of the output surface.
float scaledWidth = displayAreaBounds.width() / xScale;
float xInset = (surfaceSize.x - scaledWidth) / 2;
verify(mTransaction, atLeastOnce()).setPosition(mirroredSurface, xInset, 0);
mockSession.finishMocking();
}
@Test
public void testVirtualDisplayContent_withoutSurface() {
MockitoSession mockSession = mockitoSession()
.initMocks(this)
.spyStatic(SurfaceControl.class)
.strictness(Strictness.LENIENT)
.startMocking();
// GIVEN MediaProjection has already initialized the WindowToken of the DisplayArea to
// mirror.
final IBinder tokenToMirror = setUpDefaultTaskDisplayAreaWindowToken();
@@ -2392,25 +2297,17 @@ public class DisplayContentTests extends WindowTestsBase {
ContentRecordingSession session = ContentRecordingSession.createDisplaySession(
tokenToMirror);
session.setDisplayId(displayId);
mWm.mContentRecordingController.setContentRecordingSessionLocked(session, mWm);
mWm.setContentRecordingSession(session);
actualDC.updateRecording();
// THEN mirroring is not started, since a null surface indicates the VirtualDisplay is off.
assertThat(actualDC.isCurrentlyRecording()).isFalse();
assertThat(actualDC.getContentRecordingSession()).isEqualTo(session);
display.release();
mockSession.finishMocking();
}
@Test
public void testVirtualDisplayContent_withSurface() {
MockitoSession mockSession = mockitoSession()
.initMocks(this)
.spyStatic(SurfaceControl.class)
.strictness(Strictness.LENIENT)
.startMocking();
// GIVEN MediaProjection has already initialized the WindowToken of the DisplayArea to
// mirror.
final IBinder tokenToMirror = setUpDefaultTaskDisplayAreaWindowToken();
@@ -2438,10 +2335,8 @@ public class DisplayContentTests extends WindowTestsBase {
// THEN mirroring is initiated for the default display's DisplayArea.
assertThat(actualDC.isCurrentlyRecording()).isTrue();
assertThat(actualDC.getContentRecordingSession()).isEqualTo(session);
display.release();
mockSession.finishMocking();
}
private static class MirroringTestToken extends Binder {

View File

@@ -148,6 +148,7 @@ public class SystemServicesTestRule implements TestRule {
private void setUp() {
mMockitoSession = mockitoSession()
.spyStatic(LocalServices.class)
.spyStatic(SurfaceControl.class)
.mockStatic(LockGuard.class)
.mockStatic(Watchdog.class)
.strictness(Strictness.LENIENT)