Files
sociology_app/uni_modules/uni-usercapturescreen/utssdk/app-ios/index.uts

325 lines
11 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.isCapturediOS 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)
}
}