Merge "Capture the contents of a single task" into tm-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
4b563a2d37
@@ -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;
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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)")
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user