import { NotificationCenter } from 'Foundation'; import { CGRect } from "CoreFoundation"; import { UIApplication, UIView, UITextField, UIScreen, UIDevice } from "UIKit" import { UTSiOS } from "DCloudUTSFoundation" import { DispatchQueue } from 'Dispatch'; import { SetUserCaptureScreenOptions, OnUserCaptureScreenCallbackResult, OnUserCaptureScreen, OffUserCaptureScreen, SetUserCaptureScreen, UserCaptureScreenCallback, SetUserCaptureScreenSuccess } from "../interface.uts" import { SetUserCaptureScreenFailImpl } from "../unierror.uts" /** * 定义监听截屏事件工具类 */ class CaptureScreenTool { static listener : UserCaptureScreenCallback | null; static secureView : UIView | null; // ===== 仅防录屏(动态切换)相关状态 ===== // 保留 UITextField,使 isSecureTextEntry 可在录屏开始/结束时反复切换 static secureField : UITextField | null; // field 的安全画布(已挂入视图层级,承载 App 根视图) static secureCanvas : UIView | null; // 被安全画布包裹的 App 根视图(用于卸载时还原) static secureRootView : UIView | null; // 是否已为"仅防录屏"挂载安全层 static recordWrapped : boolean = false; // 监听截屏 static listenCaptureScreen(callback : UserCaptureScreenCallback | null) { this.listener = callback // 注册监听截屏事件及回调方法 // target-action 回调方法需要通过 Selector("方法名") 构建 const method = Selector("userDidTakeScreenshot") NotificationCenter.default.addObserver(this, selector = method, name = UIApplication.userDidTakeScreenshotNotification, object = null) } // 捕获截屏回调的方法 // target-action 的方法前需要添加 @objc 前缀 @objc static userDidTakeScreenshot() { // 回调 const res: OnUserCaptureScreenCallbackResult = { } this.listener?.(res) } // 移除监听事件 static removeListen(callback : UserCaptureScreenCallback | null) { this.listener = null NotificationCenter.default.removeObserver(this) } static createSecureView() : UIView | null { let field = new UITextField(frame = CGRect.zero) field.isSecureTextEntry = true if (field.subviews.length > 0 && UIDevice.current.systemVersion != '15.1') { let view = field.subviews[0] view.subviews.forEach((item) => { item.removeFromSuperview() }) view.isUserInteractionEnabled = true return view } return null } // 应用防截屏遮罩(secureView:实时显示正常,但被截屏/录屏捕获到的内容为黑屏) // 不触发回调,供内部复用;注意:需在主线程调用 static applySecureViewInternal() { if (this.secureView != null) { // 已应用,避免重复包裹 return } let secureView = this.createSecureView() let window = UTSiOS.getKeyWindow() let rootView = window.rootViewController == null ? null : window.rootViewController!.view if (secureView != null && rootView != null) { let rootSuperview = rootView!.superview if (rootSuperview != null) { this.secureView = secureView rootSuperview!.addSubview(secureView!) rootView!.removeFromSuperview() secureView!.addSubview(rootView!) // secureView 充满父视图并随父视图宽高自适应(横竖屏、全屏切换时自动跟随) secureView!.frame = rootSuperview!.bounds secureView!.autoresizingMask = [UIView.AutoresizingMask.flexibleWidth, UIView.AutoresizingMask.flexibleHeight] // rootView 充满 secureView 并自适应,避免全屏时被固定在竖屏尺寸而缩到角落 rootView!.frame = secureView!.bounds rootView!.autoresizingMask = [UIView.AutoresizingMask.flexibleWidth, UIView.AutoresizingMask.flexibleHeight] } } } // 开启防截屏 static onAntiScreenshot(option : SetUserCaptureScreenOptions) { // uts方法默认会在子线程中执行,涉及 UI 操作必须在主线程中运行,通过 DispatchQueue.main.async 方法可将代码在主线程中运行 DispatchQueue.main.async(execute = () : void => { this.applySecureViewInternal() let res: SetUserCaptureScreenSuccess = { } option.success?.(res) option.complete?.(res) }) } // ===== 仅防录屏:提前挂载安全层 + 翻转 isSecureTextEntry ===== // 提前挂载安全层(应在录屏开始前调用,例如进入页面时)。 // 挂载后默认不保护(isSecureTextEntry = false)→ 截屏正常、实时正常。 // 注意:需在主线程调用。 static setupRecordSecureWrapper() { if (this.recordWrapped) { return } let field = new UITextField(frame = CGRect.zero) field.isSecureTextEntry = true // 15.1 系统 secureView 不可用;拿不到安全画布则放弃 if (field.subviews.length == 0 || UIDevice.current.systemVersion == '15.1') { return } let canvas = field.subviews[0] canvas.subviews.forEach((item) => { item.removeFromSuperview() }) canvas.isUserInteractionEnabled = true let window = UTSiOS.getKeyWindow() let rootView = window.rootViewController == null ? null : window.rootViewController!.view if (rootView == null) { return } let rootSuperview = rootView!.superview if (rootSuperview == null) { return } // 把安全画布插入原层级,并把 App 根视图移入画布 rootSuperview!.addSubview(canvas) rootView!.removeFromSuperview() canvas.addSubview(rootView!) canvas.frame = rootSuperview!.bounds canvas.autoresizingMask = [UIView.AutoresizingMask.flexibleWidth, UIView.AutoresizingMask.flexibleHeight] rootView!.frame = canvas.bounds rootView!.autoresizingMask = [UIView.AutoresizingMask.flexibleWidth, UIView.AutoresizingMask.flexibleHeight] // 关键:保留 field,后续才能切换 isSecureTextEntry this.secureField = field this.secureCanvas = canvas this.secureRootView = rootView this.recordWrapped = true // 默认不保护:允许截屏、实时正常 field.isSecureTextEntry = false } // 切换录屏保护:true=保护(录屏文件黑、实时正常、此刻截屏也黑);false=不保护(允许截屏) // 注意:需在主线程调用。 static setRecordSecure(secure : boolean) { if (this.secureField != null) { this.secureField!.isSecureTextEntry = secure } } // 卸载安全层,恢复原始视图层级。注意:需在主线程调用。 static teardownRecordSecureWrapper() { if (!this.recordWrapped) { return } if (this.secureField != null) { this.secureField!.isSecureTextEntry = false } if (this.secureCanvas != null && this.secureRootView != null) { let rootSuperview = this.secureCanvas!.superview if (rootSuperview != null) { rootSuperview!.addSubview(this.secureRootView!) this.secureRootView!.frame = rootSuperview!.bounds this.secureCanvas!.removeFromSuperview() } } this.secureField = null this.secureCanvas = null this.secureRootView = null this.recordWrapped = false } // 移除防截屏遮罩视图(不触发回调,供内部复用) // 注意:需在主线程调用 static removeSecureViewInternal() { if (this.secureView != null) { let window = UTSiOS.getKeyWindow() let rootView = window.rootViewController == null ? null : window.rootViewController!.view if (rootView != null && this.secureView!.superview != null) { let rootSuperview = this.secureView!.superview if (rootSuperview != null) { rootSuperview!.addSubview(rootView!) this.secureView!.removeFromSuperview() } } this.secureView = null } } // 关闭防截屏 static offAntiScreenshot(option : SetUserCaptureScreenOptions) { DispatchQueue.main.async(execute = () : void => { this.removeSecureViewInternal() let res: SetUserCaptureScreenSuccess = { } option.success?.(res) option.complete?.(res) }) } } /** * 防录屏工具类:基于 UIScreen.isCaptured 监听录屏/投屏状态,动态切换安全层。 * 截屏不会触发 isCaptured,因此可做到"允许截屏、仅在录屏时保护"。 * * 实现要点(避免实时画面变黑): * 1) 进入时(录屏开始前)就提前挂好安全层,且默认不保护 → 截屏正常、实时正常; * 2) 仅在录屏期间翻转 isSecureTextEntry = true → 录屏文件黑、实时正常、此刻截屏也黑; * 3) 录屏结束翻回 false → 截屏恢复正常。 */ class ScreenRecordTool { // 是否已注册监听 static monitoring : boolean = false; // 开启防录屏:提前挂载安全层并监听录屏状态变化 static onAntiScreenRecord() { DispatchQueue.main.async(execute = () : void => { // 关键:录屏开始前就把安全层挂好(默认不保护) CaptureScreenTool.setupRecordSecureWrapper() if (!this.monitoring) { this.monitoring = true const method = Selector("screenCaptureDidChange") NotificationCenter.default.addObserver(this, selector = method, name = UIScreen.capturedDidChangeNotification, object = null) } // 进入时若已经在录屏,立即按当前状态保护 this.applyIfNeeded() }) } // 录屏状态变化回调 @objc static screenCaptureDidChange() { DispatchQueue.main.async(execute = () : void => { this.applyIfNeeded() }) } // 根据当前录屏状态翻转安全层开关 static applyIfNeeded() { CaptureScreenTool.setRecordSecure(UIScreen.main.isCaptured) } // 关闭防录屏:移除监听并卸载安全层 static offAntiScreenRecord() { DispatchQueue.main.async(execute = () : void => { if (this.monitoring) { NotificationCenter.default.removeObserver(this, name = UIScreen.capturedDidChangeNotification, object = null) this.monitoring = false } CaptureScreenTool.teardownRecordSecureWrapper() }) } } /** * 开启截图监听 */ export const onUserCaptureScreen : OnUserCaptureScreen = function (callback : UserCaptureScreenCallback | null) { CaptureScreenTool.listenCaptureScreen(callback) } /** * 关闭截屏监听 */ export const offUserCaptureScreen : OffUserCaptureScreen = function (callback : UserCaptureScreenCallback | null) { CaptureScreenTool.removeListen(callback) } /** * 开启/关闭防截屏 */ export const setUserCaptureScreen : SetUserCaptureScreen = function (options : SetUserCaptureScreenOptions) { const systemVersion = UIDevice.current.systemVersion if (options.enable == true) { // 允许截屏与录屏:无条件清理录屏遮罩,再移除防截屏遮罩。 // 清理动作不受版本限制,避免遗留监听/遮罩。 ScreenRecordTool.offAntiScreenRecord() CaptureScreenTool.offAntiScreenshot(options) return } if (options.antiRecordOnly == true) { // 仅禁止录屏、允许截屏:进入页面时提前挂好安全层(默认不保护), // 用 UIScreen.isCaptured(iOS 11+)监听录屏/投屏,仅在录屏期间翻转 isSecureTextEntry // —— 实时显示正常、录到的画面为黑屏,且平时不影响截屏。 // 注意:secureView 在 iOS 15.1 上不可用,该版本录屏保护会失效(属系统已知问题)。 if (systemVersion < "11.0") { let res = new SetUserCaptureScreenFailImpl(12001) options.fail?.(res) options.complete?.(res) return } ScreenRecordTool.onAntiScreenRecord() let res: SetUserCaptureScreenSuccess = { } options.success?.(res) options.complete?.(res) return } // 默认:截屏与录屏一起禁止(secureView 方案,受系统版本限制) if (systemVersion < "13.0") { let res = new SetUserCaptureScreenFailImpl(12001) options.fail?.(res) options.complete?.(res) } else if (systemVersion == "15.1") { let res = new SetUserCaptureScreenFailImpl(12010) options.fail?.(res) options.complete?.(res) } else { CaptureScreenTool.onAntiScreenshot(options) } }