diff options
21 files changed, 1305 insertions, 322 deletions
diff --git a/core/java/android/hardware/display/DisplayManagerInternal.java b/core/java/android/hardware/display/DisplayManagerInternal.java index 83e1061d8143..be482c9146c9 100644 --- a/core/java/android/hardware/display/DisplayManagerInternal.java +++ b/core/java/android/hardware/display/DisplayManagerInternal.java @@ -21,7 +21,6 @@ import android.annotation.Nullable; import android.graphics.Point; import android.hardware.SensorManager; import android.os.Handler; -import android.os.IBinder; import android.os.PowerManager; import android.util.IntArray; import android.util.Slog; @@ -351,24 +350,17 @@ public abstract class DisplayManagerInternal { public abstract List<RefreshRateLimitation> getRefreshRateLimitations(int displayId); /** - * Returns the window token of the level of the WindowManager hierarchy to mirror. Returns null - * if layer mirroring by SurfaceFlinger should not be performed for the given displayId. - * For now, only used for mirroring started from MediaProjection. - */ - public abstract IBinder getWindowTokenClientToMirror(int displayId); - - /** - * For the given displayId, updates the window token of the level of the WindowManager hierarchy - * to mirror. If windowToken is null, then SurfaceFlinger performs no layer mirroring to the + * For the given displayId, updates if WindowManager is responsible for mirroring on that + * display. If {@code false}, then SurfaceFlinger performs no layer mirroring to the * given display. - * For now, only used for mirroring started from MediaProjection. + * Only used for mirroring started from MediaProjection. */ - public abstract void setWindowTokenClientToMirror(int displayId, IBinder windowToken); + public abstract void setWindowManagerMirroring(int displayId, boolean isMirroring); /** * Returns the default size of the surface associated with the display, or null if the surface * is not provided for layer mirroring by SurfaceFlinger. - * For now, only used for mirroring started from MediaProjection. + * Only used for mirroring started from MediaProjection. */ public abstract Point getDisplaySurfaceDefaultSize(int displayId); diff --git a/core/java/android/hardware/display/VirtualDisplayConfig.java b/core/java/android/hardware/display/VirtualDisplayConfig.java index e292394c36ff..b76b98d3c37a 100644 --- a/core/java/android/hardware/display/VirtualDisplayConfig.java +++ b/core/java/android/hardware/display/VirtualDisplayConfig.java @@ -24,7 +24,6 @@ import android.annotation.Nullable; import android.hardware.display.DisplayManager.VirtualDisplayFlag; import android.media.projection.MediaProjection; import android.os.Handler; -import android.os.IBinder; import android.os.Parcel; import android.os.Parcelable; import android.view.Surface; @@ -95,11 +94,10 @@ public final class VirtualDisplayConfig implements Parcelable { private int mDisplayIdToMirror = DEFAULT_DISPLAY; /** - * The window token of the level of the WindowManager hierarchy to mirror, or null if mirroring - * should not be performed. + * Indicates if WindowManager is responsible for mirroring content to this VirtualDisplay, or + * if DisplayManager should record contents instead. */ - @Nullable - private IBinder mWindowTokenClientToMirror = null; + private boolean mWindowManagerMirroring = false; @@ -126,7 +124,7 @@ public final class VirtualDisplayConfig implements Parcelable { @Nullable Surface surface, @Nullable String uniqueId, int displayIdToMirror, - @Nullable IBinder windowTokenClientToMirror) { + boolean windowManagerMirroring) { this.mName = name; com.android.internal.util.AnnotationValidations.validate( NonNull.class, null, mName); @@ -148,7 +146,7 @@ public final class VirtualDisplayConfig implements Parcelable { this.mSurface = surface; this.mUniqueId = uniqueId; this.mDisplayIdToMirror = displayIdToMirror; - this.mWindowTokenClientToMirror = windowTokenClientToMirror; + this.mWindowManagerMirroring = windowManagerMirroring; // onConstructed(); // You can define this method to get a callback } @@ -227,12 +225,12 @@ public final class VirtualDisplayConfig implements Parcelable { } /** - * The window token of the level of the WindowManager hierarchy to mirror, or null if mirroring - * should not be performed. + * Indicates if WindowManager is responsible for mirroring content to this VirtualDisplay, or + * if DisplayManager should record contents instead. */ @DataClass.Generated.Member - public @Nullable IBinder getWindowTokenClientToMirror() { - return mWindowTokenClientToMirror; + public boolean isWindowManagerMirroring() { + return mWindowManagerMirroring; } @Override @@ -242,9 +240,9 @@ public final class VirtualDisplayConfig implements Parcelable { // void parcelFieldName(Parcel dest, int flags) { ... } int flg = 0; + if (mWindowManagerMirroring) flg |= 0x100; if (mSurface != null) flg |= 0x20; if (mUniqueId != null) flg |= 0x40; - if (mWindowTokenClientToMirror != null) flg |= 0x100; dest.writeInt(flg); dest.writeString(mName); dest.writeInt(mWidth); @@ -254,7 +252,6 @@ public final class VirtualDisplayConfig implements Parcelable { if (mSurface != null) dest.writeTypedObject(mSurface, flags); if (mUniqueId != null) dest.writeString(mUniqueId); dest.writeInt(mDisplayIdToMirror); - if (mWindowTokenClientToMirror != null) dest.writeStrongBinder(mWindowTokenClientToMirror); } @Override @@ -269,6 +266,7 @@ public final class VirtualDisplayConfig implements Parcelable { // static FieldType unparcelFieldName(Parcel in) { ... } int flg = in.readInt(); + boolean windowManagerMirroring = (flg & 0x100) != 0; String name = in.readString(); int width = in.readInt(); int height = in.readInt(); @@ -277,7 +275,6 @@ public final class VirtualDisplayConfig implements Parcelable { Surface surface = (flg & 0x20) == 0 ? null : (Surface) in.readTypedObject(Surface.CREATOR); String uniqueId = (flg & 0x40) == 0 ? null : in.readString(); int displayIdToMirror = in.readInt(); - IBinder windowTokenClientToMirror = (flg & 0x100) == 0 ? null : (IBinder) in.readStrongBinder(); this.mName = name; com.android.internal.util.AnnotationValidations.validate( @@ -300,7 +297,7 @@ public final class VirtualDisplayConfig implements Parcelable { this.mSurface = surface; this.mUniqueId = uniqueId; this.mDisplayIdToMirror = displayIdToMirror; - this.mWindowTokenClientToMirror = windowTokenClientToMirror; + this.mWindowManagerMirroring = windowManagerMirroring; // onConstructed(); // You can define this method to get a callback } @@ -334,7 +331,7 @@ public final class VirtualDisplayConfig implements Parcelable { private @Nullable Surface mSurface; private @Nullable String mUniqueId; private int mDisplayIdToMirror; - private @Nullable IBinder mWindowTokenClientToMirror; + private boolean mWindowManagerMirroring; private long mBuilderFieldsSet = 0L; @@ -470,14 +467,14 @@ public final class VirtualDisplayConfig implements Parcelable { } /** - * The window token of the level of the WindowManager hierarchy to mirror, or null if mirroring - * should not be performed. + * Indicates if WindowManager is responsible for mirroring content to this VirtualDisplay, or + * if DisplayManager should record contents instead. */ @DataClass.Generated.Member - public @NonNull Builder setWindowTokenClientToMirror(@NonNull IBinder value) { + public @NonNull Builder setWindowManagerMirroring(boolean value) { checkNotUsed(); mBuilderFieldsSet |= 0x100; - mWindowTokenClientToMirror = value; + mWindowManagerMirroring = value; return this; } @@ -499,7 +496,7 @@ public final class VirtualDisplayConfig implements Parcelable { mDisplayIdToMirror = DEFAULT_DISPLAY; } if ((mBuilderFieldsSet & 0x100) == 0) { - mWindowTokenClientToMirror = null; + mWindowManagerMirroring = false; } VirtualDisplayConfig o = new VirtualDisplayConfig( mName, @@ -510,7 +507,7 @@ public final class VirtualDisplayConfig implements Parcelable { mSurface, mUniqueId, mDisplayIdToMirror, - mWindowTokenClientToMirror); + mWindowManagerMirroring); return o; } @@ -523,10 +520,10 @@ public final class VirtualDisplayConfig implements Parcelable { } @DataClass.Generated( - time = 1643938791506L, + time = 1646227247934L, codegenVersion = "1.0.23", sourceFile = "frameworks/base/core/java/android/hardware/display/VirtualDisplayConfig.java", - inputSignatures = "private @android.annotation.NonNull java.lang.String mName\nprivate @android.annotation.IntRange int mWidth\nprivate @android.annotation.IntRange int mHeight\nprivate @android.annotation.IntRange int mDensityDpi\nprivate @android.hardware.display.DisplayManager.VirtualDisplayFlag int mFlags\nprivate @android.annotation.Nullable android.view.Surface mSurface\nprivate @android.annotation.Nullable java.lang.String mUniqueId\nprivate int mDisplayIdToMirror\nprivate @android.annotation.Nullable android.os.IBinder mWindowTokenClientToMirror\nclass VirtualDisplayConfig extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genParcelable=true, genAidl=true, genBuilder=true)") + inputSignatures = "private @android.annotation.NonNull java.lang.String mName\nprivate @android.annotation.IntRange int mWidth\nprivate @android.annotation.IntRange int mHeight\nprivate @android.annotation.IntRange int mDensityDpi\nprivate @android.hardware.display.DisplayManager.VirtualDisplayFlag int mFlags\nprivate @android.annotation.Nullable android.view.Surface mSurface\nprivate @android.annotation.Nullable java.lang.String mUniqueId\nprivate int mDisplayIdToMirror\nprivate boolean mWindowManagerMirroring\nclass VirtualDisplayConfig extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genParcelable=true, genAidl=true, genBuilder=true)") @Deprecated private void __metadata() {} diff --git a/core/java/android/view/ContentRecordingSession.aidl b/core/java/android/view/ContentRecordingSession.aidl new file mode 100644 index 000000000000..ddbc175ca7ae --- /dev/null +++ b/core/java/android/view/ContentRecordingSession.aidl @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2022, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +parcelable ContentRecordingSession; diff --git a/core/java/android/view/ContentRecordingSession.java b/core/java/android/view/ContentRecordingSession.java new file mode 100644 index 000000000000..db4ec1155e64 --- /dev/null +++ b/core/java/android/view/ContentRecordingSession.java @@ -0,0 +1,447 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import static android.view.Display.INVALID_DISPLAY; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.IBinder; +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.DataClass; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Description of a content recording session. + * + * @hide + */ +@DataClass( + genConstructor = false, + genToString = true, + genSetters = true, + genEqualsHashCode = true +) +public final class ContentRecordingSession implements Parcelable { + + /** + * An entire DisplayContent is being recorded. Recording may also be paused. + */ + public static final int RECORD_CONTENT_DISPLAY = 0; + /** + * A single Task is being recorded. Recording may also be paused. + */ + public static final int RECORD_CONTENT_TASK = 1; + + /** + * Unique logical identifier of the {@link android.hardware.display.VirtualDisplay} that has + * recorded content rendered to its surface. + */ + private int mDisplayId = INVALID_DISPLAY; + + /** + * The content to record. + */ + @RecordContent + 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}. + */ + @VisibleForTesting + @Nullable + private IBinder mTokenToRecord = null; + + /** + * Default instance, with recording the display. + */ + private ContentRecordingSession() { + } + + /** + * Returns an instance initialized for display recording. + */ + public static ContentRecordingSession createDisplaySession( + @NonNull IBinder displayContentWindowToken) { + return new ContentRecordingSession().setContentToRecord(RECORD_CONTENT_DISPLAY) + .setTokenToRecord(displayContentWindowToken); + } + + /** + * Returns an instance initialized for task recording. + */ + public static ContentRecordingSession createTaskSession( + @NonNull IBinder taskWindowContainerToken) { + return new ContentRecordingSession().setContentToRecord(RECORD_CONTENT_TASK) + .setTokenToRecord(taskWindowContainerToken); + } + + /** + * Returns {@code true} if this is a valid session. + */ + public static boolean isValid(ContentRecordingSession session) { + return session != null && (session.getDisplayId() > INVALID_DISPLAY + && session.getTokenToRecord() != null); + } + + /** + * Returns {@code true} when both sessions are for the same display. + */ + public static boolean isSameDisplay(ContentRecordingSession session, + ContentRecordingSession incomingSession) { + return session != null && incomingSession != null + && session.getDisplayId() == incomingSession.getDisplayId(); + } + + + + + // Code below generated by codegen v1.0.23. + // + // DO NOT MODIFY! + // CHECKSTYLE:OFF Generated code + // + // To regenerate run: + // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/view/ContentRecordingSession.java + // + // To exclude the generated code from IntelliJ auto-formatting enable (one-time): + // Settings > Editor > Code Style > Formatter Control + //@formatter:off + + + @IntDef(prefix = "RECORD_CONTENT_", value = { + RECORD_CONTENT_DISPLAY, + RECORD_CONTENT_TASK + }) + @Retention(RetentionPolicy.SOURCE) + @DataClass.Generated.Member + public @interface RecordContent {} + + @DataClass.Generated.Member + public static String recordContentToString(@RecordContent int value) { + switch (value) { + case RECORD_CONTENT_DISPLAY: + return "RECORD_CONTENT_DISPLAY"; + case RECORD_CONTENT_TASK: + return "RECORD_CONTENT_TASK"; + default: return Integer.toHexString(value); + } + } + + @DataClass.Generated.Member + /* package-private */ ContentRecordingSession( + int displayId, + @RecordContent int contentToRecord, + @VisibleForTesting @Nullable IBinder tokenToRecord) { + this.mDisplayId = displayId; + this.mContentToRecord = contentToRecord; + + if (!(mContentToRecord == RECORD_CONTENT_DISPLAY) + && !(mContentToRecord == RECORD_CONTENT_TASK)) { + throw new java.lang.IllegalArgumentException( + "contentToRecord was " + mContentToRecord + " but must be one of: " + + "RECORD_CONTENT_DISPLAY(" + RECORD_CONTENT_DISPLAY + "), " + + "RECORD_CONTENT_TASK(" + RECORD_CONTENT_TASK + ")"); + } + + this.mTokenToRecord = tokenToRecord; + com.android.internal.util.AnnotationValidations.validate( + VisibleForTesting.class, null, mTokenToRecord); + + // onConstructed(); // You can define this method to get a callback + } + + /** + * Unique logical identifier of the {@link android.hardware.display.VirtualDisplay} that has + * recorded content rendered to its surface. + */ + @DataClass.Generated.Member + public int getDisplayId() { + return mDisplayId; + } + + /** + * The content to record. + */ + @DataClass.Generated.Member + public @RecordContent int getContentToRecord() { + return mContentToRecord; + } + + /** + * 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}. + */ + @DataClass.Generated.Member + public @VisibleForTesting @Nullable IBinder getTokenToRecord() { + return mTokenToRecord; + } + + /** + * Unique logical identifier of the {@link android.hardware.display.VirtualDisplay} that has + * recorded content rendered to its surface. + */ + @DataClass.Generated.Member + public @NonNull ContentRecordingSession setDisplayId( int value) { + mDisplayId = value; + return this; + } + + /** + * The content to record. + */ + @DataClass.Generated.Member + public @NonNull ContentRecordingSession setContentToRecord(@RecordContent int value) { + mContentToRecord = value; + + if (!(mContentToRecord == RECORD_CONTENT_DISPLAY) + && !(mContentToRecord == RECORD_CONTENT_TASK)) { + throw new java.lang.IllegalArgumentException( + "contentToRecord was " + mContentToRecord + " but must be one of: " + + "RECORD_CONTENT_DISPLAY(" + RECORD_CONTENT_DISPLAY + "), " + + "RECORD_CONTENT_TASK(" + RECORD_CONTENT_TASK + ")"); + } + + return this; + } + + /** + * 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}. + */ + @DataClass.Generated.Member + public @NonNull ContentRecordingSession setTokenToRecord(@VisibleForTesting @NonNull IBinder value) { + mTokenToRecord = value; + com.android.internal.util.AnnotationValidations.validate( + VisibleForTesting.class, null, mTokenToRecord); + return this; + } + + @Override + @DataClass.Generated.Member + public String toString() { + // You can override field toString logic by defining methods like: + // String fieldNameToString() { ... } + + return "ContentRecordingSession { " + + "displayId = " + mDisplayId + ", " + + "contentToRecord = " + recordContentToString(mContentToRecord) + ", " + + "tokenToRecord = " + mTokenToRecord + + " }"; + } + + @Override + @DataClass.Generated.Member + public boolean equals(@Nullable Object o) { + // You can override field equality logic by defining either of the methods like: + // boolean fieldNameEquals(ContentRecordingSession other) { ... } + // boolean fieldNameEquals(FieldType otherValue) { ... } + + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + @SuppressWarnings("unchecked") + ContentRecordingSession that = (ContentRecordingSession) o; + //noinspection PointlessBooleanExpression + return true + && mDisplayId == that.mDisplayId + && mContentToRecord == that.mContentToRecord + && java.util.Objects.equals(mTokenToRecord, that.mTokenToRecord); + } + + @Override + @DataClass.Generated.Member + public int hashCode() { + // You can override field hashCode logic by defining methods like: + // int fieldNameHashCode() { ... } + + int _hash = 1; + _hash = 31 * _hash + mDisplayId; + _hash = 31 * _hash + mContentToRecord; + _hash = 31 * _hash + java.util.Objects.hashCode(mTokenToRecord); + return _hash; + } + + @Override + @DataClass.Generated.Member + public void writeToParcel(@NonNull Parcel dest, int flags) { + // You can override field parcelling by defining methods like: + // void parcelFieldName(Parcel dest, int flags) { ... } + + byte flg = 0; + if (mTokenToRecord != null) flg |= 0x4; + dest.writeByte(flg); + dest.writeInt(mDisplayId); + dest.writeInt(mContentToRecord); + if (mTokenToRecord != null) dest.writeStrongBinder(mTokenToRecord); + } + + @Override + @DataClass.Generated.Member + public int describeContents() { return 0; } + + /** @hide */ + @SuppressWarnings({"unchecked", "RedundantCast"}) + @DataClass.Generated.Member + /* package-private */ ContentRecordingSession(@NonNull Parcel in) { + // You can override field unparcelling by defining methods like: + // static FieldType unparcelFieldName(Parcel in) { ... } + + byte flg = in.readByte(); + int displayId = in.readInt(); + int contentToRecord = in.readInt(); + IBinder tokenToRecord = (flg & 0x4) == 0 ? null : (IBinder) in.readStrongBinder(); + + this.mDisplayId = displayId; + this.mContentToRecord = contentToRecord; + + if (!(mContentToRecord == RECORD_CONTENT_DISPLAY) + && !(mContentToRecord == RECORD_CONTENT_TASK)) { + throw new java.lang.IllegalArgumentException( + "contentToRecord was " + mContentToRecord + " but must be one of: " + + "RECORD_CONTENT_DISPLAY(" + RECORD_CONTENT_DISPLAY + "), " + + "RECORD_CONTENT_TASK(" + RECORD_CONTENT_TASK + ")"); + } + + this.mTokenToRecord = tokenToRecord; + com.android.internal.util.AnnotationValidations.validate( + VisibleForTesting.class, null, mTokenToRecord); + + // onConstructed(); // You can define this method to get a callback + } + + @DataClass.Generated.Member + public static final @NonNull Parcelable.Creator<ContentRecordingSession> CREATOR + = new Parcelable.Creator<ContentRecordingSession>() { + @Override + public ContentRecordingSession[] newArray(int size) { + return new ContentRecordingSession[size]; + } + + @Override + public ContentRecordingSession createFromParcel(@NonNull Parcel in) { + return new ContentRecordingSession(in); + } + }; + + /** + * A builder for {@link ContentRecordingSession} + */ + @SuppressWarnings("WeakerAccess") + @DataClass.Generated.Member + public static final class Builder { + + private int mDisplayId; + private @RecordContent int mContentToRecord; + private @VisibleForTesting @Nullable IBinder mTokenToRecord; + + private long mBuilderFieldsSet = 0L; + + public Builder() { + } + + /** + * Unique logical identifier of the {@link android.hardware.display.VirtualDisplay} that has + * recorded content rendered to its surface. + */ + @DataClass.Generated.Member + public @NonNull Builder setDisplayId(int value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x1; + mDisplayId = value; + return this; + } + + /** + * The content to record. + */ + @DataClass.Generated.Member + public @NonNull Builder setContentToRecord(@RecordContent int value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x2; + mContentToRecord = value; + return this; + } + + /** + * 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}. + */ + @DataClass.Generated.Member + public @NonNull Builder setTokenToRecord(@VisibleForTesting @NonNull IBinder value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x4; + mTokenToRecord = value; + return this; + } + + /** Builds the instance. This builder should not be touched after calling this! */ + public @NonNull ContentRecordingSession build() { + checkNotUsed(); + mBuilderFieldsSet |= 0x8; // Mark builder used + + if ((mBuilderFieldsSet & 0x1) == 0) { + mDisplayId = INVALID_DISPLAY; + } + if ((mBuilderFieldsSet & 0x2) == 0) { + mContentToRecord = RECORD_CONTENT_DISPLAY; + } + if ((mBuilderFieldsSet & 0x4) == 0) { + mTokenToRecord = null; + } + ContentRecordingSession o = new ContentRecordingSession( + mDisplayId, + mContentToRecord, + mTokenToRecord); + return o; + } + + private void checkNotUsed() { + if ((mBuilderFieldsSet & 0x8) != 0) { + throw new IllegalStateException( + "This Builder should not be reused. Use a new Builder instance instead"); + } + } + } + + @DataClass.Generated( + time = 1644843382972L, + 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)") + @Deprecated + private void __metadata() {} + + + //@formatter:on + // End of generated code + +} diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl index f8a1b45a1c75..3f29e3f3078e 100644 --- a/core/java/android/view/IWindowManager.aidl +++ b/core/java/android/view/IWindowManager.aidl @@ -32,6 +32,7 @@ import android.graphics.Region; import android.os.Bundle; import android.os.IRemoteCallback; import android.os.ParcelFileDescriptor; +import android.view.ContentRecordingSession; import android.view.DisplayCutout; import android.view.DisplayInfo; import android.view.IAppTransitionAnimationSpecsFuture; @@ -879,6 +880,17 @@ interface IWindowManager void detachWindowContextFromWindowContainer(IBinder clientToken); /** + * Updates the content recording session. If a different session is already in progress, then + * the pre-existing session is stopped, and the new incoming session takes over. + * + * The DisplayContent for the new session will begin recording when + * {@link RootWindowContainer#onDisplayChanged} is invoked for the new {@link VirtualDisplay}. + * + * @param incomingSession the nullable incoming content recording session + */ + void setContentRecordingSession(in ContentRecordingSession incomingSession); + + /** * Registers a listener, which is to be called whenever cross-window blur is enabled/disabled. * * @param listener the listener to be registered diff --git a/core/java/android/view/OWNERS b/core/java/android/view/OWNERS index e313388affb6..3e2849012b2b 100644 --- a/core/java/android/view/OWNERS +++ b/core/java/android/view/OWNERS @@ -75,6 +75,8 @@ per-file ContentInfo.java = file:/core/java/android/service/autofill/OWNERS per-file ContentInfo.java = file:/core/java/android/widget/OWNERS # WindowManager +per-file ContentRecordingSession.aidl = file:/services/core/java/com/android/server/wm/OWNERS +per-file ContentRecordingSession.java = file:/services/core/java/com/android/server/wm/OWNERS per-file DisplayCutout.aidl = file:/services/core/java/com/android/server/wm/OWNERS per-file DisplayCutout.java = file:/services/core/java/com/android/server/wm/OWNERS per-file IDisplay*.aidl = file:/services/core/java/com/android/server/wm/OWNERS diff --git a/core/java/com/android/internal/protolog/ProtoLogGroup.java b/core/java/com/android/internal/protolog/ProtoLogGroup.java index def598ca8724..45c6d5f10a5a 100644 --- a/core/java/com/android/internal/protolog/ProtoLogGroup.java +++ b/core/java/com/android/internal/protolog/ProtoLogGroup.java @@ -82,7 +82,7 @@ public enum ProtoLogGroup implements IProtoLogGroup { Consts.TAG_WM), WM_DEBUG_WINDOW_INSETS(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, Consts.TAG_WM), - WM_DEBUG_LAYER_MIRRORING(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, true, + WM_DEBUG_CONTENT_RECORDING(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, true, Consts.TAG_WM), WM_DEBUG_WALLPAPER(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, Consts.TAG_WM), WM_DEBUG_BACK_PREVIEW(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, true, diff --git a/core/tests/coretests/src/android/view/ContentRecordingSessionTest.java b/core/tests/coretests/src/android/view/ContentRecordingSessionTest.java new file mode 100644 index 000000000000..df96a7d4568a --- /dev/null +++ b/core/tests/coretests/src/android/view/ContentRecordingSessionTest.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import static android.view.ContentRecordingSession.RECORD_CONTENT_DISPLAY; +import static android.view.ContentRecordingSession.RECORD_CONTENT_TASK; +import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.Display.INVALID_DISPLAY; + +import static com.google.common.truth.Truth.assertThat; + +import android.os.Binder; +import android.os.IBinder; +import android.os.Parcel; +import android.platform.test.annotations.Presubmit; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Tests for {@link ContentRecordingSession} class. + * + * Build/Install/Run: + * atest FrameworksCoreTests:ContentRecordingSessionTest + */ +@RunWith(AndroidJUnit4.class) +@SmallTest +@Presubmit +public class ContentRecordingSessionTest { + private static final int DISPLAY_ID = 1; + private static final IBinder WINDOW_TOKEN = new Binder("DisplayContentWindowToken"); + + @Test + public void testParcelable() { + ContentRecordingSession session = ContentRecordingSession.createTaskSession(WINDOW_TOKEN); + session.setDisplayId(DISPLAY_ID); + + Parcel parcel = Parcel.obtain(); + session.writeToParcel(parcel, 0 /* flags */); + parcel.setDataPosition(0); + ContentRecordingSession session2 = ContentRecordingSession.CREATOR.createFromParcel(parcel); + assertThat(session).isEqualTo(session2); + parcel.recycle(); + } + + @Test + public void testTaskConstructor() { + ContentRecordingSession session = ContentRecordingSession.createTaskSession(WINDOW_TOKEN); + assertThat(session.getContentToRecord()).isEqualTo(RECORD_CONTENT_TASK); + assertThat(session.getTokenToRecord()).isEqualTo(WINDOW_TOKEN); + } + + @Test + public void testDisplayConstructor() { + ContentRecordingSession session = ContentRecordingSession.createDisplaySession( + WINDOW_TOKEN); + assertThat(session.getContentToRecord()).isEqualTo(RECORD_CONTENT_DISPLAY); + assertThat(session.getTokenToRecord()).isEqualTo(WINDOW_TOKEN); + } + + @Test + public void testIsValid() { + ContentRecordingSession session = ContentRecordingSession.createDisplaySession( + WINDOW_TOKEN); + assertThat(ContentRecordingSession.isValid(session)).isFalse(); + + session.setDisplayId(DEFAULT_DISPLAY); + assertThat(ContentRecordingSession.isValid(session)).isTrue(); + + session.setDisplayId(INVALID_DISPLAY); + assertThat(ContentRecordingSession.isValid(session)).isFalse(); + } + + @Test + public void testIsSameDisplay() { + assertThat(ContentRecordingSession.isSameDisplay(null, null)).isFalse(); + ContentRecordingSession session = ContentRecordingSession.createDisplaySession( + WINDOW_TOKEN); + session.setDisplayId(DEFAULT_DISPLAY); + assertThat(ContentRecordingSession.isSameDisplay(session, null)).isFalse(); + + ContentRecordingSession incomingSession = ContentRecordingSession.createDisplaySession( + WINDOW_TOKEN); + incomingSession.setDisplayId(DEFAULT_DISPLAY); + assertThat(ContentRecordingSession.isSameDisplay(session, incomingSession)).isTrue(); + + incomingSession.setDisplayId(DEFAULT_DISPLAY + 1); + assertThat(ContentRecordingSession.isSameDisplay(session, incomingSession)).isFalse(); + } + + @Test + public void testEquals() { + ContentRecordingSession session = ContentRecordingSession.createTaskSession(WINDOW_TOKEN); + session.setDisplayId(DISPLAY_ID); + + ContentRecordingSession session2 = ContentRecordingSession.createTaskSession(WINDOW_TOKEN); + session2.setDisplayId(DISPLAY_ID); + assertThat(session).isEqualTo(session2); + } +} diff --git a/core/tests/coretests/src/android/view/OWNERS b/core/tests/coretests/src/android/view/OWNERS index 10f6f1fd22f1..a142e27a1ab0 100644 --- a/core/tests/coretests/src/android/view/OWNERS +++ b/core/tests/coretests/src/android/view/OWNERS @@ -13,6 +13,7 @@ per-file *Insets* = file:/services/core/java/com/android/server/wm/OWNERS per-file *View* = file:/services/core/java/com/android/server/wm/OWNERS per-file *Visibility* = file:/services/core/java/com/android/server/wm/OWNERS per-file *Window* = file:/services/core/java/com/android/server/wm/OWNERS +per-file *ContentRecord* = file:/services/core/java/com/android/server/wm/OWNERS # Scroll Capture per-file *ScrollCapture*.java = file:/packages/SystemUI/src/com/android/systemui/screenshot/OWNERS diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index ed8986993ab0..3d505315fc95 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -193,12 +193,6 @@ "group": "WM_DEBUG_SYNC_ENGINE", "at": "com\/android\/server\/wm\/BLASTSyncEngine.java" }, - "-1898316768": { - "message": "Unable to retrieve window container to start layer mirroring for display %d", - "level": "VERBOSE", - "group": "WM_DEBUG_LAYER_MIRRORING", - "at": "com\/android\/server\/wm\/DisplayContent.java" - }, "-1895337367": { "message": "Delete root task display=%d winMode=%d", "level": "VERBOSE", @@ -349,6 +343,12 @@ "group": "WM_DEBUG_STATES", "at": "com\/android\/server\/wm\/ActivityTaskSupervisor.java" }, + "-1734445525": { + "message": "Display %d has content (%b) so pause recording", + "level": "VERBOSE", + "group": "WM_DEBUG_CONTENT_RECORDING", + "at": "com\/android\/server\/wm\/DisplayContent.java" + }, "-1730156332": { "message": "Display id=%d rotation changed to %d from %d, lastOrientation=%d", "level": "VERBOSE", @@ -511,6 +511,12 @@ "group": "WM_ERROR", "at": "com\/android\/server\/wm\/WindowManagerService.java" }, + "-1542296596": { + "message": "Going ahead with updating recording for display %d to new bounds %s and\/or orientation %d.", + "level": "VERBOSE", + "group": "WM_DEBUG_CONTENT_RECORDING", + "at": "com\/android\/server\/wm\/DisplayContent.java" + }, "-1539974875": { "message": "removeAppToken: %s delayed=%b Callers=%s", "level": "VERBOSE", @@ -733,6 +739,12 @@ "group": "WM_DEBUG_STARTING_WINDOW", "at": "com\/android\/server\/wm\/ActivityRecord.java" }, + "-1323003327": { + "message": "Unexpectedly null window container; unable to update recording for display %d", + "level": "VERBOSE", + "group": "WM_DEBUG_CONTENT_RECORDING", + "at": "com\/android\/server\/wm\/DisplayContent.java" + }, "-1311436264": { "message": "Unregister task fragment organizer=%s uid=%d pid=%d", "level": "VERBOSE", @@ -847,6 +859,12 @@ "group": "WM_DEBUG_WINDOW_INSETS", "at": "com\/android\/server\/wm\/InsetsSourceProvider.java" }, + "-1179559337": { + "message": "Unable to start recording due to invalid region for display %d", + "level": "VERBOSE", + "group": "WM_DEBUG_CONTENT_RECORDING", + "at": "com\/android\/server\/wm\/DisplayContent.java" + }, "-1176488860": { "message": "SURFACE isSecure=%b: %s", "level": "INFO", @@ -1027,6 +1045,12 @@ "group": "WM_DEBUG_DRAW", "at": "com\/android\/server\/wm\/WindowStateAnimator.java" }, + "-992111757": { + "message": "Unable to retrieve window container to start recording for display %d", + "level": "VERBOSE", + "group": "WM_DEBUG_CONTENT_RECORDING", + "at": "com\/android\/server\/wm\/DisplayContent.java" + }, "-986746907": { "message": "Starting window removed %s", "level": "DEBUG", @@ -1087,6 +1111,12 @@ "group": "WM_DEBUG_STATES", "at": "com\/android\/server\/wm\/ActivityRecord.java" }, + "-922507769": { + "message": "Display %d has no content and is on, so start recording for state %d", + "level": "VERBOSE", + "group": "WM_DEBUG_CONTENT_RECORDING", + "at": "com\/android\/server\/wm\/DisplayContent.java" + }, "-917215012": { "message": "%s: caller %d is using old GET_TASKS but privileged; allowing", "level": "WARN", @@ -1105,12 +1135,6 @@ "group": "WM_ERROR", "at": "com\/android\/server\/wm\/WindowManagerService.java" }, - "-904499590": { - "message": "Provided surface for layer mirroring on display %d is not present, so do not update the surface", - "level": "VERBOSE", - "group": "WM_DEBUG_LAYER_MIRRORING", - "at": "com\/android\/server\/wm\/DisplayContent.java" - }, "-883738232": { "message": "Adding more than one toast window for UID at a time.", "level": "WARN", @@ -1345,12 +1369,6 @@ "group": "WM_DEBUG_ORIENTATION", "at": "com\/android\/server\/wm\/RootWindowContainer.java" }, - "-663411559": { - "message": "Going ahead with updating layer mirroring for display %d to new bounds %s and\/or orientation %d.", - "level": "VERBOSE", - "group": "WM_DEBUG_LAYER_MIRRORING", - "at": "com\/android\/server\/wm\/DisplayContent.java" - }, "-655104359": { "message": "Frontmost changed immersion: %s", "level": "DEBUG", @@ -1531,6 +1549,18 @@ "group": "WM_DEBUG_KEEP_SCREEN_ON", "at": "com\/android\/server\/wm\/RootWindowContainer.java" }, + "-479990726": { + "message": "Unable to start recording due to null token for display %d", + "level": "VERBOSE", + "group": "WM_DEBUG_CONTENT_RECORDING", + "at": "com\/android\/server\/wm\/DisplayContent.java" + }, + "-473911359": { + "message": "Display %d was already recording, so apply transformations if necessary", + "level": "VERBOSE", + "group": "WM_DEBUG_CONTENT_RECORDING", + "at": "com\/android\/server\/wm\/DisplayContent.java" + }, "-463348344": { "message": "Removing and adding activity %s to root task at top callers=%s", "level": "INFO", @@ -1609,10 +1639,10 @@ "group": "WM_ERROR", "at": "com\/android\/server\/wm\/WindowManagerService.java" }, - "-384564722": { - "message": "Unable to start layer mirroring for display %d since the surface is not available.", + "-381522987": { + "message": "Display %d state is now (%d), so update recording?", "level": "VERBOSE", - "group": "WM_DEBUG_LAYER_MIRRORING", + "group": "WM_DEBUG_CONTENT_RECORDING", "at": "com\/android\/server\/wm\/DisplayContent.java" }, "-381475323": { @@ -1639,6 +1669,12 @@ "group": "WM_DEBUG_TASKS", "at": "com\/android\/server\/wm\/RootWindowContainer.java" }, + "-370641936": { + "message": "Unable to update recording for display %d to new bounds %s and\/or orientation %d, since the surface is not available.", + "level": "VERBOSE", + "group": "WM_DEBUG_CONTENT_RECORDING", + "at": "com\/android\/server\/wm\/DisplayContent.java" + }, "-360208282": { "message": "Animating wallpapers: old: %s hidden=%b new: %s hidden=%b", "level": "VERBOSE", @@ -1699,12 +1735,6 @@ "group": "WM_DEBUG_STATES", "at": "com\/android\/server\/wm\/TaskFragment.java" }, - "-309399422": { - "message": "Display %d state is now (%d), so update layer mirroring?", - "level": "VERBOSE", - "group": "WM_DEBUG_LAYER_MIRRORING", - "at": "com\/android\/server\/wm\/DisplayContent.java" - }, "-304728471": { "message": "New wallpaper: target=%s prev=%s", "level": "DEBUG", @@ -1753,6 +1783,12 @@ "group": "WM_DEBUG_APP_TRANSITIONS", "at": "com\/android\/server\/wm\/AppTransitionController.java" }, + "-237664290": { + "message": "Pause the recording session on display %s", + "level": "VERBOSE", + "group": "WM_DEBUG_CONTENT_RECORDING", + "at": "com\/android\/server\/wm\/ContentRecordingController.java" + }, "-235225312": { "message": "Skipping config check for initializing activity: %s", "level": "VERBOSE", @@ -1789,12 +1825,6 @@ "group": "WM_DEBUG_WINDOW_MOVEMENT", "at": "com\/android\/server\/wm\/WindowManagerService.java" }, - "-190034097": { - "message": "Unable to retrieve window container to update layer mirroring for display %d", - "level": "VERBOSE", - "group": "WM_DEBUG_LAYER_MIRRORING", - "at": "com\/android\/server\/wm\/DisplayContent.java" - }, "-182877285": { "message": "Wallpaper layer changed: assigning layers + relayout", "level": "VERBOSE", @@ -1903,12 +1933,6 @@ "group": "WM_DEBUG_STATES", "at": "com\/android\/server\/wm\/TaskFragment.java" }, - "-79877120": { - "message": "Display %d has content (%b) so disable layer mirroring", - "level": "VERBOSE", - "group": "WM_DEBUG_LAYER_MIRRORING", - "at": "com\/android\/server\/wm\/DisplayContent.java" - }, "-70719599": { "message": "Unregister remote animations for organizer=%s uid=%d pid=%d", "level": "VERBOSE", @@ -2473,12 +2497,6 @@ "group": "WM_DEBUG_WINDOW_ORGANIZER", "at": "com\/android\/server\/wm\/DisplayAreaOrganizerController.java" }, - "504397469": { - "message": "Unable to update layer mirroring for display %d to new bounds %s and\/or orientation %d, since the surface is not available.", - "level": "VERBOSE", - "group": "WM_DEBUG_LAYER_MIRRORING", - "at": "com\/android\/server\/wm\/DisplayContent.java" - }, "508887531": { "message": "applyAnimation voice: anim=%s transit=%s isEntrance=%b Callers=%s", "level": "VERBOSE", @@ -3253,6 +3271,12 @@ "group": "WM_DEBUG_WINDOW_ORGANIZER", "at": "com\/android\/server\/wm\/TaskOrganizerController.java" }, + "1401287081": { + "message": "Handle incoming session on display %d, with a pre-existing session %s", + "level": "VERBOSE", + "group": "WM_DEBUG_CONTENT_RECORDING", + "at": "com\/android\/server\/wm\/ContentRecordingController.java" + }, "1401295262": { "message": "Mode default, asking user", "level": "WARN", @@ -3265,12 +3289,6 @@ "group": "WM_DEBUG_SCREEN_ON", "at": "com\/android\/server\/wm\/WindowManagerService.java" }, - "1407569006": { - "message": "Display %d was already layer mirroring, so apply transformations if necessary", - "level": "VERBOSE", - "group": "WM_DEBUG_LAYER_MIRRORING", - "at": "com\/android\/server\/wm\/DisplayContent.java" - }, "1422781269": { "message": "Resuming rotation after re-position", "level": "DEBUG", @@ -3517,12 +3535,6 @@ "group": "WM_DEBUG_CONFIGURATION", "at": "com\/android\/server\/wm\/ActivityRecord.java" }, - "1687376052": { - "message": "Display %d has no content and is on, so start layer mirroring for state %d", - "level": "VERBOSE", - "group": "WM_DEBUG_LAYER_MIRRORING", - "at": "com\/android\/server\/wm\/DisplayContent.java" - }, "1689989893": { "message": "SyncGroup %d: Set ready", "level": "VERBOSE", @@ -3535,6 +3547,12 @@ "group": "WM_DEBUG_WINDOW_ORGANIZER", "at": "com\/android\/server\/wm\/DisplayAreaOrganizerController.java" }, + "1707558369": { + "message": "Unable to start recording for display %d since the surface is not available.", + "level": "VERBOSE", + "group": "WM_DEBUG_CONTENT_RECORDING", + "at": "com\/android\/server\/wm\/DisplayContent.java" + }, "1720229827": { "message": "Creating animation bounds layer", "level": "INFO", @@ -3667,6 +3685,12 @@ "group": "WM_DEBUG_STARTING_WINDOW", "at": "com\/android\/server\/wm\/ActivityRecord.java" }, + "1854279309": { + "message": "Provided surface for recording on display %d is not present, so do not update the surface", + "level": "VERBOSE", + "group": "WM_DEBUG_CONTENT_RECORDING", + "at": "com\/android\/server\/wm\/DisplayContent.java" + }, "1856211951": { "message": "moveFocusableActivityToTop: already on top, activity=%s", "level": "DEBUG", @@ -3942,6 +3966,9 @@ "WM_DEBUG_CONTAINERS": { "tag": "WindowManager" }, + "WM_DEBUG_CONTENT_RECORDING": { + "tag": "WindowManager" + }, "WM_DEBUG_DRAW": { "tag": "WindowManager" }, @@ -3960,9 +3987,6 @@ "WM_DEBUG_KEEP_SCREEN_ON": { "tag": "WindowManager" }, - "WM_DEBUG_LAYER_MIRRORING": { - "tag": "WindowManager" - }, "WM_DEBUG_LOCKTASK": { "tag": "WindowManager" }, diff --git a/media/java/android/media/projection/MediaProjection.java b/media/java/android/media/projection/MediaProjection.java index 5259c4f07639..4dde5e8d39a2 100644 --- a/media/java/android/media/projection/MediaProjection.java +++ b/media/java/android/media/projection/MediaProjection.java @@ -29,7 +29,9 @@ import android.os.IBinder; import android.os.RemoteException; import android.util.ArrayMap; import android.util.Log; +import android.view.ContentRecordingSession; import android.view.Surface; +import android.view.WindowManagerGlobal; import java.util.Map; @@ -106,16 +108,12 @@ public final class MediaProjection { if (isSecure) { flags |= DisplayManager.VIRTUAL_DISPLAY_FLAG_SECURE; } - Context windowContext = mContext.createWindowContext(mContext.getDisplayNoVerify(), - TYPE_APPLICATION, null /* options */); - final VirtualDisplayConfig.Builder builder = buildMirroredVirtualDisplay(name, width, - height, dpi, windowContext.getWindowContextToken()); - builder.setFlags(flags); + final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(name, width, + height, dpi).setFlags(flags); if (surface != null) { builder.setSurface(surface); } - VirtualDisplay virtualDisplay = createVirtualDisplay(builder.build(), callback, handler, - windowContext); + VirtualDisplay virtualDisplay = createVirtualDisplay(builder, callback, handler); return virtualDisplay; } @@ -145,38 +143,16 @@ public final class MediaProjection { public VirtualDisplay createVirtualDisplay(@NonNull String name, int width, int height, int dpi, int flags, @Nullable Surface surface, @Nullable VirtualDisplay.Callback callback, @Nullable Handler handler) { - Context windowContext = mContext.createWindowContext(mContext.getDisplayNoVerify(), - TYPE_APPLICATION, null /* options */); - final VirtualDisplayConfig.Builder builder = buildMirroredVirtualDisplay(name, width, - height, dpi, windowContext.getWindowContextToken()); - builder.setFlags(flags); + final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(name, width, + height, dpi).setFlags(flags); if (surface != null) { builder.setSurface(surface); } - VirtualDisplay virtualDisplay = createVirtualDisplay(builder.build(), callback, handler, - windowContext); + VirtualDisplay virtualDisplay = createVirtualDisplay(builder, callback, handler); return virtualDisplay; } /** - * Constructs a {@link VirtualDisplayConfig.Builder}, which will mirror the contents of a - * DisplayArea. The DisplayArea to mirror is from the DisplayArea the caller is launched on. - * - * @param name The name of the virtual display, must be non-empty. - * @param width The width of the virtual display in pixels. Must be greater than 0. - * @param height The height of the virtual display in pixels. Must be greater than 0. - * @param dpi The density of the virtual display in dpi. Must be greater than 0. - * @return a config representing a VirtualDisplay - */ - private VirtualDisplayConfig.Builder buildMirroredVirtualDisplay(@NonNull String name, - int width, int height, int dpi, IBinder windowContextToken) { - final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(name, width, - height, dpi); - builder.setWindowTokenClientToMirror(windowContextToken); - return builder; - } - - /** * Creates a {@link android.hardware.display.VirtualDisplay} to capture the * contents of the screen. * @@ -186,18 +162,52 @@ public final class MediaProjection { * @param handler The {@link android.os.Handler} on which the callback should be invoked, or * null if the callback should be invoked on the calling thread's main * {@link android.os.Looper}. - * @param windowContext the WindowContext associated with the caller. * * @see android.hardware.display.VirtualDisplay * @hide */ @Nullable - public VirtualDisplay createVirtualDisplay(@NonNull VirtualDisplayConfig virtualDisplayConfig, - @Nullable VirtualDisplay.Callback callback, @Nullable Handler handler, - Context windowContext) { - DisplayManager dm = mContext.getSystemService(DisplayManager.class); - return dm.createVirtualDisplay(this, virtualDisplayConfig, callback, handler, - windowContext); + public VirtualDisplay createVirtualDisplay( + @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(); + virtualDisplayConfig.setWindowManagerMirroring(true); + final DisplayManager dm = mContext.getSystemService(DisplayManager.class); + final VirtualDisplay virtualDisplay = dm.createVirtualDisplay(this, + virtualDisplayConfig.build(), + callback, handler, windowContext); + setSession(windowContextToken, virtualDisplay); + return virtualDisplay; + } catch (RemoteException e) { + // Can not capture if WMS is not accessible, so bail out. + throw e.rethrowFromSystemServer(); + } + } + + /** + * 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); } /** diff --git a/media/java/android/media/projection/OWNERS b/media/java/android/media/projection/OWNERS index 7e7335d68d3b..9ca391013aa3 100644 --- a/media/java/android/media/projection/OWNERS +++ b/media/java/android/media/projection/OWNERS @@ -1 +1,2 @@ michaelwr@google.com +santoscordon@google.com diff --git a/services/core/java/com/android/server/display/DisplayDevice.java b/services/core/java/com/android/server/display/DisplayDevice.java index 5de162ce711c..311fa7c16958 100644 --- a/services/core/java/com/android/server/display/DisplayDevice.java +++ b/services/core/java/com/android/server/display/DisplayDevice.java @@ -109,27 +109,26 @@ abstract class DisplayDevice { } /** - * Returns the window token of the level of the WindowManager hierarchy to mirror, or null - * if layer mirroring by SurfaceFlinger should not be performed. - * For now, only used for mirroring started from MediaProjection. + * Returns the if WindowManager is responsible for mirroring on this display. If {@code false}, + * then SurfaceFlinger performs no layer mirroring on this display. + * Only used for mirroring started from MediaProjection. */ - @Nullable - public IBinder getWindowTokenClientToMirrorLocked() { - return null; + public boolean isWindowManagerMirroringLocked() { + return false; } /** - * Updates the window token of the level of the level of the WindowManager hierarchy to mirror. - * If windowToken is null, then no layer mirroring by SurfaceFlinger to should be performed. - * For now, only used for mirroring started from MediaProjection. + * Updates if WindowManager is responsible for mirroring on this display. If {@code false}, then + * SurfaceFlinger performs no layer mirroring to this display. + * Only used for mirroring started from MediaProjection. */ - public void setWindowTokenClientToMirrorLocked(IBinder windowToken) { + public void setWindowManagerMirroringLocked(boolean isMirroring) { } /** * Returns the default size of the surface associated with the display, or null if the surface * is not provided for layer mirroring by SurfaceFlinger. - * For now, only used for mirroring started from MediaProjection. + * Only used for mirroring started from MediaProjection. */ @Nullable public Point getDisplaySurfaceDefaultSize() { diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index 66e9da040102..5d1ad2a377dd 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -2265,13 +2265,12 @@ public final class DisplayManagerService extends SystemService { final DisplayDeviceInfo info = device.getDisplayDeviceInfoLocked(); final boolean ownContent = (info.flags & DisplayDeviceInfo.FLAG_OWN_CONTENT_ONLY) != 0; - // Mirror the part of WM hierarchy that corresponds to the provided window token. - IBinder windowTokenClientToMirror = device.getWindowTokenClientToMirrorLocked(); - // Find the logical display that the display device is showing. // Certain displays only ever show their own content. LogicalDisplay display = mLogicalDisplayMapper.getDisplayLocked(device); - if (!ownContent && windowTokenClientToMirror == null) { + // Proceed with display-managed mirroring only if window manager will not be handling it. + if (!ownContent && !device.isWindowManagerMirroringLocked()) { + // Only mirror the display if content recording is not taking place in WM. if (display != null && !display.hasContentLocked()) { // If the display does not have any content of its own, then // automatically mirror the requested logical display contents if possible. @@ -3864,23 +3863,11 @@ public final class DisplayManagerService extends SystemService { } @Override - public IBinder getWindowTokenClientToMirror(int displayId) { - final DisplayDevice device; - synchronized (mSyncRoot) { - device = getDeviceForDisplayLocked(displayId); - if (device == null) { - return null; - } - } - return device.getWindowTokenClientToMirrorLocked(); - } - - @Override - public void setWindowTokenClientToMirror(int displayId, IBinder windowToken) { + public void setWindowManagerMirroring(int displayId, boolean isMirroring) { synchronized (mSyncRoot) { final DisplayDevice device = getDeviceForDisplayLocked(displayId); if (device != null) { - device.setWindowTokenClientToMirrorLocked(windowToken); + device.setWindowManagerMirroringLocked(isMirroring); } } } diff --git a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java index 797c3fb44338..ea313743d28d 100644 --- a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java +++ b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java @@ -33,7 +33,6 @@ import static com.android.server.display.DisplayDeviceInfo.FLAG_ALWAYS_UNLOCKED; import static com.android.server.display.DisplayDeviceInfo.FLAG_OWN_DISPLAY_GROUP; import static com.android.server.display.DisplayDeviceInfo.FLAG_TRUSTED; -import android.annotation.Nullable; import android.content.Context; import android.graphics.Point; import android.hardware.display.IVirtualDisplayCallback; @@ -235,7 +234,7 @@ public class VirtualDisplayAdapter extends DisplayAdapter { private Display.Mode mMode; private boolean mIsDisplayOn; private int mDisplayIdToMirror; - private IBinder mWindowTokenClientToMirror; + private boolean mIsWindowManagerMirroring; public VirtualDisplayDevice(IBinder displayToken, IBinder appToken, int ownerUid, String ownerPackageName, Surface surface, int flags, @@ -258,7 +257,7 @@ public class VirtualDisplayAdapter extends DisplayAdapter { mUniqueIndex = uniqueIndex; mIsDisplayOn = surface != null; mDisplayIdToMirror = virtualDisplayConfig.getDisplayIdToMirror(); - mWindowTokenClientToMirror = virtualDisplayConfig.getWindowTokenClientToMirror(); + mIsWindowManagerMirroring = virtualDisplayConfig.isWindowManagerMirroring(); } @Override @@ -289,15 +288,14 @@ public class VirtualDisplayAdapter extends DisplayAdapter { } @Override - @Nullable - public IBinder getWindowTokenClientToMirrorLocked() { - return mWindowTokenClientToMirror; + public boolean isWindowManagerMirroringLocked() { + return mIsWindowManagerMirroring; } @Override - public void setWindowTokenClientToMirrorLocked(IBinder windowToken) { - if (mWindowTokenClientToMirror != windowToken) { - mWindowTokenClientToMirror = windowToken; + public void setWindowManagerMirroringLocked(boolean mirroring) { + if (mIsWindowManagerMirroring != mirroring) { + mIsWindowManagerMirroring = mirroring; sendDisplayDeviceEventLocked(this, DISPLAY_DEVICE_EVENT_CHANGED); sendTraversalRequestLocked(); } @@ -391,7 +389,7 @@ public class VirtualDisplayAdapter extends DisplayAdapter { pw.println("mDisplayState=" + Display.stateToString(mDisplayState)); pw.println("mStopped=" + mStopped); pw.println("mDisplayIdToMirror=" + mDisplayIdToMirror); - pw.println("mWindowTokenClientToMirror=" + mWindowTokenClientToMirror); + pw.println("mWindowManagerMirroring=" + mIsWindowManagerMirroring); } diff --git a/services/core/java/com/android/server/media/projection/OWNERS b/services/core/java/com/android/server/media/projection/OWNERS index 7e7335d68d3b..9ca391013aa3 100644 --- a/services/core/java/com/android/server/media/projection/OWNERS +++ b/services/core/java/com/android/server/media/projection/OWNERS @@ -1 +1,2 @@ michaelwr@google.com +santoscordon@google.com diff --git a/services/core/java/com/android/server/wm/ContentRecordingController.java b/services/core/java/com/android/server/wm/ContentRecordingController.java new file mode 100644 index 000000000000..fca4942d4b79 --- /dev/null +++ b/services/core/java/com/android/server/wm/ContentRecordingController.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm; + +import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_CONTENT_RECORDING; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.view.ContentRecordingSession; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.protolog.common.ProtoLog; + +/** + * Orchestrates the handoff between displays if the recording session changes, and keeps track of + * the current recording session state. Only supports one content recording session on the device at + * once. + */ +final class ContentRecordingController { + + /** + * The current recording session. + */ + @Nullable + private ContentRecordingSession mSession = null; + + @Nullable + private DisplayContent mDisplayContent = null; + + /** + * Returns the current recording session. If returns {@code null}, then recording is not taking + * place. + */ + @Nullable + @VisibleForTesting + ContentRecordingSession getContentRecordingSessionLocked() { + // Copy out the session, to allow it to be modified without updating this reference. + return mSession; + } + + /** + * Updates the current recording session. If a new display is taking over recording, then + * stops the prior display from recording. + * + * @param incomingSession the new recording session. Should either be {@code null}, to stop + * the current session, or a session on a new/different display than the + * current session. + * @param wmService the window manager service + */ + void setContentRecordingSessionLocked(@Nullable ContentRecordingSession incomingSession, + @NonNull WindowManagerService wmService) { + if (incomingSession != null && (!ContentRecordingSession.isValid(incomingSession) + || ContentRecordingSession.isSameDisplay(mSession, incomingSession))) { + // Ignore an invalid session, or a session for the same display as currently recording. + return; + } + DisplayContent incomingDisplayContent = null; + if (incomingSession != null) { + // Recording will start on a new display, possibly taking over from a current session. + ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, + "Handle incoming session on display %d, with a pre-existing session %s", + incomingSession.getDisplayId(), + mSession == null ? null : mSession.getDisplayId()); + incomingDisplayContent = wmService.mRoot.getDisplayContentOrCreate( + incomingSession.getDisplayId()); + incomingDisplayContent.setContentRecordingSession(incomingSession); + } + if (mSession != null) { + // Update the pre-existing display about the new session. + ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, + "Pause the recording session on display %s", + mDisplayContent.getDisplayId()); + mDisplayContent.pauseRecording(); + mDisplayContent.setContentRecordingSession(null); + } + // Update the cached states. + mDisplayContent = incomingDisplayContent; + mSession = incomingSession; + } +} diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 77787e1e7530..eb2c01adc79e 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -37,6 +37,7 @@ import static android.os.Process.SYSTEM_UID; import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; import static android.util.DisplayMetrics.DENSITY_DEFAULT; import static android.util.RotationUtils.deltaRotation; +import static android.view.ContentRecordingSession.RECORD_CONTENT_DISPLAY; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.FLAG_CAN_SHOW_WITH_INSECURE_KEYGUARD; import static android.view.Display.FLAG_PRIVATE; @@ -88,10 +89,10 @@ import static android.window.DisplayAreaOrganizer.FEATURE_WINDOWED_MAGNIFICATION import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_APP_TRANSITIONS; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_BOOT; +import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_CONTENT_RECORDING; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_FOCUS; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_FOCUS_LIGHT; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_IME; -import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_LAYER_MIRRORING; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ORIENTATION; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_SCREEN_ON; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_WALLPAPER; @@ -197,6 +198,7 @@ import android.util.Slog; import android.util.SparseArray; import android.util.SparseBooleanArray; import android.util.proto.ProtoOutputStream; +import android.view.ContentRecordingSession; import android.view.Display; import android.view.DisplayCutout; import android.view.DisplayInfo; @@ -304,22 +306,28 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp */ private SurfaceControl mWindowingLayer; + // TODO(216756854) move all recording fields to the controller /** - * The window token of the layer of the hierarchy to mirror, or null if this DisplayContent - * is not being used for layer mirroring. + * The session for content recording, or null if this DisplayContent is not being used for + * recording. */ - @VisibleForTesting IBinder mTokenToMirror = null; + @VisibleForTesting private ContentRecordingSession mContentRecordingSession = null; /** - * The surface for mirroring the contents of this hierarchy, or null if layer mirroring is + * The WindowContainer for the level of the hierarchy to record. + */ + @Nullable private DisplayContent mRecordedWindowContainer = null; + + /** + * The surface for recording the contents of this hierarchy, or null if content recording is * temporarily disabled. */ - private SurfaceControl mMirroredSurface = null; + @Nullable private SurfaceControl mRecordedSurface = null; /** - * The last bounds of the DisplayArea to mirror. + * The last bounds of the region to record. */ - private Rect mLastMirroredDisplayAreaBounds = null; + @Nullable private Rect mLastRecordedBounds = null; /** * The default per Display minimal size of tasks. Calculated at construction. @@ -2535,44 +2543,41 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp // Update IME parent if needed. updateImeParent(); - // Update mirroring surface for MediaProjection, if this DisplayContent is being used - // for layer mirroring. - if (isCurrentlyMirroring() && mLastMirroredDisplayAreaBounds != null) { - // Mirroring has already begun, but update mirroring since the display is now on. - final WindowContainer wc = mWmService.mWindowContextListenerController.getContainer( - mTokenToMirror); - if (wc == null) { - ProtoLog.v(WM_DEBUG_LAYER_MIRRORING, - "Unable to retrieve window container to update layer mirroring for " + // Update surface for MediaProjection, if this DisplayContent is being used for recording. + if (isCurrentlyRecording() && mLastRecordedBounds != null) { + // Recording has already begun, but update recording since the display is now on. + if (mRecordedWindowContainer == null) { + ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, + "Unexpectedly null window container; unable to update recording for " + "display %d", mDisplayId); return; } - ProtoLog.v(WM_DEBUG_LAYER_MIRRORING, - "Display %d was already layer mirroring, so apply transformations if necessary", + ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, + "Display %d was already recording, so apply transformations if necessary", mDisplayId); - // Retrieve the size of the DisplayArea to mirror, and continue with the update + // Retrieve the size of the region to record, and continue with the update // if the bounds or orientation has changed. - final Rect displayAreaBounds = wc.getDisplayContent().getBounds(); - int displayAreaOrientation = wc.getDisplayContent().getOrientation(); - if (!mLastMirroredDisplayAreaBounds.equals(displayAreaBounds) - || lastOrientation != displayAreaOrientation) { + final Rect recordedContentBounds = mRecordedWindowContainer.getBounds(); + int recordedContentOrientation = mRecordedWindowContainer.getOrientation(); + if (!mLastRecordedBounds.equals(recordedContentBounds) + || lastOrientation != recordedContentOrientation) { Point surfaceSize = fetchSurfaceSizeIfPresent(); if (surfaceSize != null) { - ProtoLog.v(WM_DEBUG_LAYER_MIRRORING, - "Going ahead with updating layer mirroring for display %d to new " + ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, + "Going ahead with updating recording for display %d to new " + "bounds %s and/or orientation %d.", - mDisplayId, displayAreaBounds, displayAreaOrientation); + mDisplayId, recordedContentBounds, recordedContentOrientation); updateMirroredSurface(mWmService.mTransactionFactory.get(), - displayAreaBounds, surfaceSize); + recordedContentBounds, surfaceSize); } else { // If the surface removed, do nothing. We will handle this via onDisplayChanged // (the display will be off if the surface is removed). - ProtoLog.v(WM_DEBUG_LAYER_MIRRORING, - "Unable to update layer mirroring for display %d to new bounds %s" + ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, + "Unable to update recording for display %d to new bounds %s" + " and/or orientation %d, since the surface is not available.", - mDisplayId, displayAreaBounds, displayAreaOrientation); + mDisplayId, recordedContentBounds, recordedContentOrientation); } } } @@ -4563,8 +4568,8 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp mTmpApplySurfaceChangesTransactionState.preferMinimalPostProcessing, true /* inTraversal, must call performTraversalInTrans... below */); } - // If the display now has content, or no longer has content, update layer mirroring. - updateMirroring(); + // If the display now has content, or no longer has content, update recording. + updateRecording(); final boolean wallpaperVisible = mWallpaperController.isWallpaperVisible(); if (wallpaperVisible != mLastWallpaperVisible) { @@ -5558,13 +5563,13 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp } else if (displayState == Display.STATE_ON) { mOffTokenAcquirer.release(mDisplayId); } - ProtoLog.v(WM_DEBUG_LAYER_MIRRORING, - "Display %d state is now (%d), so update layer mirroring?", + ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, + "Display %d state is now (%d), so update recording?", mDisplayId, displayState); if (lastDisplayState != displayState) { - // If state is on due to surface being added, then start layer mirroring. - // If state is off due to surface being removed, then stop layer mirroring. - updateMirroring(); + // If state is on due to surface being added, then start recording. + // If state is off due to surface being removed, then stop recording. + updateRecording(); } } // Dispatch pending Configuration to WindowContext if the associated display changes to @@ -5867,11 +5872,12 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp } mRemoved = true; - if (mMirroredSurface != null) { + if (mRecordedSurface != null) { // Do not wait for the mirrored surface to be garbage collected, but clean up // immediately. - mWmService.mTransactionFactory.get().remove(mMirroredSurface).apply(); - mMirroredSurface = null; + mWmService.mTransactionFactory.get().remove(mRecordedSurface).apply(); + mRecordedSurface = null; + clearContentRecordingSession(); } // Only update focus/visibility for the last one because there may be many root tasks are @@ -6112,68 +6118,84 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp } /** - * Start mirroring to this DisplayContent if it does not have its own content. Captures the - * content of a WindowContainer indicated by a WindowToken. If unable to start mirroring, falls + * Start recording to this DisplayContent if it does not have its own content. Captures the + * content of a WindowContainer indicated by a WindowToken. If unable to start recording, falls * back to original MediaProjection approach. */ - private void startMirrorIfNeeded() { - // Only mirror if this display does not have its own content, is not mirroring already, + private void startRecordingIfNeeded() { + // Only record if this display does not have its own content, is not recording already, // and if this display is on (it has a surface to write output to). - if (mLastHasContent || isCurrentlyMirroring() || mDisplay.getState() == Display.STATE_OFF) { + if (mLastHasContent || isCurrentlyRecording() || mDisplay.getState() == Display.STATE_OFF + || mContentRecordingSession == null) { return; } - // Given the WindowToken of the DisplayArea to mirror, retrieve the associated + final int contentToRecord = mContentRecordingSession.getContentToRecord(); + if (contentToRecord != RECORD_CONTENT_DISPLAY) { + // TODO(b/216625226) handle task-based recording + // Not a valid region, or recording is disabled, so fall back to prior MediaProjection + // approach. + clearContentRecordingSession(); + ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, + "Unable to start recording due to invalid region for display %d", + mDisplayId); + return; + } + // Given the WindowToken of the DisplayArea to record, retrieve the associated // SurfaceControl. - IBinder tokenToMirror = mWmService.mDisplayManagerInternal.getWindowTokenClientToMirror( - mDisplayId); - if (tokenToMirror == null) { - // This DisplayContent instance is not involved in layer mirroring. If the display - // has been created for capturing, fall back to prior MediaProjection approach. + IBinder tokenToRecord = mContentRecordingSession.getTokenToRecord(); + if (tokenToRecord == null) { + // Unexpectedly missing token. Fall back to prior MediaProjection approach. + clearContentRecordingSession(); + ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, + "Unable to start recording due to null token for display %d", mDisplayId); return; } final WindowContainer wc = mWmService.mWindowContextListenerController.getContainer( - tokenToMirror); + tokenToRecord); if (wc == null) { - // Un-set the window token to mirror for this VirtualDisplay, to fall back to the + // Un-set the window token to record for this VirtualDisplay. Fall back to the // original MediaProjection approach. - mWmService.mDisplayManagerInternal.setWindowTokenClientToMirror(mDisplayId, null); - ProtoLog.v(WM_DEBUG_LAYER_MIRRORING, - "Unable to retrieve window container to start layer mirroring for display %d", + mWmService.mDisplayManagerInternal.setWindowManagerMirroring(mDisplayId, false); + clearContentRecordingSession(); + ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, + "Unable to retrieve window container to start recording for " + + "display %d", mDisplayId); return; } + // TODO(206461622) Migrate to the RootDisplayArea + mRecordedWindowContainer = wc.getDisplayContent(); - Point surfaceSize = fetchSurfaceSizeIfPresent(); + final Point surfaceSize = fetchSurfaceSizeIfPresent(); if (surfaceSize == null) { - ProtoLog.v(WM_DEBUG_LAYER_MIRRORING, - "Unable to start layer mirroring for display %d since the surface is not " + ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, + "Unable to start recording for display %d since the surface is not " + "available.", mDisplayId); return; } - ProtoLog.v(WM_DEBUG_LAYER_MIRRORING, - "Display %d has no content and is on, so start layer mirroring for state %d", + ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, + "Display %d has no content and is on, so start recording for state %d", mDisplayId, mDisplay.getState()); // Create a mirrored hierarchy for the SurfaceControl of the DisplayArea to capture. - SurfaceControl sc = wc.getDisplayContent().getSurfaceControl(); - mMirroredSurface = SurfaceControl.mirrorSurface(sc); + mRecordedSurface = SurfaceControl.mirrorSurface( + mRecordedWindowContainer.getSurfaceControl()); SurfaceControl.Transaction transaction = mWmService.mTransactionFactory.get() // Set the mMirroredSurface's parent to the root SurfaceControl for this // DisplayContent. This brings the new mirrored hierarchy under this DisplayContent, // so SurfaceControl will write the layers of this hierarchy to the output surface // provided by the app. - .reparent(mMirroredSurface, mSurfaceControl) + .reparent(mRecordedSurface, mSurfaceControl) // Reparent the SurfaceControl of this DisplayContent to null, to prevent content // being added to it. This ensures that no app launched explicitly on the // VirtualDisplay will show up as part of the mirrored content. .reparent(mWindowingLayer, null) .reparent(mOverlayLayer, null); // Retrieve the size of the DisplayArea to mirror. - updateMirroredSurface(transaction, wc.getDisplayContent().getBounds(), surfaceSize); - mTokenToMirror = tokenToMirror; + updateMirroredSurface(transaction, mRecordedWindowContainer.getBounds(), surfaceSize); // No need to clean up. In SurfaceFlinger, parents hold references to their children. The // mirrored SurfaceControl is alive since the parent DisplayContent SurfaceControl is @@ -6182,34 +6204,64 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp } /** - * Start mirroring if this DisplayContent no longer has content. Stop mirroring if it now + * Pause the recording session. + */ + public void pauseRecording() { + if (mRecordedSurface == null) { + return; + } + ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, + "Display %d has content (%b) so pause recording", mDisplayId, + mLastHasContent); + // If the display is not on and it is a virtual display, then it no longer has an + // associated surface to write output to. + // If the display now has content, stop mirroring to it. + mWmService.mTransactionFactory.get() + // Remove the reference to mMirroredSurface, to clean up associated memory. + .remove(mRecordedSurface) + // Reparent the SurfaceControl of this DisplayContent back to mSurfaceControl, + // to allow content to be added to it. This allows this DisplayContent to stop + // mirroring and show content normally. + .reparent(mWindowingLayer, mSurfaceControl) + .reparent(mOverlayLayer, mSurfaceControl) + .apply(); + // Pause mirroring by destroying the reference to the mirrored layer. + mRecordedSurface = null; + // Do not un-set the token, in case content is removed and recording should begin again. + } + + /** + * Sets the incoming recording session. Should only be used when starting to record on + * this display; stopping recording is handled separately when the display is destroyed. + * @param session the new session indicating recording will begin on this display. + */ + public void setContentRecordingSession(@Nullable ContentRecordingSession session) { + mContentRecordingSession = session; + } + + /** + * Removes both the local cache and WM Service view of the current session, to stop the session + * on this display. + */ + private void clearContentRecordingSession() { + // Update the cached session state first, since updating the service will result in always + // returning to this instance to update recording state. + mContentRecordingSession = null; + mWmService.setContentRecordingSession(null); + } + + /** + * Start recording if this DisplayContent no longer has content. Stop recording if it now * has content or the display is not on. */ - private void updateMirroring() { - if (isCurrentlyMirroring() && (mLastHasContent + @VisibleForTesting void updateRecording() { + if (isCurrentlyRecording() && (mLastHasContent || mDisplay.getState() == Display.STATE_OFF)) { - ProtoLog.v(WM_DEBUG_LAYER_MIRRORING, - "Display %d has content (%b) so disable layer mirroring", mDisplayId, - mLastHasContent); - // If the display is not on and it is a virtual display, then it no longer has an - // associated surface to write output to. - // If the display now has content, stop mirroring to it. - mWmService.mTransactionFactory.get() - // Remove the reference to mMirroredSurface, to clean up associated memory. - .remove(mMirroredSurface) - // Reparent the SurfaceControl of this DisplayContent back to mSurfaceControl, - // to allow content to be added to it. This allows this DisplayContent to stop - // mirroring and show content normally. - .reparent(mWindowingLayer, mSurfaceControl) - .reparent(mOverlayLayer, mSurfaceControl) - .apply(); - // Stop mirroring by destroying the reference to the mirrored layer. - mMirroredSurface = null; - // Do not un-set the token, in case content is removed and mirroring should begin again. + pauseRecording(); } else { // Display no longer has content, or now has a surface to write to, so try to start - // mirroring to it. - startMirrorIfNeeded(); + // recording. + startRecordingIfNeeded(); } } @@ -6217,22 +6269,23 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp * Apply transformations to the mirrored surface to ensure the captured contents are scaled to * fit and centred in the output surface. * - * @param transaction the transaction to include transformations of mMirroredSurface - * to. Transaction is not applied before returning. - * @param displayAreaBounds bounds of the DisplayArea to mirror to the surface provided by - * the app. - * @param surfaceSize the default size of the surface to write the display area content to + * @param transaction the transaction to include transformations of mMirroredSurface + * to. Transaction is not applied before returning. + * @param recordedContentBounds bounds of the content to record to the surface provided by + * the app. + * @param surfaceSize the default size of the surface to write the display area + * content to */ @VisibleForTesting void updateMirroredSurface(SurfaceControl.Transaction transaction, - Rect displayAreaBounds, Point surfaceSize) { + Rect recordedContentBounds, Point surfaceSize) { // Calculate the scale to apply to the root mirror SurfaceControl to fit the size of the // output surface. - float scaleX = surfaceSize.x / (float) displayAreaBounds.width(); - float scaleY = surfaceSize.y / (float) displayAreaBounds.height(); + float scaleX = surfaceSize.x / (float) recordedContentBounds.width(); + float scaleY = surfaceSize.y / (float) recordedContentBounds.height(); float scale = Math.min(scaleX, scaleY); - int scaledWidth = Math.round(scale * (float) displayAreaBounds.width()); - int scaledHeight = Math.round(scale * (float) displayAreaBounds.height()); + int scaledWidth = Math.round(scale * (float) recordedContentBounds.width()); + int scaledHeight = Math.round(scale * (float) recordedContentBounds.height()); // Calculate the shift to apply to the root mirror SurfaceControl to centre the mirrored // contents in the output surface. @@ -6248,16 +6301,16 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp transaction // Crop the area to capture to exclude the 'extra' wallpaper that is used // for parallax (b/189930234). - .setWindowCrop(mMirroredSurface, displayAreaBounds.width(), - displayAreaBounds.height()) + .setWindowCrop(mRecordedSurface, recordedContentBounds.width(), + recordedContentBounds.height()) // Scale the root mirror SurfaceControl, based upon the size difference between the // source (DisplayArea to capture) and output (surface the app reads images from). - .setMatrix(mMirroredSurface, scale, 0 /* dtdx */, 0 /* dtdy */, scale) + .setMatrix(mRecordedSurface, scale, 0 /* dtdx */, 0 /* dtdy */, scale) // Position needs to be updated when the mirrored DisplayArea has changed, since // the content will no longer be centered in the output surface. - .setPosition(mMirroredSurface, shiftedX /* x */, shiftedY /* y */) + .setPosition(mRecordedSurface, shiftedX /* x */, shiftedY /* y */) .apply(); - mLastMirroredDisplayAreaBounds = new Rect(displayAreaBounds); + mLastRecordedBounds = new Rect(recordedContentBounds); } /** @@ -6274,8 +6327,8 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp // Layer mirroring started with a null surface, so do not apply any transformations yet. // State of virtual display will change to 'ON' when the surface is set. // will get event DISPLAY_DEVICE_EVENT_CHANGED - ProtoLog.v(WM_DEBUG_LAYER_MIRRORING, - "Provided surface for layer mirroring on display %d is not present, so do not" + ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, + "Provided surface for recording on display %d is not present, so do not" + " update the surface", mDisplayId); return null; @@ -6284,10 +6337,15 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp } /** - * Returns {@code true} if this DisplayContent is currently layer mirroring. + * Returns {@code true} if this DisplayContent is currently recording. */ - boolean isCurrentlyMirroring() { - return mTokenToMirror != null && mMirroredSurface != null; + boolean isCurrentlyRecording() { + return mContentRecordingSession != null && mRecordedSurface != null; + } + + @VisibleForTesting + @Nullable ContentRecordingSession getContentRecordingSession() { + return mContentRecordingSession; } /** The entry for proceeding to handle {@link #mFixedRotationLaunchingApp}. */ diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 03e21405ff03..fd69f54a7a10 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -229,6 +229,7 @@ import android.util.TimeUtils; import android.util.TypedValue; import android.util.proto.ProtoOutputStream; import android.view.Choreographer; +import android.view.ContentRecordingSession; import android.view.Display; import android.view.DisplayInfo; import android.view.Gravity; @@ -749,6 +750,9 @@ public class WindowManagerService extends IWindowManager.Stub private InputTarget mFocusedInputTarget; @VisibleForTesting + final ContentRecordingController mContentRecordingController = new ContentRecordingController(); + + @VisibleForTesting final class SettingsObserver extends ContentObserver { private final Uri mDisplayInversionEnabledUri = Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED); @@ -2894,6 +2898,16 @@ public class WindowManagerService extends IWindowManager.Stub } } + /** + * Updates the current content mirroring session. + */ + @Override + public void setContentRecordingSession(@Nullable ContentRecordingSession incomingSession) { + synchronized (mGlobalLock) { + mContentRecordingController.setContentRecordingSessionLocked(incomingSession, this); + } + } + // TODO(multi-display): remove when no default display use case. void prepareAppTransitionNone() { if (!checkCallingPermission(MANAGE_APP_TOKENS, "prepareAppTransition()")) { diff --git a/services/tests/wmtests/src/com/android/server/wm/ContentRecordingControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/ContentRecordingControllerTests.java new file mode 100644 index 000000000000..7698033d8df2 --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/ContentRecordingControllerTests.java @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm; + +import static android.view.Display.DEFAULT_DISPLAY; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.never; + +import android.os.Binder; +import android.os.IBinder; +import android.platform.test.annotations.Presubmit; +import android.view.ContentRecordingSession; + +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Tests for the {@link ContentRecordingController} class. + * + * Build/Install/Run: + * atest WmTests:ContentRecordingControllerTests + */ +@SmallTest +@Presubmit +@RunWith(WindowTestRunner.class) +public class ContentRecordingControllerTests extends WindowTestsBase { + private static final IBinder TEST_TOKEN = new RecordingTestToken(); + private final ContentRecordingSession mDefaultSession = + ContentRecordingSession.createDisplaySession( + TEST_TOKEN); + + @Before + public void setup() { + spyOn(mDisplayContent); + mDefaultSession.setDisplayId(DEFAULT_DISPLAY); + } + + @Test + public void testGetContentRecordingSessionLocked() { + ContentRecordingController controller = new ContentRecordingController(); + assertThat(controller.getContentRecordingSessionLocked()).isNull(); + } + + @Test + public void testSetContentRecordingSessionLocked_defaultSession() { + ContentRecordingController controller = new ContentRecordingController(); + controller.setContentRecordingSessionLocked(mDefaultSession, mWm); + ContentRecordingSession resultingSession = controller.getContentRecordingSessionLocked(); + assertThat(resultingSession).isEqualTo(mDefaultSession); + } + + @Test + public void testSetContentRecordingSessionLocked_invalidDisplayId_notAccepted() { + ContentRecordingController controller = new ContentRecordingController(); + // GIVEN an invalid display session (no display id is set). + ContentRecordingSession session = ContentRecordingSession.createDisplaySession(TEST_TOKEN); + // WHEN updating the session. + controller.setContentRecordingSessionLocked(session, mWm); + ContentRecordingSession resultingSession = controller.getContentRecordingSessionLocked(); + // THEN the invalid session was not accepted. + assertThat(resultingSession).isNull(); + } + + @Test + public void testSetContentRecordingSessionLocked_invalidToken_notAccepted() { + ContentRecordingController controller = new ContentRecordingController(); + // GIVEN an invalid display session (null token). + ContentRecordingSession session = ContentRecordingSession.createDisplaySession(null); + session.setDisplayId(DEFAULT_DISPLAY); + // WHEN updating the session. + controller.setContentRecordingSessionLocked(session, mWm); + ContentRecordingSession resultingSession = controller.getContentRecordingSessionLocked(); + // THEN the invalid session was not accepted. + assertThat(resultingSession).isNull(); + } + + @Test + public void testSetContentRecordingSessionLocked_newDisplaySession_accepted() { + ContentRecordingController controller = new ContentRecordingController(); + // GIVEN a valid display session. + ContentRecordingSession session = ContentRecordingSession.createDisplaySession(TEST_TOKEN); + session.setDisplayId(DEFAULT_DISPLAY); + // WHEN updating the session. + controller.setContentRecordingSessionLocked(session, mWm); + ContentRecordingSession resultingSession = controller.getContentRecordingSessionLocked(); + // THEN the valid session was accepted. + assertThat(resultingSession).isEqualTo(session); + verify(mDisplayContent, atLeastOnce()).setContentRecordingSession(session); + } + + @Test + public void testSetContentRecordingSessionLocked_updateCurrentDisplaySession_notAccepted() { + ContentRecordingController controller = new ContentRecordingController(); + // GIVEN a valid display session already in place. + ContentRecordingSession session = ContentRecordingSession.createDisplaySession(TEST_TOKEN); + session.setDisplayId(DEFAULT_DISPLAY); + controller.setContentRecordingSessionLocked(session, mWm); + verify(mDisplayContent, atLeastOnce()).setContentRecordingSession(session); + + // WHEN updating the session. + ContentRecordingSession sessionUpdate = ContentRecordingSession.createDisplaySession( + new RecordingTestToken()); + sessionUpdate.setDisplayId(DEFAULT_DISPLAY); + controller.setContentRecordingSessionLocked(sessionUpdate, mWm); + + ContentRecordingSession resultingSession = controller.getContentRecordingSessionLocked(); + // THEN the session was not accepted. + assertThat(resultingSession).isEqualTo(session); + verify(mDisplayContent, never()).setContentRecordingSession(sessionUpdate); + } + + @Test + public void testSetContentRecordingSessionLocked_disableCurrentDisplaySession_accepted() { + ContentRecordingController controller = new ContentRecordingController(); + // GIVEN a valid display session already in place. + ContentRecordingSession session = ContentRecordingSession.createDisplaySession(TEST_TOKEN); + session.setDisplayId(DEFAULT_DISPLAY); + controller.setContentRecordingSessionLocked(session, mWm); + verify(mDisplayContent, atLeastOnce()).setContentRecordingSession(session); + + // WHEN updating the session. + ContentRecordingSession sessionUpdate = null; + controller.setContentRecordingSessionLocked(sessionUpdate, mWm); + + ContentRecordingSession resultingSession = controller.getContentRecordingSessionLocked(); + // THEN the valid session was accepted. + assertThat(resultingSession).isEqualTo(sessionUpdate); + // Do not need to update the display content, since it will handle stopping the session + // via state change callbacks. + } + + @Test + public void testSetContentRecordingSessionLocked_takeOverCurrentDisplaySession_accepted() { + ContentRecordingController controller = new ContentRecordingController(); + // GIVEN a valid display session already in place. + ContentRecordingSession session = ContentRecordingSession.createDisplaySession(TEST_TOKEN); + session.setDisplayId(DEFAULT_DISPLAY); + controller.setContentRecordingSessionLocked(session, mWm); + verify(mDisplayContent, atLeastOnce()).setContentRecordingSession(session); + + // WHEN updating the session. + final DisplayContent virtualDisplay = new TestDisplayContent.Builder(mAtm, + mDisplayInfo).build(); + ContentRecordingSession sessionUpdate = ContentRecordingSession.createDisplaySession( + TEST_TOKEN); + sessionUpdate.setDisplayId(virtualDisplay.getDisplayId()); + controller.setContentRecordingSessionLocked(sessionUpdate, mWm); + + ContentRecordingSession resultingSession = controller.getContentRecordingSessionLocked(); + // THEN the valid session was accepted. + assertThat(resultingSession).isEqualTo(sessionUpdate); + verify(virtualDisplay).setContentRecordingSession(sessionUpdate); + // THEN the recording was paused on the prior display. + verify(mDisplayContent).pauseRecording(); + + } + + private static class RecordingTestToken extends Binder { + } +} diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java index 925f4f5a4130..b7be932c0fd3 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java @@ -119,6 +119,7 @@ import android.os.SystemClock; import android.platform.test.annotations.Presubmit; import android.util.ArraySet; import android.util.DisplayMetrics; +import android.view.ContentRecordingSession; import android.view.DisplayCutout; import android.view.DisplayInfo; import android.view.Gravity; @@ -2301,8 +2302,18 @@ public class DisplayContentTests extends WindowTestsBase { final DisplayContent virtualDisplay = new TestDisplayContent.Builder(mAtm, mDisplayInfo).build(); + // GIVEN a session is set up to capture a DisplayContent. + ContentRecordingSession session = ContentRecordingSession.createDisplaySession( + tokenToMirror); + session.setDisplayId(virtualDisplay.getDisplayId()); + mWm.mContentRecordingController.setContentRecordingSessionLocked(session, mWm); + + // WHEN attempting to mirror on the virtual display. + virtualDisplay.updateRecording(); + // THEN mirroring is initiated for the default display's DisplayArea. - assertThat(virtualDisplay.mTokenToMirror).isEqualTo(tokenToMirror); + assertThat(virtualDisplay.isCurrentlyRecording()).isTrue(); + assertThat(virtualDisplay.getContentRecordingSession()).isEqualTo(session); mockSession.finishMocking(); } @@ -2329,9 +2340,14 @@ public class DisplayContentTests extends WindowTestsBase { final DisplayContent virtualDisplay = new TestDisplayContent.Builder(mAtm, mDisplayInfo).build(); - // THEN mirroring is initiated for the default display's DisplayArea. - assertThat(virtualDisplay.mTokenToMirror).isEqualTo(tokenToMirror); + // GIVEN a session is set up to capture a DisplayContent. + ContentRecordingSession session = ContentRecordingSession.createDisplaySession( + tokenToMirror); + session.setDisplayId(virtualDisplay.getDisplayId()); + mWm.mContentRecordingController.setContentRecordingSessionLocked(session, mWm); + // WHEN attempting to mirror on the virtual display, and the captured content is resized. + virtualDisplay.updateRecording(); float xScale = 0.7f; float yScale = 2f; Rect displayAreaBounds = new Rect(0, 0, Math.round(surfaceSize.x * xScale), @@ -2359,7 +2375,7 @@ public class DisplayContentTests extends WindowTestsBase { // GIVEN MediaProjection has already initialized the WindowToken of the DisplayArea to // mirror. - setUpDefaultTaskDisplayAreaWindowToken(); + final IBinder tokenToMirror = setUpDefaultTaskDisplayAreaWindowToken(); // GIVEN SurfaceControl does not mirror a null surface. Point surfaceSize = new Point( @@ -2373,9 +2389,15 @@ public class DisplayContentTests extends WindowTestsBase { // WHEN getting the DisplayContent for the new virtual display. DisplayContent actualDC = mWm.mRoot.getDisplayContent(displayId); + ContentRecordingSession session = ContentRecordingSession.createDisplaySession( + tokenToMirror); + session.setDisplayId(displayId); + mWm.mContentRecordingController.setContentRecordingSessionLocked(session, mWm); + actualDC.updateRecording(); // THEN mirroring is not started, since a null surface indicates the VirtualDisplay is off. - assertThat(actualDC.mTokenToMirror).isNull(); + assertThat(actualDC.isCurrentlyRecording()).isFalse(); + assertThat(actualDC.getContentRecordingSession()).isEqualTo(session); display.release(); mockSession.finishMocking(); @@ -2402,44 +2424,27 @@ public class DisplayContentTests extends WindowTestsBase { // GIVEN a new VirtualDisplay with an associated surface. final VirtualDisplay display = createVirtualDisplay(surfaceSize, new Surface()); final int displayId = display.getDisplay().getDisplayId(); + + // GIVEN a session for this display. + ContentRecordingSession session = ContentRecordingSession.createDisplaySession( + tokenToMirror); + session.setDisplayId(displayId); + mWm.setContentRecordingSession(session); mWm.mRoot.onDisplayAdded(displayId); // WHEN getting the DisplayContent for the new virtual display. DisplayContent actualDC = mWm.mRoot.getDisplayContent(displayId); + actualDC.updateRecording(); // THEN mirroring is initiated for the default display's DisplayArea. - assertThat(actualDC.mTokenToMirror).isEqualTo(tokenToMirror); + assertThat(actualDC.isCurrentlyRecording()).isTrue(); + assertThat(actualDC.getContentRecordingSession()).isEqualTo(session); display.release(); mockSession.finishMocking(); } - @Test - public void testKeepClearAreasMultipleWindows() { - final WindowState w1 = createWindow(null, TYPE_NAVIGATION_BAR, mDisplayContent, "w1"); - final Rect rect1 = new Rect(0, 0, 10, 10); - w1.setKeepClearAreas(Arrays.asList(rect1), Collections.emptyList()); - final WindowState w2 = createWindow(null, TYPE_NOTIFICATION_SHADE, mDisplayContent, "w2"); - final Rect rect2 = new Rect(10, 10, 20, 20); - w2.setKeepClearAreas(Arrays.asList(rect2), Collections.emptyList()); - - // No keep clear areas on display, because the windows are not visible - assertEquals(Arrays.asList(), mDisplayContent.getKeepClearAreas()); - - makeWindowVisible(w1); - - // The returned keep-clear areas contain the areas just from the visible window - assertEquals(new ArraySet(Arrays.asList(rect1)), - new ArraySet(mDisplayContent.getKeepClearAreas())); - - makeWindowVisible(w1, w2); - - // The returned keep-clear areas contain the areas from all visible windows - assertEquals(new ArraySet(Arrays.asList(rect1, rect2)), - new ArraySet(mDisplayContent.getKeepClearAreas())); - } - - private class TestToken extends Binder { + private static class MirroringTestToken extends Binder { } /** @@ -2449,10 +2454,7 @@ public class DisplayContentTests extends WindowTestsBase { private IBinder setUpDefaultTaskDisplayAreaWindowToken() { // GIVEN MediaProjection has already initialized the WindowToken of the DisplayArea to // mirror. - final IBinder tokenToMirror = new TestToken(); - doReturn(tokenToMirror).when(mWm.mDisplayManagerInternal).getWindowTokenClientToMirror( - anyInt()); - + final IBinder tokenToMirror = new MirroringTestToken(); // GIVEN the default task display area is represented by the WindowToken. spyOn(mWm.mWindowContextListenerController); doReturn(mDefaultDisplay.getDefaultTaskDisplayArea()).when( @@ -2481,6 +2483,31 @@ public class DisplayContentTests extends WindowTestsBase { DisplayMetrics.DENSITY_140, surface, VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR); } + @Test + public void testKeepClearAreasMultipleWindows() { + final WindowState w1 = createWindow(null, TYPE_NAVIGATION_BAR, mDisplayContent, "w1"); + final Rect rect1 = new Rect(0, 0, 10, 10); + w1.setKeepClearAreas(Arrays.asList(rect1), Collections.emptyList()); + final WindowState w2 = createWindow(null, TYPE_NOTIFICATION_SHADE, mDisplayContent, "w2"); + final Rect rect2 = new Rect(10, 10, 20, 20); + w2.setKeepClearAreas(Arrays.asList(rect2), Collections.emptyList()); + + // No keep clear areas on display, because the windows are not visible + assertEquals(Arrays.asList(), mDisplayContent.getKeepClearAreas()); + + makeWindowVisible(w1); + + // The returned keep-clear areas contain the areas just from the visible window + assertEquals(new ArraySet(Arrays.asList(rect1)), + new ArraySet(mDisplayContent.getKeepClearAreas())); + + makeWindowVisible(w1, w2); + + // The returned keep-clear areas contain the areas from all visible windows + assertEquals(new ArraySet(Arrays.asList(rect1, rect2)), + new ArraySet(mDisplayContent.getKeepClearAreas())); + } + private void removeRootTaskTests(Runnable runnable) { final TaskDisplayArea taskDisplayArea = mRootWindowContainer.getDefaultTaskDisplayArea(); final Task rootTask1 = taskDisplayArea.createRootTask(WINDOWING_MODE_FULLSCREEN, |