From d9a09179e8d3dbcb2efaf38e0ee58f904ba62e72 Mon Sep 17 00:00:00 2001 From: BrickyStudios <125573700+BrickyStudios@users.noreply.github.com> Date: Sun, 25 Oct 2026 00:56:44 +0200 Subject: [PATCH 1/2] add per-app toggles for blocking screenshot and screen recording detection Apps can register for screenshot notifications (Activity.ScreenCaptureCallback, Android 14) and for screen recording state changes (WindowManager#addScreenRecordingCallback, Android 15). The new per-app switches make these callbacks silent for the app. The APIs keep working, so apps don't break. With screenshot detection blocked, the app also isn't named in the "app detected this screenshot" notice. https://github.com/GrapheneOS/os-issue-tracker/issues/2590 Co-Authored-By: Claude Opus 5.5 --- .../content/pm/GosPackageStateFlag.java | 4 ++ .../app/AswBlockScreenRecordingDetection.java | 30 ++++++++++++++ .../app/AswBlockScreenshotDetection.java | 31 +++++++++++++++ .../server/pm/GosPackageStatePermissions.java | 4 ++ .../server/wm/ScreenCaptureAppPolicy.java | 39 +++++++++++++++++++ .../wm/ScreenRecordingCallbackController.java | 6 ++- .../server/wm/WindowManagerService.java | 3 +- 7 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 core/java/android/ext/settings/app/AswBlockScreenRecordingDetection.java create mode 100644 core/java/android/ext/settings/app/AswBlockScreenshotDetection.java create mode 100644 services/core/java/com/android/server/wm/ScreenCaptureAppPolicy.java diff --git a/core/java/android/content/pm/GosPackageStateFlag.java b/core/java/android/content/pm/GosPackageStateFlag.java index ab6429f42c3ea..311490b867633 100644 --- a/core/java/android/content/pm/GosPackageStateFlag.java +++ b/core/java/android/content/pm/GosPackageStateFlag.java @@ -41,6 +41,8 @@ public interface GosPackageStateFlag { /** @hide */ int ALLOW_CLIPBOARD_READ_NON_DEFAULT = 31; /** @hide */ int ALLOW_CLIPBOARD_READ = 32; /** @hide */ int SUPPRESS_CLIPBOARD_ACCESS_DENIAL_NOTIF = 33; + /** @hide */ int BLOCK_SCREENSHOT_DETECTION = 34; + /** @hide */ int BLOCK_SCREEN_RECORDING_DETECTION = 35; /** @hide */ @IntDef(value = { @@ -74,6 +76,8 @@ public interface GosPackageStateFlag { ALLOW_CLIPBOARD_READ_NON_DEFAULT, ALLOW_CLIPBOARD_READ, SUPPRESS_CLIPBOARD_ACCESS_DENIAL_NOTIF, + BLOCK_SCREENSHOT_DETECTION, + BLOCK_SCREEN_RECORDING_DETECTION, }) @Retention(RetentionPolicy.SOURCE) @interface Enum {} diff --git a/core/java/android/ext/settings/app/AswBlockScreenRecordingDetection.java b/core/java/android/ext/settings/app/AswBlockScreenRecordingDetection.java new file mode 100644 index 0000000000000..75425d2cda146 --- /dev/null +++ b/core/java/android/ext/settings/app/AswBlockScreenRecordingDetection.java @@ -0,0 +1,30 @@ +package android.ext.settings.app; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.GosPackageState; +import android.content.pm.GosPackageStateFlag; + +/** + * When on, callbacks registered via WindowManager#addScreenRecordingCallback always report that + * the app isn't being recorded. + * + * @hide + */ +public class AswBlockScreenRecordingDetection extends AppSwitch { + public static final AswBlockScreenRecordingDetection I = new AswBlockScreenRecordingDetection(); + + private AswBlockScreenRecordingDetection() { + gosPsFlag = GosPackageStateFlag.BLOCK_SCREEN_RECORDING_DETECTION; + } + + @Override + protected boolean getDefaultValueInner(Context ctx, int userId, ApplicationInfo appInfo, + GosPackageState ps, StateInfo si) { + return false; + } + + public boolean get(GosPackageState ps) { + return getNonDefaultValue(ps); + } +} diff --git a/core/java/android/ext/settings/app/AswBlockScreenshotDetection.java b/core/java/android/ext/settings/app/AswBlockScreenshotDetection.java new file mode 100644 index 0000000000000..e41c172607e1d --- /dev/null +++ b/core/java/android/ext/settings/app/AswBlockScreenshotDetection.java @@ -0,0 +1,31 @@ +package android.ext.settings.app; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.GosPackageState; +import android.content.pm.GosPackageStateFlag; + +/** + * When on, the app isn't notified about screenshots via Activity.ScreenCaptureCallback and isn't + * listed in the "app detected this screenshot" notice. The callback API keeps working, it just + * never fires. + * + * @hide + */ +public class AswBlockScreenshotDetection extends AppSwitch { + public static final AswBlockScreenshotDetection I = new AswBlockScreenshotDetection(); + + private AswBlockScreenshotDetection() { + gosPsFlag = GosPackageStateFlag.BLOCK_SCREENSHOT_DETECTION; + } + + @Override + protected boolean getDefaultValueInner(Context ctx, int userId, ApplicationInfo appInfo, + GosPackageState ps, StateInfo si) { + return false; + } + + public boolean get(GosPackageState ps) { + return getNonDefaultValue(ps); + } +} diff --git a/services/core/java/com/android/server/pm/GosPackageStatePermissions.java b/services/core/java/com/android/server/pm/GosPackageStatePermissions.java index 66a2b817c80a7..e8b2a54d55e42 100644 --- a/services/core/java/com/android/server/pm/GosPackageStatePermissions.java +++ b/services/core/java/com/android/server/pm/GosPackageStatePermissions.java @@ -28,6 +28,8 @@ import static android.content.pm.GosPackageStateFlag.BLOCK_NATIVE_DEBUGGING_NON_DEFAULT; import static android.content.pm.GosPackageStateFlag.BLOCK_NATIVE_DEBUGGING_SUPPRESS_NOTIF; import static android.content.pm.GosPackageStateFlag.BLOCK_PLAY_INTEGRITY_API; +import static android.content.pm.GosPackageStateFlag.BLOCK_SCREENSHOT_DETECTION; +import static android.content.pm.GosPackageStateFlag.BLOCK_SCREEN_RECORDING_DETECTION; import static android.content.pm.GosPackageStateFlag.CONTACT_SCOPES_ENABLED; import static android.content.pm.GosPackageStateFlag.ENABLE_EXPLOIT_PROTECTION_COMPAT_MODE; import static android.content.pm.GosPackageStateFlag.FORCE_MEMTAG; @@ -151,6 +153,8 @@ static void init(PackageManagerService pm) { ALLOW_CLIPBOARD_READ_NON_DEFAULT, ALLOW_CLIPBOARD_READ, SUPPRESS_CLIPBOARD_ACCESS_DENIAL_NOTIF, + BLOCK_SCREENSHOT_DETECTION, + BLOCK_SCREEN_RECORDING_DETECTION, }; builder() .readWriteFlags(settingsReadWriteFlags) diff --git a/services/core/java/com/android/server/wm/ScreenCaptureAppPolicy.java b/services/core/java/com/android/server/wm/ScreenCaptureAppPolicy.java new file mode 100644 index 0000000000000..f624c8e477dc0 --- /dev/null +++ b/services/core/java/com/android/server/wm/ScreenCaptureAppPolicy.java @@ -0,0 +1,39 @@ +package com.android.server.wm; + +import android.annotation.Nullable; +import android.content.pm.GosPackageState; +import android.ext.settings.app.AswBlockScreenRecordingDetection; +import android.ext.settings.app.AswBlockScreenshotDetection; +import android.os.UserHandle; + +import com.android.server.pm.pkg.PackageStateInternal; + +/** + * Per-app screen capture switches: {@link AswBlockScreenshotDetection} and + * {@link AswBlockScreenRecordingDetection}. + */ +final class ScreenCaptureAppPolicy { + private ScreenCaptureAppPolicy() {} + + private static GosPackageState getGosPackageState(WindowManagerService wms, + @Nullable String packageName, int uid) { + if (packageName == null) { + return GosPackageState.DEFAULT; + } + PackageStateInternal ps = wms.mPmInternal.getPackageStateInternal(packageName); + if (ps == null || ps.getAppId() != UserHandle.getAppId(uid)) { + return GosPackageState.DEFAULT; + } + return ps.getUserStateOrDefault(UserHandle.getUserId(uid)).getGosPackageState(); + } + + static boolean isScreenshotDetectionBlocked(ActivityRecord ar) { + return AswBlockScreenshotDetection.I.get( + getGosPackageState(ar.mWmService, ar.packageName, ar.getUid())); + } + + static boolean isScreenRecordingDetectionBlocked(ActivityRecord ar) { + return AswBlockScreenRecordingDetection.I.get( + getGosPackageState(ar.mWmService, ar.packageName, ar.getUid())); + } +} diff --git a/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java b/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java index 00e1c01bbadb1..ed989f2219cc1 100644 --- a/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java +++ b/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java @@ -235,7 +235,8 @@ private boolean uidHasRecordedActivity(int uid) { } boolean[] hasRecordedActivity = {false}; mRecordedWC.forAllActivities(activityRecord -> { - if (activityRecord.getUid() == uid && activityRecord.isVisibleRequested()) { + if (activityRecord.getUid() == uid && activityRecord.isVisibleRequested() + && !ScreenCaptureAppPolicy.isScreenRecordingDetectionBlocked(activityRecord)) { hasRecordedActivity[0] = true; return true; } @@ -252,7 +253,8 @@ private ArraySet getRecordedUids() { } mRecordedWC.forAllActivities(activityRecord -> { if (activityRecord.isVisibleRequested() && mLastInvokedStateByUid.containsKey( - activityRecord.getUid())) { + activityRecord.getUid()) + && !ScreenCaptureAppPolicy.isScreenRecordingDetectionBlocked(activityRecord)) { result.add(activityRecord.getUid()); } }, true /*traverseTopToBottom*/); diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index dbc1e6b5800b2..07abb6af218d0 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -11186,7 +11186,8 @@ public List notifyScreenshotListeners(int displayId) { displayContent.forAllActivities( (ar) -> { if (!notifiedApps.contains(ar.mActivityComponent) && ar.isVisible() - && ar.isRegisteredForScreenCaptureCallback()) { + && ar.isRegisteredForScreenCaptureCallback() + && !ScreenCaptureAppPolicy.isScreenshotDetectionBlocked(ar)) { ar.reportScreenCaptured(); notifiedApps.add(ar.mActivityComponent); } From fcb5a98705552f194554bf40ef3a7a15ba70f4c6 Mon Sep 17 00:00:00 2001 From: BrickyStudios <125573700+BrickyStudios@users.noreply.github.com> Date: Sun, 25 Oct 2026 00:56:45 +0200 Subject: [PATCH 2/2] add per-app screenshot and screen recording overrides Each app gets two three-state switches, one for screenshots and one for screen recording / screen sharing (MediaProjection): - Default: the app decides via FLAG_SECURE, as before. - Allow: the app's FLAG_SECURE windows are captured. Only the capture aspect of FLAG_SECURE is overridden. Recents thumbnails, scroll capture and mirroring to non-secure displays stay blocked. Device policy and sensitive content protection can't be overridden. - Block: the app's windows are left out of the capture. Screenshots: WindowManagerService#captureDisplay applies the overrides when SystemUI takes the screenshot. Blocked windows are excluded. If an allowed FLAG_SECURE window is visible, secure layers are captured and every window that isn't allowed and may contain secure content (secure windows and untrusted windows, which can contain secure SurfaceViews) is excluded. Blocked apps also get theme-based task snapshots, which covers screenshots taken from recents and from task snapshots, and they're skipped for scroll capture. Screen recording: SensitiveContentProtectionManagerService passes the per-app choices to WindowManager when a MediaProjection session starts, including for recorders exempted from sensitive content protection such as the SystemUI screen recorder. Blocked apps become secure for the duration of the session and their stale task snapshots are dropped. Allowed apps get a non-secure surface during the session while WindowState#isSecureLocked() stays true, so their recents thumbnails remain protected. https://github.com/GrapheneOS/os-issue-tracker/issues/664 https://github.com/GrapheneOS/os-issue-tracker/issues/2931 Co-Authored-By: Claude Opus 5.5 --- .../content/pm/GosPackageStateFlag.java | 8 ++ .../android/ext/settings/app/AppSwitch.java | 5 ++ .../settings/app/AswAllowScreenRecording.java | 35 ++++++++ .../ext/settings/app/AswAllowScreenshots.java | 34 ++++++++ .../android/window/ScreenCaptureInternal.java | 6 ++ ...sitiveContentProtectionManagerService.java | 31 +++++++ .../server/pm/GosPackageStatePermissions.java | 8 ++ .../com/android/server/wm/ActivityRecord.java | 4 +- .../com/android/server/wm/DisplayContent.java | 3 +- .../server/wm/RootWindowContainer.java | 2 +- .../server/wm/ScreenCaptureAppPolicy.java | 84 ++++++++++++++++++- .../server/wm/SensitiveContentPackages.java | 37 ++++++++ .../server/wm/WindowManagerInternal.java | 10 +++ .../server/wm/WindowManagerService.java | 71 ++++++++++++++-- .../com/android/server/wm/WindowState.java | 34 +++++++- ...ntProtectionManagerServiceContentTest.java | 60 +++++++++++++ .../wm/SensitiveContentPackagesTest.java | 23 +++++ 17 files changed, 445 insertions(+), 10 deletions(-) create mode 100644 core/java/android/ext/settings/app/AswAllowScreenRecording.java create mode 100644 core/java/android/ext/settings/app/AswAllowScreenshots.java diff --git a/core/java/android/content/pm/GosPackageStateFlag.java b/core/java/android/content/pm/GosPackageStateFlag.java index 311490b867633..85df22c8f6f81 100644 --- a/core/java/android/content/pm/GosPackageStateFlag.java +++ b/core/java/android/content/pm/GosPackageStateFlag.java @@ -43,6 +43,10 @@ public interface GosPackageStateFlag { /** @hide */ int SUPPRESS_CLIPBOARD_ACCESS_DENIAL_NOTIF = 33; /** @hide */ int BLOCK_SCREENSHOT_DETECTION = 34; /** @hide */ int BLOCK_SCREEN_RECORDING_DETECTION = 35; + /** @hide */ int ALLOW_SCREENSHOTS_NON_DEFAULT = 36; + /** @hide */ int ALLOW_SCREENSHOTS = 37; + /** @hide */ int ALLOW_SCREEN_RECORDING_NON_DEFAULT = 38; + /** @hide */ int ALLOW_SCREEN_RECORDING = 39; /** @hide */ @IntDef(value = { @@ -78,6 +82,10 @@ public interface GosPackageStateFlag { SUPPRESS_CLIPBOARD_ACCESS_DENIAL_NOTIF, BLOCK_SCREENSHOT_DETECTION, BLOCK_SCREEN_RECORDING_DETECTION, + ALLOW_SCREENSHOTS_NON_DEFAULT, + ALLOW_SCREENSHOTS, + ALLOW_SCREEN_RECORDING_NON_DEFAULT, + ALLOW_SCREEN_RECORDING, }) @Retention(RetentionPolicy.SOURCE) @interface Enum {} diff --git a/core/java/android/ext/settings/app/AppSwitch.java b/core/java/android/ext/settings/app/AppSwitch.java index 59bda5ebfed1f..661a4902fb49b 100644 --- a/core/java/android/ext/settings/app/AppSwitch.java +++ b/core/java/android/ext/settings/app/AppSwitch.java @@ -155,6 +155,11 @@ protected final boolean getNonDefaultValue(GosPackageState ps) { return gosPsFlagInverted ? !value : value; } + // returns null if default value is used, for switches whose default isn't a fixed value + public final Boolean getNonDefaultValueOrNull(GosPackageState ps) { + return isUsingDefaultValue(ps) ? null : getNonDefaultValue(ps); + } + public final void setUseDefaultValue(GosPackageState.Editor ed) { ed.clearFlag(gosPsFlagNonDefault); ed.clearFlag(gosPsFlag); diff --git a/core/java/android/ext/settings/app/AswAllowScreenRecording.java b/core/java/android/ext/settings/app/AswAllowScreenRecording.java new file mode 100644 index 0000000000000..6870bc1c2dd97 --- /dev/null +++ b/core/java/android/ext/settings/app/AswAllowScreenRecording.java @@ -0,0 +1,35 @@ +package android.ext.settings.app; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.GosPackageState; +import android.content.pm.GosPackageStateFlag; + +/** + * Per-app override of whether screen recording and screen sharing (MediaProjection) may capture + * the app's windows. + *

+ * Default: the app decides, i.e. windows with FLAG_SECURE are redacted.
+ * On: windows with FLAG_SECURE are captured while a MediaProjection session is active. Recents + * thumbnails and long screenshots stay blocked.
+ * Off: the app's windows are redacted while a MediaProjection session is active. + *

+ * Use {@link #getNonDefaultValueOrNull(GosPackageState)} to check whether there's an override. + * + * @hide + */ +public class AswAllowScreenRecording extends AppSwitch { + public static final AswAllowScreenRecording I = new AswAllowScreenRecording(); + + private AswAllowScreenRecording() { + gosPsFlagNonDefault = GosPackageStateFlag.ALLOW_SCREEN_RECORDING_NON_DEFAULT; + gosPsFlag = GosPackageStateFlag.ALLOW_SCREEN_RECORDING; + } + + @Override + protected boolean getDefaultValueInner(Context ctx, int userId, ApplicationInfo appInfo, + GosPackageState ps, StateInfo si) { + // screen recording is allowed unless the app opts out via FLAG_SECURE + return true; + } +} diff --git a/core/java/android/ext/settings/app/AswAllowScreenshots.java b/core/java/android/ext/settings/app/AswAllowScreenshots.java new file mode 100644 index 0000000000000..5590a42825959 --- /dev/null +++ b/core/java/android/ext/settings/app/AswAllowScreenshots.java @@ -0,0 +1,34 @@ +package android.ext.settings.app; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.GosPackageState; +import android.content.pm.GosPackageStateFlag; + +/** + * Per-app override of whether user-initiated screenshots may capture the app's windows. + *

+ * Default: the app decides, i.e. windows with FLAG_SECURE are redacted.
+ * On: windows with FLAG_SECURE are captured. Other FLAG_SECURE properties (hidden recents + * thumbnail, no mirroring to non-secure displays, screen recording) are unaffected.
+ * Off: the app's windows are excluded from screenshots and its recents thumbnail is hidden. + *

+ * Use {@link #getNonDefaultValueOrNull(GosPackageState)} to check whether there's an override. + * + * @hide + */ +public class AswAllowScreenshots extends AppSwitch { + public static final AswAllowScreenshots I = new AswAllowScreenshots(); + + private AswAllowScreenshots() { + gosPsFlagNonDefault = GosPackageStateFlag.ALLOW_SCREENSHOTS_NON_DEFAULT; + gosPsFlag = GosPackageStateFlag.ALLOW_SCREENSHOTS; + } + + @Override + protected boolean getDefaultValueInner(Context ctx, int userId, ApplicationInfo appInfo, + GosPackageState ps, StateInfo si) { + // screenshots are allowed unless the app opts out via FLAG_SECURE + return true; + } +} diff --git a/core/java/android/window/ScreenCaptureInternal.java b/core/java/android/window/ScreenCaptureInternal.java index 3585ddaef12a7..da79f0885e1d6 100644 --- a/core/java/android/window/ScreenCaptureInternal.java +++ b/core/java/android/window/ScreenCaptureInternal.java @@ -380,6 +380,12 @@ private CaptureArgs(Parcel in) { mExclusionMask = in.readInt(); } + /** Layers set using {@link Builder#setExcludeLayers(SurfaceControl[])}, if any. */ + @Nullable + public SurfaceControl[] getExcludeLayers() { + return mExcludeLayers; + } + /** Release any layers if set using {@link Builder#setExcludeLayers(SurfaceControl[])}. */ public void release() { if (mExcludeLayers == null || mExcludeLayers.length == 0) { diff --git a/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java b/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java index cd632e6382816..4d3640b9760c5 100644 --- a/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java +++ b/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java @@ -37,6 +37,7 @@ import android.content.Context; import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; +import android.ext.settings.app.AswAllowScreenRecording; import android.media.projection.MediaProjectionInfo; import android.media.projection.MediaProjectionManager; import android.os.Binder; @@ -55,6 +56,7 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.FrameworkStatsLog; +import com.android.server.pm.pkg.PackageUserStateInternal; import com.android.server.wm.SensitiveContentPackages.PackageInfo; import com.android.server.wm.WindowManagerInternal; @@ -317,6 +319,9 @@ private ArraySet getExemptedPackages() { } private void onProjectionStart(MediaProjectionInfo projectionInfo) { + // the user's per-app choices apply regardless of the exemptions below + updateScreenRecordingOverrides(); + boolean isPackageExempted = (mExemptedPackages != null && mExemptedPackages.contains( projectionInfo.getPackageName())) || canRecordSensitiveContent(projectionInfo.getPackageName()) @@ -377,9 +382,35 @@ private void onProjectionEnd() { // notify windowmanager to clear any sensitive notifications observed during projection // session mWindowManager.clearBlockedApps(); + mWindowManager.setScreenRecordingOverrides(new ArraySet<>(), new ArraySet<>()); } } + // shortcut: overrides are computed once per projection session, changes made during the + // session apply to the next one + private void updateScreenRecordingOverrides() { + final ArraySet blocked = new ArraySet<>(); + final ArraySet allowed = new ArraySet<>(); + mPackageManagerInternal.forEachPackageState(ps -> { + final var userStates = ps.getUserStates(); + for (int i = 0, n = userStates.size(); i < n; ++i) { + final PackageUserStateInternal userState = userStates.valueAt(i); + if (!userState.isInstalled()) { + continue; + } + final Boolean allow = AswAllowScreenRecording.I.getNonDefaultValueOrNull( + userState.getGosPackageState()); + if (allow == null) { + continue; + } + final var info = new PackageInfo(ps.getPackageName(), + UserHandle.getUid(userStates.keyAt(i), ps.getAppId())); + (allow ? allowed : blocked).add(info); + } + }); + mWindowManager.setScreenRecordingOverrides(blocked, allowed); + } + @GuardedBy("mSensitiveContentProtectionLock") private void updateAppsThatShouldBlockScreenCapture() { RankingMap rankingMap; diff --git a/services/core/java/com/android/server/pm/GosPackageStatePermissions.java b/services/core/java/com/android/server/pm/GosPackageStatePermissions.java index e8b2a54d55e42..7c403b416cfe6 100644 --- a/services/core/java/com/android/server/pm/GosPackageStatePermissions.java +++ b/services/core/java/com/android/server/pm/GosPackageStatePermissions.java @@ -24,6 +24,10 @@ import static android.content.pm.GosPackageStateFlag.ALLOW_ACCESS_TO_OBB_DIRECTORY; import static android.content.pm.GosPackageStateFlag.ALLOW_CLIPBOARD_READ; import static android.content.pm.GosPackageStateFlag.ALLOW_CLIPBOARD_READ_NON_DEFAULT; +import static android.content.pm.GosPackageStateFlag.ALLOW_SCREENSHOTS; +import static android.content.pm.GosPackageStateFlag.ALLOW_SCREENSHOTS_NON_DEFAULT; +import static android.content.pm.GosPackageStateFlag.ALLOW_SCREEN_RECORDING; +import static android.content.pm.GosPackageStateFlag.ALLOW_SCREEN_RECORDING_NON_DEFAULT; import static android.content.pm.GosPackageStateFlag.BLOCK_NATIVE_DEBUGGING; import static android.content.pm.GosPackageStateFlag.BLOCK_NATIVE_DEBUGGING_NON_DEFAULT; import static android.content.pm.GosPackageStateFlag.BLOCK_NATIVE_DEBUGGING_SUPPRESS_NOTIF; @@ -155,6 +159,10 @@ static void init(PackageManagerService pm) { SUPPRESS_CLIPBOARD_ACCESS_DENIAL_NOTIF, BLOCK_SCREENSHOT_DETECTION, BLOCK_SCREEN_RECORDING_DETECTION, + ALLOW_SCREENSHOTS_NON_DEFAULT, + ALLOW_SCREENSHOTS, + ALLOW_SCREEN_RECORDING_NON_DEFAULT, + ALLOW_SCREEN_RECORDING, }; builder() .readWriteFlags(settingsReadWriteFlags) diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 88afb1ad1f2ef..339a35d5915d6 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -5723,7 +5723,9 @@ void setRecentsScreenshotEnabled(boolean enabled) { */ boolean shouldUseAppThemeSnapshot() { return !mEnableRecentsScreenshot || forAllWindows(WindowState::isSecureLocked, - true /* topToBottom */); + true /* topToBottom */) + // also covers screenshots that are taken from task snapshots + || ScreenCaptureAppPolicy.areScreenshotsBlocked(this); } /** diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index c55085d3e4161..6c06717fa33e4 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -7266,7 +7266,8 @@ public boolean test(WindowState nextWindow) { return false; /* continue */ } } - if (nextWindow.isSecureLocked()) { + if (nextWindow.isSecureLocked() + || ScreenCaptureAppPolicy.areScreenshotsBlocked(nextWindow)) { return false; /* continue */ } return true; /* stop, match found */ diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java index d4d14a9ae3712..881fbfcbda201 100644 --- a/services/core/java/com/android/server/wm/RootWindowContainer.java +++ b/services/core/java/com/android/server/wm/RootWindowContainer.java @@ -612,7 +612,7 @@ void dispatchConfigurationToChild(DisplayContent child, Configuration config) { void refreshSecureSurfaceState() { forAllWindows(w -> { - w.setSecureLocked(w.isSecureLocked()); + w.setSecureLocked(w.isSurfaceSecureLocked()); }, true /* traverseTopToBottom */); forAllTasks(t -> { t.setSecure(t.isSecure()); diff --git a/services/core/java/com/android/server/wm/ScreenCaptureAppPolicy.java b/services/core/java/com/android/server/wm/ScreenCaptureAppPolicy.java index f624c8e477dc0..607170af82d87 100644 --- a/services/core/java/com/android/server/wm/ScreenCaptureAppPolicy.java +++ b/services/core/java/com/android/server/wm/ScreenCaptureAppPolicy.java @@ -2,14 +2,20 @@ import android.annotation.Nullable; import android.content.pm.GosPackageState; +import android.ext.settings.app.AswAllowScreenRecording; +import android.ext.settings.app.AswAllowScreenshots; import android.ext.settings.app.AswBlockScreenRecordingDetection; import android.ext.settings.app.AswBlockScreenshotDetection; import android.os.UserHandle; +import android.view.SurfaceControl; import com.android.server.pm.pkg.PackageStateInternal; +import java.util.ArrayList; + /** - * Per-app screen capture switches: {@link AswBlockScreenshotDetection} and + * Per-app screen capture switches: {@link AswAllowScreenshots}, {@link AswAllowScreenRecording} + * (applied via {@link SensitiveContentPackages}), {@link AswBlockScreenshotDetection} and * {@link AswBlockScreenRecordingDetection}. */ final class ScreenCaptureAppPolicy { @@ -36,4 +42,80 @@ static boolean isScreenRecordingDetectionBlocked(ActivityRecord ar) { return AswBlockScreenRecordingDetection.I.get( getGosPackageState(ar.mWmService, ar.packageName, ar.getUid())); } + + // null means that the app decides via FLAG_SECURE + @Nullable + private static Boolean getScreenshotOverride(WindowState w) { + return AswAllowScreenshots.I.getNonDefaultValueOrNull( + getGosPackageState(w.mWmService, w.getOwningPackage(), w.getOwningUid())); + } + + static boolean areScreenshotsBlocked(WindowState w) { + return Boolean.FALSE.equals(getScreenshotOverride(w)); + } + + static boolean areScreenshotsBlocked(ActivityRecord ar) { + return Boolean.FALSE.equals(AswAllowScreenshots.I.getNonDefaultValueOrNull( + getGosPackageState(ar.mWmService, ar.packageName, ar.getUid()))); + } + + /** + * Applies per-app screenshot overrides to a user-initiated screenshot of {@code dc}. + * + * @param outExcludeLayers receives copies of the surfaces of windows that must be excluded, + * the caller is responsible for releasing them + * @return whether secure layers should be captured + */ + static boolean applyScreenshotOverrides(DisplayContent dc, + ArrayList outExcludeLayers) { + // windows that must be excluded if secure layers are captured + final ArrayList maybeSecureWindows = new ArrayList<>(); + final boolean[] captureSecureLayers = new boolean[1]; + dc.forAllWindows(w -> { + final Boolean override = getScreenshotOverride(w); + if (Boolean.FALSE.equals(override)) { + addExcludeLayer(w, outExcludeLayers); + return; + } + final boolean allowed = Boolean.TRUE.equals(override); + if (!w.isSecureLocked()) { + // untrusted windows might contain secure layers that WM doesn't know about, + // e.g. a SurfaceView marked as secure + if (!allowed && !w.mSession.mCanAddInternalSystemWindow) { + maybeSecureWindows.add(w); + } + return; + } + if (allowed && !w.isSecureIgnoringFlagSecureLocked()) { + // only the app's own FLAG_SECURE can be overridden, not e.g. device policy + captureSecureLayers[0] = true; + } else if (!w.isSurfaceSecureLocked()) { + // the surface isn't secure due to AswAllowScreenRecording, the window is still + // secure as far as screenshots are concerned + addExcludeLayer(w, outExcludeLayers); + } else { + maybeSecureWindows.add(w); + } + }, true /* traverseTopToBottom */); + + // transitions can add snapshot layers that contain secure content of other apps and + // aren't windows, fall back to redacting secure content + if (captureSecureLayers[0] && dc.mTransitionController.inTransition()) { + return false; + } + if (captureSecureLayers[0]) { + for (int i = 0, n = maybeSecureWindows.size(); i < n; ++i) { + addExcludeLayer(maybeSecureWindows.get(i), outExcludeLayers); + } + } + return captureSecureLayers[0]; + } + + private static void addExcludeLayer(WindowState w, ArrayList out) { + final SurfaceControl sc = w.getSurfaceControl(); + if (sc != null && sc.isValid()) { + // a copy is needed, capture args are released after the capture + out.add(new SurfaceControl(sc, "ScreenCaptureAppPolicy")); + } + } } diff --git a/services/core/java/com/android/server/wm/SensitiveContentPackages.java b/services/core/java/com/android/server/wm/SensitiveContentPackages.java index 9ab11b8059a75..00892de4a59f7 100644 --- a/services/core/java/com/android/server/wm/SensitiveContentPackages.java +++ b/services/core/java/com/android/server/wm/SensitiveContentPackages.java @@ -36,6 +36,11 @@ public class SensitiveContentPackages { private final ArraySet mProtectedPackages = new ArraySet<>(); + // Per-app screen recording overrides (android.ext.settings.app.AswAllowScreenRecording), + // populated only while a media projection session is active + private final ArraySet mScreenRecordingBlockedPackages = new ArraySet<>(); + private final ArraySet mScreenRecordingAllowedPackages = new ArraySet<>(); + /** * Returns {@code true} if package/uid/window combination should be blocked * from screen capture. @@ -100,6 +105,34 @@ public boolean clearBlockedApps() { return true; } + public boolean isScreenRecordingBlockedForApp(String pkg, int uid) { + return !mScreenRecordingBlockedPackages.isEmpty() + && mScreenRecordingBlockedPackages.contains(new PackageInfo(pkg, uid)); + } + + public boolean isScreenRecordingAllowedForApp(String pkg, int uid) { + return !mScreenRecordingAllowedPackages.isEmpty() + && mScreenRecordingAllowedPackages.contains(new PackageInfo(pkg, uid)); + } + + /** + * Replaces per-app screen recording overrides. + * + * @return {@code true} if the overrides changed, {@code false} otherwise. + */ + public boolean setScreenRecordingOverrides(@NonNull ArraySet blocked, + @NonNull ArraySet allowed) { + if (mScreenRecordingBlockedPackages.equals(blocked) + && mScreenRecordingAllowedPackages.equals(allowed)) { + return false; + } + mScreenRecordingBlockedPackages.clear(); + mScreenRecordingBlockedPackages.addAll(blocked); + mScreenRecordingAllowedPackages.clear(); + mScreenRecordingAllowedPackages.addAll(allowed); + return true; + } + /** * @return the size of protected packages. */ @@ -117,6 +150,10 @@ void dump(PrintWriter pw) { pw.println(innerPrefix + " package=" + info.mPkg + " uid=" + info.mUid + " windowToken=" + info.mWindowToken); } + pw.println(innerPrefix + "Packages blocked from screen recording by user: " + + mScreenRecordingBlockedPackages); + pw.println(innerPrefix + "Packages allowed to be screen recorded by user: " + + mScreenRecordingAllowedPackages); } /** diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java index e12ad800ab474..d6a155e546494 100644 --- a/services/core/java/com/android/server/wm/WindowManagerInternal.java +++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java @@ -1215,6 +1215,16 @@ public abstract void removeBlockScreenCaptureForApps( */ public abstract void clearBlockedApps(); + /** + * Sets per-app screen recording overrides for the active media projection session, see + * {@link android.ext.settings.app.AswAllowScreenRecording}. Empty sets clear the overrides. + * + * @param blocked apps whose windows are redacted from screen capture + * @param allowed apps whose FLAG_SECURE windows aren't redacted from screen capture + */ + public abstract void setScreenRecordingOverrides(@NonNull ArraySet blocked, + @NonNull ArraySet allowed); + /** * Register a listener to receive a callback on window removal. * diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 07abb6af218d0..9cce6a6ddb4b4 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -114,6 +114,7 @@ import static android.view.flags.Flags.sensitiveContentAppProtection; import static android.window.ScreenCapture.ScreenCaptureParams.CAPTURE_MODE_REQUIRE_OPTIMIZED; import static android.window.ScreenCapture.ScreenCaptureParams.PROTECTED_CONTENT_POLICY_THROW_EXCEPTION; +import static android.window.ScreenCapture.ScreenCaptureParams.SECURE_CONTENT_POLICY_CAPTURE; import static android.window.ScreenCapture.ScreenCaptureParams.SECURE_CONTENT_POLICY_THROW_EXCEPTION; import static android.window.WindowProviderService.isWindowProviderService; @@ -351,6 +352,7 @@ import com.android.internal.policy.KeyInterceptionInfo; import com.android.internal.protolog.ProtoLog; import com.android.internal.protolog.WmProtoLogGroups; +import com.android.internal.util.ArrayUtils; import com.android.internal.util.DumpUtils; import com.android.internal.util.FastPrintWriter; import com.android.internal.util.FrameworkStatsLog; @@ -2749,7 +2751,7 @@ public int relayoutWindow(Session session, IWindow client, LayoutParams attrs, && win.hasWallpaper(); wallpaperMayMove |= (flagChanges & FLAG_SHOW_WALLPAPER) != 0; if ((flagChanges & FLAG_SECURE) != 0) { - win.setSecureLocked(win.isSecureLocked()); + win.setSecureLocked(win.isSurfaceSecureLocked()); } final boolean wasVisible = win.isVisible(); @@ -9542,6 +9544,28 @@ public void clearBlockedApps() { } } + @Override + public void setScreenRecordingOverrides(ArraySet blocked, + ArraySet allowed) { + synchronized (mGlobalLock) { + if (!mSensitiveContentPackages.setScreenRecordingOverrides(blocked, allowed)) { + return; + } + WindowManagerService.this.refreshScreenCaptureDisabled(); + // recents thumbnails of blocked apps would otherwise be visible in the recording + mRoot.forAllWindows((w) -> { + final Task task = w.getTask(); + if (task != null && !w.isVisible() + && mSensitiveContentPackages.isScreenRecordingBlockedForApp( + w.getOwningPackage(), w.getOwningUid())) { + mTaskSnapshotController.removeAndDeleteSnapshot( + task.mTaskId, task.mUserId); + task.onSnapshotReleased(); + } + }, /* traverseTopToBottom= */ true); + } + } + @Override public void registerWindowFocusChangeListener(WindowFocusChangeListener listener) { WindowManagerService.this.addWindowFocusChangeListener(listener); @@ -11001,8 +11025,14 @@ public void captureDisplay( throw new SecurityException("Requires READ_FRAME_BUFFER permission"); } + // per-app screenshot overrides apply only to screenshots taken by SystemUI + final ArrayList appOverrideExcludeLayers = + Binder.getCallingUid() != SYSTEM_UID + && mContext.checkCallingPermission(STATUS_BAR_SERVICE) + == PackageManager.PERMISSION_GRANTED + ? new ArrayList<>() : null; ScreenCaptureInternal.LayerCaptureArgs layerCaptureArgs = - getCaptureArgs(displayId, captureArgs); + getCaptureArgs(displayId, captureArgs, appOverrideExcludeLayers); ScreenCaptureInternal.captureLayers(layerCaptureArgs, listener); if (Binder.getCallingUid() != SYSTEM_UID) { @@ -11010,6 +11040,11 @@ public void captureDisplay( // parcelling occurs in this case. layerCaptureArgs.release(); } + if (appOverrideExcludeLayers != null) { + for (int i = 0, n = appOverrideExcludeLayers.size(); i < n; ++i) { + appOverrideExcludeLayers.get(i).release(); + } + } } @Override @@ -11083,7 +11118,19 @@ public void screenCapture( @VisibleForTesting ScreenCaptureInternal.LayerCaptureArgs getCaptureArgs( int displayId, @Nullable ScreenCaptureInternal.CaptureArgs captureArgs) { + return getCaptureArgs(displayId, captureArgs, null); + } + + /** + * @param appOverrideExcludeLayers if non-null, per-app screenshot overrides are applied and the + * surface copies that they exclude are added to it + */ + @VisibleForTesting + ScreenCaptureInternal.LayerCaptureArgs getCaptureArgs( + int displayId, @Nullable ScreenCaptureInternal.CaptureArgs captureArgs, + @Nullable ArrayList appOverrideExcludeLayers) { final SurfaceControl displaySurfaceControl; + boolean captureSecureLayers = false; synchronized (mGlobalLock) { DisplayContent displayContent = mRoot.getDisplayContent(displayId); if (displayContent == null) { @@ -11103,12 +11150,26 @@ ScreenCaptureInternal.LayerCaptureArgs getCaptureArgs( } else { mTmpRect.set(captureArgs.mSourceCrop); } + + if (appOverrideExcludeLayers != null) { + captureSecureLayers = ScreenCaptureAppPolicy.applyScreenshotOverrides( + displayContent, appOverrideExcludeLayers); + } } - return new ScreenCaptureInternal.LayerCaptureArgs.Builder( + final ScreenCaptureInternal.LayerCaptureArgs.Builder builder = + new ScreenCaptureInternal.LayerCaptureArgs.Builder( displaySurfaceControl, captureArgs) - .setSourceCrop(mTmpRect) - .build(); + .setSourceCrop(mTmpRect); + if (appOverrideExcludeLayers != null && !appOverrideExcludeLayers.isEmpty()) { + builder.setExcludeLayers(ArrayUtils.concat(SurfaceControl.class, + captureArgs.getExcludeLayers(), + appOverrideExcludeLayers.toArray(new SurfaceControl[0]))); + } + if (captureSecureLayers) { + builder.setSecureContentPolicy(SECURE_CONTENT_POLICY_CAPTURE); + } + return builder.build(); } @Override diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index dc2f6c9741e41..8b59aeef0cc8c 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -1197,7 +1197,7 @@ private boolean calculateUsesSyncedInsetsAnimation() { void setInitialSurfaceControlProperties(SurfaceControl.Builder b) { super.setInitialSurfaceControlProperties(b); getPendingTransaction().setTrustedOverlay(mSurfaceControl, isWindowTrustedOverlay()); - getPendingTransaction().setSecure(mSurfaceControl, isSecureLocked()); + getPendingTransaction().setSecure(mSurfaceControl, isSurfaceSecureLocked()); // All apps should be considered as occluding when computing TrustedPresentation Thresholds. getPendingTransaction().setCanOccludePresentation(mSurfaceControl, canOccludePresentation()); @@ -1969,15 +1969,47 @@ boolean isSecureLocked() { return true; } + return isSecureIgnoringFlagSecureLocked(); + } + + /** + * Whether the window is secure for reasons other than the app's own FLAG_SECURE request, which + * is the only reason that per-app screen capture overrides can override. + */ + boolean isSecureIgnoringFlagSecureLocked() { + if (mWmService.getDisableSecureWindows()) { + return false; + } + // block screen capture to protect sensitive notifications or content on the screen. if (mWmService.mSensitiveContentPackages.shouldBlockScreenCaptureForApp( getOwningPackage(), getOwningUid(), getWindowToken())) { return true; } + if (mWmService.mSensitiveContentPackages.isScreenRecordingBlockedForApp( + getOwningPackage(), getOwningUid())) { + return true; + } + return !DevicePolicyCache.getInstance().isScreenCaptureAllowed(mShowUserId); } + /** + * Whether the surface should be marked as secure. Differs from {@link #isSecureLocked()} only + * during media projection for apps that are allowed to be recorded despite FLAG_SECURE, see + * {@link android.ext.settings.app.AswAllowScreenRecording}. isSecureLocked() stays true for + * them, which keeps their recents thumbnail and scroll capture blocked. + */ + boolean isSurfaceSecureLocked() { + if (!isSecureLocked()) { + return false; + } + return isSecureIgnoringFlagSecureLocked() + || !mWmService.mSensitiveContentPackages.isScreenRecordingAllowedForApp( + getOwningPackage(), getOwningUid()); + } + /** * Whether this window's drawn state might affect the drawn states of the app token. * diff --git a/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceContentTest.java b/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceContentTest.java index 2e315ecd7b375..be298f2a0856e 100644 --- a/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceContentTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceContentTest.java @@ -24,17 +24,23 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.role.RoleManager; import android.companion.AssociationRequest; +import android.content.pm.GosPackageState; +import android.content.pm.GosPackageStateFlag; import android.content.pm.PackageManagerInternal; import android.media.projection.MediaProjectionInfo; import android.media.projection.MediaProjectionManager; import android.os.Binder; import android.os.Process; +import android.os.UserHandle; import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; @@ -42,9 +48,12 @@ import android.testing.AndroidTestingRunner; import android.testing.TestableContext; import android.util.ArraySet; +import android.util.SparseArray; import androidx.test.filters.SmallTest; +import com.android.server.pm.pkg.PackageStateInternal; +import com.android.server.pm.pkg.PackageUserStateInternal; import com.android.server.wm.SensitiveContentPackages.PackageInfo; import com.android.server.wm.WindowManagerInternal; @@ -59,6 +68,7 @@ import java.util.List; import java.util.Set; +import java.util.function.Consumer; @SmallTest @RunWith(AndroidTestingRunner.class) @@ -153,6 +163,56 @@ public void testAppWindowsAreUnblockedOnScreenCaptureEnd() { verify(mWindowManager).clearBlockedApps(); } + @Test + public void testScreenRecordingOverridesApplyToExemptedRecorder() { + int userId = Process.myUserHandle().getIdentifier(); + PackageStateInternal blockedApp = mockPackageState("test.blocked", 10001, userId, false); + PackageStateInternal allowedApp = mockPackageState("test.allowed", 10002, userId, true); + PackageStateInternal defaultApp = mockPackageState("test.default", 10003, userId, null); + doAnswer(invocation -> { + Consumer action = invocation.getArgument(0); + action.accept(blockedApp); + action.accept(allowedApp); + action.accept(defaultApp); + return null; + }).when(mPackageManagerInternal).forEachPackageState(any()); + + MediaProjectionInfo exemptedRecorderPackage = new MediaProjectionInfo( + mExemptedScreenRecorderPackage, Process.myUserHandle(), null); + mMediaPorjectionCallback.onStart(exemptedRecorderPackage); + verify(mWindowManager).setScreenRecordingOverrides( + new ArraySet<>(Set.of(new PackageInfo("test.blocked", + UserHandle.getUid(userId, 10001)))), + new ArraySet<>(Set.of(new PackageInfo("test.allowed", + UserHandle.getUid(userId, 10002))))); + + mMediaPorjectionCallback.onStop(exemptedRecorderPackage); + verify(mWindowManager).setScreenRecordingOverrides(new ArraySet<>(), new ArraySet<>()); + } + + private static PackageStateInternal mockPackageState(String packageName, int appId, + int userId, Boolean allowScreenRecording) { + long flags = 0; + if (allowScreenRecording != null) { + flags |= 1L << GosPackageStateFlag.ALLOW_SCREEN_RECORDING_NON_DEFAULT; + if (allowScreenRecording) { + flags |= 1L << GosPackageStateFlag.ALLOW_SCREEN_RECORDING; + } + } + PackageUserStateInternal userState = mock(PackageUserStateInternal.class); + when(userState.isInstalled()).thenReturn(true); + when(userState.getGosPackageState()).thenReturn( + new GosPackageState(flags, 0L, null, null)); + SparseArray userStates = new SparseArray<>(); + userStates.put(userId, userState); + + PackageStateInternal ps = mock(PackageStateInternal.class); + when(ps.getPackageName()).thenReturn(packageName); + when(ps.getAppId()).thenReturn(appId); + doReturn(userStates).when(ps).getUserStates(); + return ps; + } + @Test public void testAutofillServicePackageExemption() { String testAutofillService = mScreenRecorderPackage + "/com.example.SampleAutofillService"; diff --git a/services/tests/wmtests/src/com/android/server/wm/SensitiveContentPackagesTest.java b/services/tests/wmtests/src/com/android/server/wm/SensitiveContentPackagesTest.java index 824b8d7f6727c..5f399f039e0b5 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SensitiveContentPackagesTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/SensitiveContentPackagesTest.java @@ -182,4 +182,27 @@ public void clearBlockedApps_alreadyEmpty() { assertFalse(modified); assertTrue(mSensitiveContentPackages.size() == 0); } + + @Test + public void setScreenRecordingOverrides() { + ArraySet blocked = new ArraySet( + Set.of(new PackageInfo(APP_PKG_1, APP_UID_1))); + ArraySet allowed = new ArraySet( + Set.of(new PackageInfo(APP_PKG_2, APP_UID_2))); + + assertTrue(mSensitiveContentPackages.setScreenRecordingOverrides(blocked, allowed)); + assertFalse(mSensitiveContentPackages.setScreenRecordingOverrides(blocked, allowed)); + + assertTrue(mSensitiveContentPackages.isScreenRecordingBlockedForApp(APP_PKG_1, APP_UID_1)); + assertFalse(mSensitiveContentPackages.isScreenRecordingBlockedForApp(APP_PKG_1, APP_UID_2)); + assertFalse(mSensitiveContentPackages.isScreenRecordingAllowedForApp(APP_PKG_1, APP_UID_1)); + assertTrue(mSensitiveContentPackages.isScreenRecordingAllowedForApp(APP_PKG_2, APP_UID_2)); + // independent of sensitive content protection + assertTrue(mSensitiveContentPackages.size() == 0); + + assertTrue(mSensitiveContentPackages.setScreenRecordingOverrides( + new ArraySet<>(), new ArraySet<>())); + assertFalse(mSensitiveContentPackages.isScreenRecordingBlockedForApp(APP_PKG_1, APP_UID_1)); + assertFalse(mSensitiveContentPackages.isScreenRecordingAllowedForApp(APP_PKG_2, APP_UID_2)); + } }