diff --git a/Ports/iOSPort/nativeSources/IOSNative.m b/Ports/iOSPort/nativeSources/IOSNative.m index 8c1335f672..b60af7739b 100644 --- a/Ports/iOSPort/nativeSources/IOSNative.m +++ b/Ports/iOSPort/nativeSources/IOSNative.m @@ -116,6 +116,76 @@ #endif #endif +// iOS doesn't allow blocking a static screenshot, but it does expose a flag +// that tells us when the screen is being captured (recorded or mirrored). We +// use that signal to temporarily cover the app view with a black overlay. +static BOOL cn1_disableScreenshots = NO; +static UIView *cn1ScreenCaptureView = nil; +static id cn1ScreenCaptureObserver = nil; + +static UIView *cn1_screenCaptureContainer() { + // Prefer the GL view controller's view. Fall back to the key window so + // we can still cover the app if the controller isn't ready yet. + UIView *container = [[CodenameOne_GLViewController instance] view]; + if (container == nil) { + container = [UIApplication sharedApplication].keyWindow; + if (container == nil && [[UIApplication sharedApplication].windows count] > 0) { + container = [[UIApplication sharedApplication].windows objectAtIndex:0]; + } + } + return container; +} + +static void cn1_updateScreenCaptureBlocker() { +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 + // Compile-time guard: screen-capture detection APIs were introduced in iOS 11. + // This keeps older SDKs building cleanly without referencing unavailable symbols. + if (!cn1_disableScreenshots) { + if (cn1ScreenCaptureView != nil) { + [cn1ScreenCaptureView removeFromSuperview]; + cn1ScreenCaptureView = nil; + } + return; + } + if (![[UIScreen mainScreen] respondsToSelector:@selector(isCaptured)]) { + // Runtime guard: if running on an older iOS version, just skip. + return; + } + BOOL captured = NO; + if (@available(iOS 11.0, *)) { + captured = [UIScreen mainScreen].isCaptured; + } + if (captured) { + // If screen capture is active, hide the UI behind an overlay view. + UIView *container = cn1_screenCaptureContainer(); + if (container == nil) { + return; + } + if (cn1ScreenCaptureView == nil) { + cn1ScreenCaptureView = [[UIView alloc] initWithFrame:container.bounds]; + cn1ScreenCaptureView.backgroundColor = [UIColor blackColor]; + cn1ScreenCaptureView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + cn1ScreenCaptureView.userInteractionEnabled = NO; + } + if (cn1ScreenCaptureView.superview != container) { + [cn1ScreenCaptureView removeFromSuperview]; + [container addSubview:cn1ScreenCaptureView]; + } else { + cn1ScreenCaptureView.frame = container.bounds; + } + } else if (cn1ScreenCaptureView != nil) { + [cn1ScreenCaptureView removeFromSuperview]; + cn1ScreenCaptureView = nil; + } +#else + // Older SDKs don't have the screen-capture APIs. Nothing to do. + if (cn1ScreenCaptureView != nil) { + [cn1ScreenCaptureView removeFromSuperview]; + cn1ScreenCaptureView = nil; + } +#endif +} + /*static JAVA_OBJECT utf8_constant = JAVA_NULL; JAVA_OBJECT fromNSString(NSString* str) { @@ -2025,6 +2095,37 @@ void com_codename1_impl_ios_IOSNative_unlockScreen__(CN1_THREAD_STATE_MULTI_ARG }); } +void com_codename1_impl_ios_IOSNative_setDisableScreenshots___boolean(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_BOOLEAN disable) +{ + BOOL shouldDisable = disable ? YES : NO; + dispatch_async(dispatch_get_main_queue(), ^{ + cn1_disableScreenshots = shouldDisable; + if (cn1ScreenCaptureObserver != nil) { + [[NSNotificationCenter defaultCenter] removeObserver:cn1ScreenCaptureObserver]; + cn1ScreenCaptureObserver = nil; + } + if (cn1ScreenCaptureView != nil) { + [cn1ScreenCaptureView removeFromSuperview]; + cn1ScreenCaptureView = nil; + } +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 + if (cn1_disableScreenshots && [[UIScreen mainScreen] respondsToSelector:@selector(isCaptured)]) { + if (@available(iOS 11.0, *)) { + // Listen for capture state changes so we can add/remove the overlay. + cn1ScreenCaptureObserver = [[NSNotificationCenter defaultCenter] addObserverForName:UIScreenCapturedDidChangeNotification + object:[UIScreen mainScreen] + queue:[NSOperationQueue mainQueue] + usingBlock:^(NSNotification *notification) { + cn1_updateScreenCaptureBlocker(); + }]; + } + } +#endif + // Ensure the overlay reflects the current capture state immediately. + cn1_updateScreenCaptureBlocker(); + }); +} + extern void vibrateDevice(); void com_codename1_impl_ios_IOSNative_vibrate___int(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_INT duration) { vibrateDevice(); diff --git a/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java b/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java index d2ecd288fe..e52eca80b1 100644 --- a/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java +++ b/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java @@ -218,6 +218,9 @@ public void init(Object m) { life = (Lifecycle)m; } VideoCaptureConstraints.init(new IOSVideoCaptureConstraintsCompiler()); + if("true".equals(Display.getInstance().getProperty("DisableScreenshots", ""))) { + nativeInstance.setDisableScreenshots(true); + } } public void setThreadPriority(Thread t, int p) { diff --git a/Ports/iOSPort/src/com/codename1/impl/ios/IOSNative.java b/Ports/iOSPort/src/com/codename1/impl/ios/IOSNative.java index 3fdef9db6a..c7ef943077 100644 --- a/Ports/iOSPort/src/com/codename1/impl/ios/IOSNative.java +++ b/Ports/iOSPort/src/com/codename1/impl/ios/IOSNative.java @@ -157,6 +157,7 @@ byte[] loadResource(String name, String type) { native void unlockOrientation(); native void lockScreen(); native void unlockScreen(); + native void setDisableScreenshots(boolean disable); native void vibrate(int duration); diff --git a/docs/developer-guide/security.asciidoc b/docs/developer-guide/security.asciidoc index 98bf1cb7d7..4a16f01bc1 100644 --- a/docs/developer-guide/security.asciidoc +++ b/docs/developer-guide/security.asciidoc @@ -133,7 +133,9 @@ This is useful as it prevents the encrypted preferences from colliding with the One of the common security features some apps expect is the ability to block a screenshot. In the past apps like snapchat required that you touch the screen to view a photo to block the ability to grab a screenshot (on iOS). This no longer works... -Blocking screenshots is an Android specific feature that can't be implemented on iOS. This is implemented by classifying the app window as secure and you can do that via the build hint `android.disableScreenshots=true`. Once that is added screenshots should no longer work for the app, this might impact other things as well such as the task view which will no longer show the screenshot either. +Blocking screenshots is implemented by classifying the app window as secure on Android. You can enable this via the build hint `android.disableScreenshots=true`. Once that is added screenshots should no longer work for the app, this might impact other things as well such as the task view which will no longer show the screenshot either. + +On iOS, you cannot prevent a user from taking a static screenshot, but you can block screen capture/recording while it is active. This uses `UIScreen.isCaptured` and its change notification to hide the app contents while a capture session is active. Enable this behavior with the build hint `ios.disableScreenshots=true`. === Blocking Copy & Paste diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java index 31aac1eb3e..a583b5afdc 100644 --- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java @@ -932,6 +932,10 @@ public void usesClassMethod(String cls, String method) { if(request.getArg("ios.newStorageLocation", "true").equals("true")) { newStorage = " Display.getInstance().setProperty(\"iosNewStorage\", \"true\");\n"; } + String disableScreenshots = ""; + if (request.getArg("ios.disableScreenshots", "false").equalsIgnoreCase("true")) { + disableScreenshots = " Display.getInstance().setProperty(\"DisableScreenshots\", \"true\");\n"; + } String didEnterBackground = " stopped = true;\n" + " final long bgTask = com.codename1.impl.ios.IOSImplementation.beginBackgroundTask();\n" @@ -967,6 +971,7 @@ public void usesClassMethod(String cls, String method) { + " Display.getInstance().setProperty(\"AppVersion\", APPLICATION_VERSION);\n" + " Display.getInstance().setProperty(\"AppName\", APPLICATION_NAME);\n" + newStorage + + disableScreenshots + adPadding + integrateFacebook + integrateGoogleConnect