From 13194c3ca43f3736216f9e39e420306bfa409e44 Mon Sep 17 00:00:00 2001 From: Naomi Musgrave Date: Fri, 25 Feb 2022 15:59:13 +0000 Subject: [PATCH] Capture the contents of a single task Introduces layer mirorring of a single task, given the task is both identified (future work - dependency on setup flow in SysUI) and the flag is enabled (currently disabled). Error handling (tearing down the VirtualDisplay if task recording setup failed) will come in a future change Bug: 216625226 Test: atest WmTests:DisplayContentTests Test: atest WmTests:ContentRecorderTests Change-Id: I16abd55ce75537ff8fbbb379e4970668ea02a230 --- core/java/android/app/ContextImpl.java | 6 + core/java/android/content/Context.java | 5 +- .../android/view/ContentRecordingSession.java | 38 +++-- data/etc/services.core.protolog.json | 24 ++- .../media/projection/IMediaProjection.aidl | 13 ++ .../media/projection/MediaProjection.java | 57 +++---- .../MediaProjectionManagerService.java | 17 +- .../android/server/wm/ContentRecorder.java | 154 ++++++++++++----- .../server/wm/ContentRecorderTests.java | 155 ++++++++++++++---- 9 files changed, 341 insertions(+), 128 deletions(-) diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index a3dd705a7e298..802084ed320e2 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -216,6 +216,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; diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 2bda020192d5b..f7244c7b82136 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -7145,8 +7145,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 */ diff --git a/core/java/android/view/ContentRecordingSession.java b/core/java/android/view/ContentRecordingSession.java index db4ec1155e644..c66c70af0656a 100644 --- a/core/java/android/view/ContentRecordingSession.java +++ b/core/java/android/view/ContentRecordingSession.java @@ -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)") diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index df2b2a395ea70..e9ae2586edcbd 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -2251,6 +2251,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", @@ -2671,12 +2677,6 @@ "group": "WM_ERROR", "at": "com\/android\/server\/wm\/WindowManagerService.java" }, - "664667685": { - "message": "Activity %s: enableOnBackInvokedCallback=false. Returning null BackNavigationInfo.", - "level": "DEBUG", - "group": "WM_DEBUG_BACK_PREVIEW", - "at": "com\/android\/server\/wm\/BackNavigationController.java" - }, "665256544": { "message": "All windows drawn!", "level": "DEBUG", @@ -2755,6 +2755,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", @@ -2785,6 +2791,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", diff --git a/media/java/android/media/projection/IMediaProjection.aidl b/media/java/android/media/projection/IMediaProjection.aidl index 19fc0521d7aa8..b136d5bc4db3f 100644 --- a/media/java/android/media/projection/IMediaProjection.aidl +++ b/media/java/android/media/projection/IMediaProjection.aidl @@ -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); } diff --git a/media/java/android/media/projection/MediaProjection.java b/media/java/android/media/projection/MediaProjection.java index 4dde5e8d39a25..b5f95938f8458 100644 --- a/media/java/android/media/projection/MediaProjection.java +++ b/media/java/android/media/projection/MediaProjection.java @@ -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. */ diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java index 3ce8e46597371..1937852fa3336 100644 --- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java +++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java @@ -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); } diff --git a/services/core/java/com/android/server/wm/ContentRecorder.java b/services/core/java/com/android/server/wm/ContentRecorder.java index 07a0c372778b8..87523f44d4b67 100644 --- a/services/core/java/com/android/server/wm/ContentRecorder.java +++ b/services/core/java/com/android/server/wm/ContentRecorder.java @@ -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. + *

+ * 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. + *

+ * If this is a display session, just stop recording by layer mirroring. Fall back to recording + * from the display stack. + *

+ */ + 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. diff --git a/services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java b/services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java index 50eefa066a453..c5117bb83976d 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java @@ -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(); + } + } + } }