Merge "Capture the contents of a single task" into tm-dev

This commit is contained in:
Naomi Musgrave
2022-03-17 17:03:48 +00:00
committed by Android (Google) Code Review
9 changed files with 341 additions and 122 deletions

View File

@@ -217,6 +217,12 @@ class ContextImpl extends Context {
@UnsupportedAppUsage
private @Nullable ClassLoader mClassLoader;
/**
* The {@link com.android.server.wm.WindowToken} representing this instance if it is
* {@link #CONTEXT_TYPE_WINDOW_CONTEXT} or {@link #CONTEXT_TYPE_SYSTEM_OR_SYSTEM_UI}.
* If the type is {@link #CONTEXT_TYPE_ACTIVITY}, then represents the
* {@link android.window.WindowContainerToken} of the activity.
*/
private final @Nullable IBinder mToken;
private final @NonNull UserHandle mUser;

View File

@@ -7146,8 +7146,9 @@ public abstract class Context {
}
/**
* Returns token if the {@link Context} is a {@link android.app.WindowContext}. Returns
* {@code null} otherwise.
* Returns the {@link IBinder} representing the associated
* {@link com.android.server.wm.WindowToken} if the {@link Context} is a
* {@link android.app.WindowContext}. Returns {@code null} otherwise.
*
* @hide
*/

View File

@@ -66,10 +66,11 @@ public final class ContentRecordingSession implements Parcelable {
private int mContentToRecord = RECORD_CONTENT_DISPLAY;
/**
* The window token of the layer of the hierarchy to record.
* The display content if {@link #getContentToRecord()} is
* {@link RecordContent#RECORD_CONTENT_DISPLAY}, or task if {@link #getContentToRecord()} is
* {@link RecordContent#RECORD_CONTENT_TASK}.
* The token of the layer of the hierarchy to record.
* If {@link #getContentToRecord()} is @link RecordContent#RECORD_CONTENT_DISPLAY}, then
* represents the WindowToken corresponding to the DisplayContent to record.
* If {@link #getContentToRecord()} is {@link RecordContent#RECORD_CONTENT_TASK}, then
* represents the {@link android.window.WindowContainerToken} of the Task to record.
*/
@VisibleForTesting
@Nullable
@@ -192,10 +193,11 @@ public final class ContentRecordingSession implements Parcelable {
}
/**
* The window token of the layer of the hierarchy to record.
* The display content if {@link #getContentToRecord()} is
* {@link RecordContent#RECORD_CONTENT_DISPLAY}, or task if {@link #getContentToRecord()} is
* {@link RecordContent#RECORD_CONTENT_TASK}.
* {The token of the layer of the hierarchy to record.
* If {@link #getContentToRecord()} is @link RecordContent#RECORD_CONTENT_DISPLAY}, then
* represents the WindowToken corresponding to the DisplayContent to record.
* If {@link #getContentToRecord()} is {@link RecordContent#RECORD_CONTENT_TASK}, then
* represents the {@link android.window.WindowContainerToken} of the Task to record.
*/
@DataClass.Generated.Member
public @VisibleForTesting @Nullable IBinder getTokenToRecord() {
@@ -231,10 +233,11 @@ public final class ContentRecordingSession implements Parcelable {
}
/**
* The window token of the layer of the hierarchy to record.
* The display content if {@link #getContentToRecord()} is
* {@link RecordContent#RECORD_CONTENT_DISPLAY}, or task if {@link #getContentToRecord()} is
* {@link RecordContent#RECORD_CONTENT_TASK}.
* {The token of the layer of the hierarchy to record.
* If {@link #getContentToRecord()} is @link RecordContent#RECORD_CONTENT_DISPLAY}, then
* represents the WindowToken corresponding to the DisplayContent to record.
* If {@link #getContentToRecord()} is {@link RecordContent#RECORD_CONTENT_TASK}, then
* represents the {@link android.window.WindowContainerToken} of the Task to record.
*/
@DataClass.Generated.Member
public @NonNull ContentRecordingSession setTokenToRecord(@VisibleForTesting @NonNull IBinder value) {
@@ -390,10 +393,11 @@ public final class ContentRecordingSession implements Parcelable {
}
/**
* The window token of the layer of the hierarchy to record.
* The display content if {@link #getContentToRecord()} is
* {@link RecordContent#RECORD_CONTENT_DISPLAY}, or task if {@link #getContentToRecord()} is
* {@link RecordContent#RECORD_CONTENT_TASK}.
* {The token of the layer of the hierarchy to record.
* If {@link #getContentToRecord()} is @link RecordContent#RECORD_CONTENT_DISPLAY}, then
* represents the WindowToken corresponding to the DisplayContent to record.
* If {@link #getContentToRecord()} is {@link RecordContent#RECORD_CONTENT_TASK}, then
* represents the {@link android.window.WindowContainerToken} of the Task to record.
*/
@DataClass.Generated.Member
public @NonNull Builder setTokenToRecord(@VisibleForTesting @NonNull IBinder value) {
@@ -433,7 +437,7 @@ public final class ContentRecordingSession implements Parcelable {
}
@DataClass.Generated(
time = 1644843382972L,
time = 1645803878639L,
codegenVersion = "1.0.23",
sourceFile = "frameworks/base/core/java/android/view/ContentRecordingSession.java",
inputSignatures = "public static final int RECORD_CONTENT_DISPLAY\npublic static final int RECORD_CONTENT_TASK\nprivate int mDisplayId\nprivate @android.view.ContentRecordingSession.RecordContent int mContentToRecord\nprivate @com.android.internal.annotations.VisibleForTesting @android.annotation.Nullable android.os.IBinder mTokenToRecord\npublic static android.view.ContentRecordingSession createDisplaySession(android.os.IBinder)\npublic static android.view.ContentRecordingSession createTaskSession(android.os.IBinder)\npublic static boolean isValid(android.view.ContentRecordingSession)\npublic static boolean isSameDisplay(android.view.ContentRecordingSession,android.view.ContentRecordingSession)\nclass ContentRecordingSession extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genConstructor=false, genToString=true, genSetters=true, genEqualsHashCode=true)")

View File

@@ -2257,6 +2257,12 @@
"group": "WM_DEBUG_WINDOW_TRANSITIONS",
"at": "com\/android\/server\/wm\/TransitionController.java"
},
"264036181": {
"message": "Unable to retrieve task to start recording for display %d",
"level": "VERBOSE",
"group": "WM_DEBUG_CONTENT_RECORDING",
"at": "com\/android\/server\/wm\/ContentRecorder.java"
},
"269576220": {
"message": "Resuming rotation after drag",
"level": "DEBUG",
@@ -2761,6 +2767,12 @@
"group": "WM_DEBUG_WALLPAPER",
"at": "com\/android\/server\/wm\/WallpaperWindowToken.java"
},
"736003885": {
"message": "Unable to retrieve the task token to start recording for display %d",
"level": "VERBOSE",
"group": "WM_DEBUG_CONTENT_RECORDING",
"at": "com\/android\/server\/wm\/ContentRecorder.java"
},
"736692676": {
"message": "Config is relaunching %s",
"level": "VERBOSE",
@@ -2791,6 +2803,12 @@
"group": "WM_DEBUG_RECENTS_ANIMATIONS",
"at": "com\/android\/server\/wm\/RecentsAnimation.java"
},
"778774915": {
"message": "Unable to record task since feature is disabled %d",
"level": "VERBOSE",
"group": "WM_DEBUG_CONTENT_RECORDING",
"at": "com\/android\/server\/wm\/ContentRecorder.java"
},
"781471998": {
"message": "moveWindowTokenToDisplay: Cannot move to the original display for token: %s",
"level": "WARN",

View File

@@ -17,6 +17,7 @@
package android.media.projection;
import android.media.projection.IMediaProjectionCallback;
import android.window.WindowContainerToken;
/** {@hide} */
interface IMediaProjection {
@@ -28,4 +29,16 @@ interface IMediaProjection {
int applyVirtualDisplayFlags(int flags);
void registerCallback(IMediaProjectionCallback callback);
void unregisterCallback(IMediaProjectionCallback callback);
/**
* Returns the {@link android.window.WindowContainerToken} identifying the task to record, or
* {@code null} if there is none.
*/
WindowContainerToken getTaskRecordingWindowContainerToken();
/**
* Updates the {@link android.window.WindowContainerToken} identifying the task to record, or
* {@code null} if there is none.
*/
void setTaskRecordingWindowContainerToken(in WindowContainerToken token);
}

View File

@@ -25,13 +25,14 @@ import android.hardware.display.DisplayManager;
import android.hardware.display.VirtualDisplay;
import android.hardware.display.VirtualDisplayConfig;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.ArrayMap;
import android.util.Log;
import android.view.ContentRecordingSession;
import android.view.IWindowManager;
import android.view.Surface;
import android.view.WindowManagerGlobal;
import android.window.WindowContainerToken;
import java.util.Map;
@@ -171,16 +172,34 @@ public final class MediaProjection {
@NonNull VirtualDisplayConfig.Builder virtualDisplayConfig,
@Nullable VirtualDisplay.Callback callback, @Nullable Handler handler) {
try {
final Context windowContext = mContext.createWindowContext(
mContext.getDisplayNoVerify(),
TYPE_APPLICATION, null /* options */);
final IBinder windowContextToken = windowContext.getWindowContextToken();
final IWindowManager wmService = WindowManagerGlobal.getWindowManagerService();
final WindowContainerToken taskWindowContainerToken =
mImpl.getTaskRecordingWindowContainerToken();
Context windowContext = null;
ContentRecordingSession session;
if (taskWindowContainerToken == null) {
windowContext = mContext.createWindowContext(mContext.getDisplayNoVerify(),
TYPE_APPLICATION, null /* options */);
session = ContentRecordingSession.createDisplaySession(
windowContext.getWindowContextToken());
} else {
session = ContentRecordingSession.createTaskSession(
taskWindowContainerToken.asBinder());
}
virtualDisplayConfig.setWindowManagerMirroring(true);
final DisplayManager dm = mContext.getSystemService(DisplayManager.class);
final VirtualDisplay virtualDisplay = dm.createVirtualDisplay(this,
virtualDisplayConfig.build(),
callback, handler, windowContext);
setSession(windowContextToken, virtualDisplay);
virtualDisplayConfig.build(), callback, handler, windowContext);
if (virtualDisplay == null) {
// Since WM handling a new display and DM creating a new VirtualDisplay is async,
// WM may have tried to start task recording and encountered an error that required
// stopping recording entirely. The VirtualDisplay would then be null when the
// MediaProjection is no longer active.
return null;
}
session.setDisplayId(virtualDisplay.getDisplay().getDisplayId());
// Successfully set up, so save the current session details.
wmService.setContentRecordingSession(session);
return virtualDisplay;
} catch (RemoteException e) {
// Can not capture if WMS is not accessible, so bail out.
@@ -188,28 +207,6 @@ public final class MediaProjection {
}
}
/**
* Updates the {@link ContentRecordingSession} describing the recording taking place on this
* {@link VirtualDisplay}.
*
* @throws RemoteException if updating the session on the server failed.
*/
private void setSession(@NonNull IBinder windowContextToken,
@Nullable VirtualDisplay virtualDisplay)
throws RemoteException {
if (virtualDisplay == null) {
// Not able to set up a new VirtualDisplay.
return;
}
// Identify the VirtualDisplay that will be hosting the recording.
ContentRecordingSession session = ContentRecordingSession.createDisplaySession(
windowContextToken);
session.setDisplayId(virtualDisplay.getDisplay().getDisplayId());
// TODO(b/216625226) handle task recording.
// Successfully set up, so save the current session details.
WindowManagerGlobal.getWindowManagerService().setContentRecordingSession(session);
}
/**
* Stops projection.
*/

View File

@@ -45,6 +45,7 @@ import android.os.RemoteException;
import android.os.UserHandle;
import android.util.ArrayMap;
import android.util.Slog;
import android.window.WindowContainerToken;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.DumpUtils;
@@ -410,6 +411,7 @@ public final class MediaProjectionManagerService extends SystemService
private IBinder mToken;
private IBinder.DeathRecipient mDeathEater;
private boolean mRestoreSystemAlertWindow;
private WindowContainerToken mTaskRecordingWindowContainerToken = null;
MediaProjection(int type, int uid, String packageName, int targetSdkVersion,
boolean isPrivileged) {
@@ -568,7 +570,7 @@ public final class MediaProjectionManagerService extends SystemService
}
}
@Override
@Override // Binder call
public void registerCallback(IMediaProjectionCallback callback) {
if (callback == null) {
throw new IllegalArgumentException("callback must not be null");
@@ -576,7 +578,7 @@ public final class MediaProjectionManagerService extends SystemService
mCallbackDelegate.add(callback);
}
@Override
@Override // Binder call
public void unregisterCallback(IMediaProjectionCallback callback) {
if (callback == null) {
throw new IllegalArgumentException("callback must not be null");
@@ -584,6 +586,17 @@ public final class MediaProjectionManagerService extends SystemService
mCallbackDelegate.remove(callback);
}
@Override // Binder call
public void setTaskRecordingWindowContainerToken(WindowContainerToken token) {
// TODO(b/221417940) set the task id to record from sysui, for the package chosen.
mTaskRecordingWindowContainerToken = token;
}
@Override // Binder call
public WindowContainerToken getTaskRecordingWindowContainerToken() {
return mTaskRecordingWindowContainerToken;
}
public MediaProjectionInfo getProjectionInfo() {
return new MediaProjectionInfo(packageName, userHandle);
}

View File

@@ -17,6 +17,7 @@
package com.android.server.wm;
import static android.view.ContentRecordingSession.RECORD_CONTENT_DISPLAY;
import static android.view.ContentRecordingSession.RECORD_CONTENT_TASK;
import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_CONTENT_RECORDING;
@@ -26,6 +27,7 @@ import android.content.res.Configuration;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.IBinder;
import android.provider.DeviceConfig;
import android.view.ContentRecordingSession;
import android.view.Display;
import android.view.SurfaceControl;
@@ -38,6 +40,11 @@ import com.android.internal.protolog.common.ProtoLog;
*/
final class ContentRecorder {
/**
* The key for accessing the device config that controls if task recording is supported.
*/
@VisibleForTesting static final String KEY_RECORD_TASK_FEATURE = "record_task_content";
/**
* The display content this class is handling recording for.
*/
@@ -48,7 +55,7 @@ final class ContentRecorder {
* The session for content recording, or null if this DisplayContent is not being used for
* recording.
*/
@VisibleForTesting private ContentRecordingSession mContentRecordingSession = null;
private ContentRecordingSession mContentRecordingSession = null;
/**
* The WindowContainer for the level of the hierarchy to record.
@@ -187,6 +194,8 @@ final class ContentRecorder {
mDisplayContent.mWmService.mTransactionFactory.get().remove(mRecordedSurface).apply();
mRecordedSurface = null;
clearContentRecordingSession();
// Do not need to force remove the VirtualDisplay; this is handled by the media
// projection service.
}
}
@@ -215,46 +224,12 @@ final class ContentRecorder {
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());
mRecordedWindowContainer = retrieveRecordedWindowContainer();
if (mRecordedWindowContainer == null) {
// Either the token is missing, or the window associated with the token is missing.
// Error has already been handled, so just leave.
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) {
@@ -295,6 +270,107 @@ final class ContentRecorder {
// when the VirtualDisplay is destroyed - which will clean up this DisplayContent.
}
/**
* Retrieves the {@link WindowContainer} for the level of the hierarchy to start recording,
* indicated by the {@link #mContentRecordingSession}. Performs any error handling and state
* updates necessary if the {@link WindowContainer} could not be retrieved.
* {@link #mContentRecordingSession} must be non-null.
*
* @return a {@link WindowContainer} to record, or {@code null} if an error was encountered. The
* error is logged and any cleanup is handled.
*/
@Nullable
private WindowContainer retrieveRecordedWindowContainer() {
final int contentToRecord = mContentRecordingSession.getContentToRecord();
// Given the WindowToken of the region to record, retrieve the associated
// SurfaceControl.
final IBinder tokenToRecord = mContentRecordingSession.getTokenToRecord();
if (tokenToRecord == null) {
handleStartRecordingFailed();
ProtoLog.v(WM_DEBUG_CONTENT_RECORDING,
"Unable to start recording due to null token for display %d",
mDisplayContent.getDisplayId());
return null;
}
switch (contentToRecord) {
case RECORD_CONTENT_DISPLAY:
final WindowContainer wc =
mDisplayContent.mWmService.mWindowContextListenerController.getContainer(
tokenToRecord);
if (wc == null) {
// Un-set the window token to record for this VirtualDisplay. Fall back to
// Display stack capture for the entire display.
mDisplayContent.mWmService.mDisplayManagerInternal.setWindowManagerMirroring(
mDisplayContent.getDisplayId(), false);
handleStartRecordingFailed();
ProtoLog.v(WM_DEBUG_CONTENT_RECORDING,
"Unable to retrieve window container to start recording for "
+ "display %d", mDisplayContent.getDisplayId());
return null;
}
// TODO(206461622) Migrate to using the RootDisplayArea
return wc.getDisplayContent();
case RECORD_CONTENT_TASK:
if (!DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_WINDOW_MANAGER,
KEY_RECORD_TASK_FEATURE, false)) {
handleStartRecordingFailed();
ProtoLog.v(WM_DEBUG_CONTENT_RECORDING,
"Unable to record task since feature is disabled %d",
mDisplayContent.getDisplayId());
return null;
}
Task taskToRecord = WindowContainer.fromBinder(tokenToRecord).asTask();
if (taskToRecord == null) {
handleStartRecordingFailed();
ProtoLog.v(WM_DEBUG_CONTENT_RECORDING,
"Unable to retrieve task to start recording for "
+ "display %d", mDisplayContent.getDisplayId());
}
return taskToRecord;
default:
// Not a valid region, or recording is disabled, so fall back to Display stack
// capture for the entire display.
handleStartRecordingFailed();
ProtoLog.v(WM_DEBUG_CONTENT_RECORDING,
"Unable to start recording due to invalid region for display %d",
mDisplayContent.getDisplayId());
return null;
}
}
/**
* Exit this recording session.
* <p>
* If this is a task session, tear down the recording entirely. Do not fall back
* to recording the entire display on the display stack; this would surprise the user
* given they selected task capture.
* </p><p>
* If this is a display session, just stop recording by layer mirroring. Fall back to recording
* from the display stack.
* </p>
*/
private void handleStartRecordingFailed() {
final boolean shouldExitTaskRecording = mContentRecordingSession != null
&& mContentRecordingSession.getContentToRecord() == RECORD_CONTENT_TASK;
if (shouldExitTaskRecording) {
// Clean up the cached session first, since tearing down the display will generate
// display
// events which will trickle back to here.
clearContentRecordingSession();
tearDownVirtualDisplay();
} else {
clearContentRecordingSession();
}
}
/**
* Ensure recording does not fall back to the display stack; ensure the recording is stopped
* and the client notified by tearing down the virtual display.
*/
private void tearDownVirtualDisplay() {
// TODO(b/219761722) Clean up the VirtualDisplay if task mirroring fails
}
/**
* Apply transformations to the mirrored surface to ensure the captured contents are scaled to
* fit and centred in the output surface.

View File

@@ -19,12 +19,12 @@ 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.android.server.wm.ContentRecorder.KEY_RECORD_TASK_FEATURE;
import static com.google.common.truth.Truth.assertThat;
@@ -40,17 +40,22 @@ import android.hardware.display.VirtualDisplay;
import android.os.Binder;
import android.os.IBinder;
import android.platform.test.annotations.Presubmit;
import android.provider.DeviceConfig;
import android.util.DisplayMetrics;
import android.view.ContentRecordingSession;
import android.view.Surface;
import android.view.SurfaceControl;
import androidx.annotation.NonNull;
import androidx.test.filters.SmallTest;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.concurrent.CountDownLatch;
/**
* Tests for the {@link ContentRecorder} class.
*
@@ -62,17 +67,18 @@ import org.junit.runner.RunWith;
@RunWith(WindowTestRunner.class)
public class ContentRecorderTests extends WindowTestsBase {
private static final IBinder TEST_TOKEN = new RecordingTestToken();
private final ContentRecordingSession mDefaultSession =
private static IBinder sTaskWindowContainerToken;
private final ContentRecordingSession mDisplaySession =
ContentRecordingSession.createDisplaySession(TEST_TOKEN);
private ContentRecordingSession mTaskSession;
private static Point sSurfaceSize;
private ContentRecorder mContentRecorder;
private SurfaceControl mRecordedSurface;
// Handle feature flag.
private ConfigListener mConfigListener;
private CountDownLatch mLatch;
@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(),
@@ -84,12 +90,32 @@ public class ContentRecorderTests extends WindowTestsBase {
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);
final DisplayContent virtualDisplayContent = mWm.mRoot.getDisplayContent(displayId);
mContentRecorder = new ContentRecorder(virtualDisplayContent);
spyOn(virtualDisplayContent);
// GIVEN MediaProjection has already initialized the WindowToken of the DisplayArea to
// record.
setUpDefaultTaskDisplayAreaWindowToken();
mDisplaySession.setDisplayId(displayId);
// GIVEN there is a window token associated with a task to record.
sTaskWindowContainerToken = setUpTaskWindowContainerToken(virtualDisplayContent);
mTaskSession = ContentRecordingSession.createTaskSession(sTaskWindowContainerToken);
mTaskSession.setDisplayId(displayId);
mConfigListener = new ConfigListener();
DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_WINDOW_MANAGER,
mContext.getMainExecutor(), mConfigListener);
mLatch = new CountDownLatch(1);
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_WINDOW_MANAGER, KEY_RECORD_TASK_FEATURE,
"true", true);
}
@After
public void teardown() {
DeviceConfig.removeOnPropertiesChangedListener(mConfigListener);
}
@Test
@@ -102,22 +128,74 @@ public class ContentRecorderTests extends WindowTestsBase {
@Test
public void testUpdateRecording_display() {
mContentRecorder.setContentRecordingSession(mDefaultSession);
mContentRecorder.setContentRecordingSession(mDisplaySession);
mContentRecorder.updateRecording();
assertThat(mContentRecorder.isCurrentlyRecording()).isTrue();
}
@Test
public void testUpdateRecording_task() {
mDefaultSession.setContentToRecord(RECORD_CONTENT_TASK);
mContentRecorder.setContentRecordingSession(mDefaultSession);
public void testUpdateRecording_display_nullToken() {
ContentRecordingSession session = ContentRecordingSession.createDisplaySession(TEST_TOKEN);
session.setDisplayId(mDisplaySession.getDisplayId());
session.setTokenToRecord(null);
mContentRecorder.setContentRecordingSession(session);
mContentRecorder.updateRecording();
assertThat(mContentRecorder.isCurrentlyRecording()).isFalse();
}
@Test
public void testUpdateRecording_display_noWindowContainer() {
doReturn(null).when(
mWm.mWindowContextListenerController).getContainer(any());
mContentRecorder.setContentRecordingSession(mDisplaySession);
mContentRecorder.updateRecording();
assertThat(mContentRecorder.isCurrentlyRecording()).isFalse();
}
@Test
public void testUpdateRecording_task_featureDisabled() {
mLatch = new CountDownLatch(1);
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_WINDOW_MANAGER, KEY_RECORD_TASK_FEATURE,
"false", false);
mContentRecorder.setContentRecordingSession(mTaskSession);
mContentRecorder.updateRecording();
assertThat(mContentRecorder.isCurrentlyRecording()).isFalse();
}
@Test
public void testUpdateRecording_task_featureEnabled() {
// Feature already enabled; don't need to again.
mContentRecorder.setContentRecordingSession(mTaskSession);
mContentRecorder.updateRecording();
assertThat(mContentRecorder.isCurrentlyRecording()).isTrue();
}
@Test
public void testUpdateRecording_task_nullToken() {
ContentRecordingSession session = ContentRecordingSession.createTaskSession(
sTaskWindowContainerToken);
session.setDisplayId(mDisplaySession.getDisplayId());
session.setTokenToRecord(null);
mContentRecorder.setContentRecordingSession(session);
mContentRecorder.updateRecording();
assertThat(mContentRecorder.isCurrentlyRecording()).isFalse();
// TODO(b/219761722) validate VirtualDisplay is torn down when can't set up task recording.
}
@Test
public void testUpdateRecording_task_noWindowContainer() {
// Use the window container token of the DisplayContent, rather than task.
ContentRecordingSession invalidTaskSession = ContentRecordingSession.createTaskSession(
new WindowContainer.RemoteToken(mDisplayContent));
mContentRecorder.setContentRecordingSession(invalidTaskSession);
mContentRecorder.updateRecording();
assertThat(mContentRecorder.isCurrentlyRecording()).isFalse();
// TODO(b/219761722) validate VirtualDisplay is torn down when can't set up task recording.
}
@Test
public void testUpdateRecording_wasPaused() {
mContentRecorder.setContentRecordingSession(mDefaultSession);
mContentRecorder.setContentRecordingSession(mDisplaySession);
mContentRecorder.updateRecording();
mContentRecorder.pauseRecording();
@@ -125,16 +203,6 @@ public class ContentRecorderTests extends WindowTestsBase {
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);
@@ -146,7 +214,7 @@ public class ContentRecorderTests extends WindowTestsBase {
@Test
public void testOnConfigurationChanged_resizesSurface() {
mContentRecorder.setContentRecordingSession(mDefaultSession);
mContentRecorder.setContentRecordingSession(mDisplaySession);
mContentRecorder.updateRecording();
mContentRecorder.onConfigurationChanged(ORIENTATION_PORTRAIT);
@@ -158,7 +226,7 @@ public class ContentRecorderTests extends WindowTestsBase {
@Test
public void testPauseRecording_pausesRecording() {
mContentRecorder.setContentRecordingSession(mDefaultSession);
mContentRecorder.setContentRecordingSession(mDisplaySession);
mContentRecorder.updateRecording();
mContentRecorder.pauseRecording();
@@ -173,7 +241,7 @@ public class ContentRecorderTests extends WindowTestsBase {
@Test
public void testRemove_stopsRecording() {
mContentRecorder.setContentRecordingSession(mDefaultSession);
mContentRecorder.setContentRecordingSession(mDisplaySession);
mContentRecorder.updateRecording();
mContentRecorder.remove();
@@ -188,8 +256,9 @@ public class ContentRecorderTests extends WindowTestsBase {
@Test
public void testUpdateMirroredSurface_capturedAreaResized() {
mContentRecorder.setContentRecordingSession(mDefaultSession);
mContentRecorder.setContentRecordingSession(mDisplaySession);
mContentRecorder.updateRecording();
assertThat(mContentRecorder.isCurrentlyRecording()).isTrue();
// WHEN attempting to mirror on the virtual display, and the captured content is resized.
float xScale = 0.7f;
@@ -197,13 +266,14 @@ public class ContentRecorderTests extends WindowTestsBase {
Rect displayAreaBounds = new Rect(0, 0, Math.round(sSurfaceSize.x * xScale),
Math.round(sSurfaceSize.y * yScale));
mContentRecorder.updateMirroredSurface(mTransaction, displayAreaBounds, sSurfaceSize);
assertThat(mContentRecorder.isCurrentlyRecording()).isTrue();
// 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;
int scaledWidth = Math.round((float) displayAreaBounds.width() / xScale);
int xInset = (sSurfaceSize.x - scaledWidth) / 2;
verify(mTransaction, atLeastOnce()).setPosition(mRecordedSurface, xInset, 0);
}
@@ -221,6 +291,18 @@ public class ContentRecorderTests extends WindowTestsBase {
mWm.mWindowContextListenerController).getContainer(any());
}
/**
* Creates a {@link android.window.WindowContainerToken} associated with a task, in order for
* that task to be recorded.
*/
private IBinder setUpTaskWindowContainerToken(DisplayContent displayContent) {
final Task rootTask = createTask(displayContent);
final Task task = createTaskInRootTask(rootTask, 0 /* userId */);
// Ensure the task is not empty.
createActivityRecord(displayContent, task);
return task.getTaskInfo().token.asBinder();
}
/**
* SurfaceControl successfully creates a mirrored surface of the given size.
*/
@@ -236,4 +318,13 @@ public class ContentRecorderTests extends WindowTestsBase {
anyInt());
return mirroredSurface;
}
private class ConfigListener implements DeviceConfig.OnPropertiesChangedListener {
@Override
public void onPropertiesChanged(@NonNull DeviceConfig.Properties properties) {
if (mLatch != null && properties.getKeyset().contains(KEY_RECORD_TASK_FEATURE)) {
mLatch.countDown();
}
}
}
}