diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index fa7508f5d8cc1..a9910d228111e 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -13768,6 +13768,19 @@ public static boolean putFloatForUser(ContentResolver cr, String name, float val */ public static final String DISABLE_SECURE_WINDOWS = "disable_secure_windows"; + /** + + Whether to allow user-initiated screenshots of secure windows. + + When this setting is set to a non-zero value, user-initiated screenshots + will capture content in windows with + {@link android.view.WindowManager.LayoutParams#FLAG_SECURE}. + Other FLAG_SECURE behaviors (recents thumbnails, casting, screen recording) + are preserved. + @hide */ + public static final String FORCE_SCREENSHOT_SECURE_WINDOWS = "force_screenshot_secure_windows"; + + /** * Controls if the adaptive authentication feature should be disabled, which * will attempt to lock the device after a number of consecutive authentication diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 686a2208a14ea..6faaf9b71461f 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -113,6 +113,7 @@ 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_THROW_EXCEPTION; +import static android.window.ScreenCapture.ScreenCaptureParams.SECURE_CONTENT_POLICY_CAPTURE; import static android.window.WindowProviderService.isWindowProviderService; import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_ADD_REMOVE; @@ -835,6 +836,8 @@ final class SettingsObserver extends ContentObserver { Settings.Secure.getUriFor(Settings.Secure.IMMERSIVE_MODE_CONFIRMATIONS); private final Uri mDisableSecureWindowsUri = Settings.Secure.getUriFor(Settings.Secure.DISABLE_SECURE_WINDOWS); + private final Uri mForceScreenshotSecureWindowsUri = + Settings.Secure.getUriFor(Settings.Secure.FORCE_SCREENSHOT_SECURE_WINDOWS); private final Uri mMagnifyImeEnabledUri = Settings.Secure.getUriFor( Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MAGNIFY_NAV_AND_IME); private final Uri mPolicyControlUri = @@ -869,6 +872,9 @@ public SettingsObserver() { UserHandle.USER_ALL); resolver.registerContentObserver(mDisableSecureWindowsUri, false, this, UserHandle.USER_ALL); + resolver.registerContentObserver(mForceScreenshotSecureWindowsUri, false, this, + UserHandle.USER_ALL); + if (com.android.server.accessibility.Flags.enableMagnificationMagnifyNavBarAndIme()) { resolver.registerContentObserver(mMagnifyImeEnabledUri, false, this, UserHandle.USER_ALL); @@ -934,6 +940,11 @@ public void onChange(boolean selfChange, Uri uri) { return; } + if (mForceScreenshotSecureWindowsUri.equals(uri)) { + updateForceScreenshotSecureWindows(); + return; + } + if (mMagnifyImeEnabledUri.equals(uri)) { updateMagnifyIme(); } @@ -962,6 +973,7 @@ public void onChange(boolean selfChange, Uri uri) { void loadSettings() { updateMaximumObscuringOpacityForTouch(); updateDisableSecureWindows(); + updateForceScreenshotSecureWindows(); updateMagnifyIme(); } @@ -1072,6 +1084,19 @@ void updateDisableSecureWindows() { } } + void updateForceScreenshotSecureWindows() { + + boolean forceScreenshotSecureWindows; + try { + forceScreenshotSecureWindows = Settings.Secure.getIntForUser( + mContext.getContentResolver(), + Settings.Secure.FORCE_SCREENSHOT_SECURE_WINDOWS, 0) != 0; + } catch (Settings.SettingNotFoundException e) { + forceScreenshotSecureWindows = false; + } + mForceScreenshotSecureWindows = forceScreenshotSecureWindows; + } + void updateMagnifyIme() { if (!com.android.server.accessibility.Flags.enableMagnificationMagnifyNavBarAndIme()) { mMagnifyIme = false; @@ -1247,6 +1272,7 @@ public void onAppTransitionFinishedLocked(IBinder token) { private final ScreenRecordingCallbackController mScreenRecordingCallbackController; private volatile boolean mDisableSecureWindows = false; + private volatile boolean mForceScreenshotSecureWindows = false; /** Creates an instance of the WindowManagerService for the system server. */ public static WindowManagerService main(@NonNull final Context context, @@ -10669,10 +10695,17 @@ ScreenCaptureInternal.LayerCaptureArgs getCaptureArgs( } } - return new ScreenCaptureInternal.LayerCaptureArgs.Builder( + ScreenCaptureInternal.LayerCaptureArgs.Builder builder = + new ScreenCaptureInternal.LayerCaptureArgs.Builder( displaySurfaceControl, captureArgs) - .setSourceCrop(mTmpRect) - .build(); + .setSourceCrop(mTmpRect); + + if (mForceScreenshotSecureWindows) { + builder = builder.setSecureContentPolicy( + SECURE_CONTENT_POLICY_CAPTURE); + } + + return builder.build(); } @Override @@ -10870,6 +10903,10 @@ boolean getDisableSecureWindows() { return mDisableSecureWindows; } + boolean getForceScreenshotSecureWindows() { + return mForceScreenshotSecureWindows; + } + /** * Called to notify WMS that the specified window has become visible. This shows a Toast if the * window is deemed to hold sensitive content. diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java index bea5b4e9e5b0e..321ab5216cf71 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java @@ -84,6 +84,7 @@ import android.app.IApplicationThread; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; +import android.content.ContentResolver; import android.graphics.Rect; import android.os.Binder; import android.os.IBinder; @@ -121,6 +122,7 @@ import android.window.ClientWindowFrames; import android.window.ConfigurationChangeSetting; import android.window.InputTransferToken; +import android.window.ScreenCapture; import android.window.ScreenCaptureInternal; import android.window.WindowContainerToken; @@ -1364,6 +1366,30 @@ public void testCaptureDisplay() { assertEquals(validRect, resultingArgs.mSourceCrop); } + @Test + public void testCaptureArgs_forceScreenshotSecureWindows() { + Rect displayBounds = new Rect(0, 0, 100, 200); + spyOn(mDisplayContent); + when(mDisplayContent.getBounds()).thenReturn(displayBounds); + ContentResolver cr = useFakeSettingsProvider(); + + // secureContentPolicy should be REDACT (default) when setting is disabled + Settings.Secure.putInt(cr, Settings.Secure.FORCE_SCREENSHOT_SECURE_WINDOWS, 0); + mWm.mSettingsObserver.onChange(false, + Settings.Secure.getUriFor(Settings.Secure.FORCE_SCREENSHOT_SECURE_WINDOWS)); + ScreenCaptureInternal.LayerCaptureArgs resultingArgs = + mWm.getCaptureArgs(DEFAULT_DISPLAY, null); + assertEquals(ScreenCapture.ScreenCaptureParams.SECURE_CONTENT_POLICY_REDACT, + resultingArgs.mSecureContentPolicy); + + // secureContentPolicy should be CAPTURE when setting is enabled + Settings.Secure.putInt(cr, Settings.Secure.FORCE_SCREENSHOT_SECURE_WINDOWS, 1); + mWm.mSettingsObserver.onChange(false, + Settings.Secure.getUriFor(Settings.Secure.FORCE_SCREENSHOT_SECURE_WINDOWS)); + resultingArgs = mWm.getCaptureArgs(DEFAULT_DISPLAY, null); + assertEquals(ScreenCapture.ScreenCaptureParams.SECURE_CONTENT_POLICY_CAPTURE, + resultingArgs.mSecureContentPolicy); + } @Test public void testGrantInputChannel_sanitizeSpyWindowForApplications() { final Session session = mock(Session.class);