diff --git a/CHANGELOG.md b/CHANGELOG.md index 582a150..c7eed32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 1.5.2 + +- iOS: add Swift Package Manager support +- Android: AGP 9 compatibility — apply `kotlin-android` only on AGP < 9 (AGP 9+ has built-in Kotlin), switch to new DSL (`minSdk`/`targetSdk`), drop unused `kotlinOptions` block + ## 1.5.1 - Refactor: replace hack-based screenshot prevention with overlay implementation diff --git a/android/build.gradle b/android/build.gradle index a94baa1..69bc2e6 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,10 +1,11 @@ -buildscript { - ext.kotlin_version = '2.2.20' -} - plugins { id 'com.android.library' - id 'org.jetbrains.kotlin.android' +} + +// AGP 9+ has built-in Kotlin support; older versions need the plugin explicitly +def agpMajor = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.split('\\.')[0].toInteger() +if (agpMajor < 9) { + apply plugin: 'kotlin-android' } rootProject.allprojects { @@ -17,9 +18,6 @@ rootProject.allprojects { group 'com.prongbang.screen_protector' -apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' - android { namespace 'com.prongbang.screen_protector' compileSdk 36 @@ -29,17 +27,13 @@ android { targetCompatibility JavaVersion.VERSION_17 } - kotlinOptions { - jvmTarget = '17' - } - sourceSets { main.java.srcDirs += 'src/main/kotlin' } defaultConfig { - minSdkVersion 21 - targetSdkVersion 36 + minSdk 21 + targetSdk 36 } lintOptions { diff --git a/ios/screen_protector/Package.swift b/ios/screen_protector/Package.swift new file mode 100644 index 0000000..f6f5623 --- /dev/null +++ b/ios/screen_protector/Package.swift @@ -0,0 +1,23 @@ +// swift-tools-version: 5.9 +import PackageDescription + +let package = Package( + name: "screen_protector", + platforms: [ + .iOS(.v15) + ], + products: [ + .library(name: "screen-protector", targets: ["screen_protector"]) + ], + dependencies: [ + .package(url: "https://github.com/prongbang/ScreenProtectorKit.git", exact: "1.5.1"), + ], + targets: [ + .target( + name: "screen_protector", + dependencies: [ + .product(name: "ScreenProtectorKit", package: "ScreenProtectorKit") + ] + ) + ] +) diff --git a/ios/screen_protector/Sources/screen_protector/FlutterRootViewResolver.swift b/ios/screen_protector/Sources/screen_protector/FlutterRootViewResolver.swift new file mode 100644 index 0000000..a3233fb --- /dev/null +++ b/ios/screen_protector/Sources/screen_protector/FlutterRootViewResolver.swift @@ -0,0 +1,39 @@ +// +// FlutterRootViewResolver.swift +// +// +// Created by INTENIQUETIC on 18/1/2569 BE. +// + +import Flutter +import UIKit +import ScreenProtectorKit + +final class FlutterRootViewResolver: ScreenProtectorRootViewResolving { + func resolveRootView() -> UIView? { + guard Thread.isMainThread else { + log("resolveFlutterRootView: called off main thread") + return nil + } + + guard let windowScene = UIApplication.shared.connectedScenes + .compactMap({ $0 as? UIWindowScene }) + .first(where: { $0.activationState == .foregroundActive }) else { + log("resolveFlutterRootView: no foreground active UIWindowScene") + return nil + } + + guard let flutterVC = windowScene.windows + .first(where: { $0.isKeyWindow })? + .rootViewController as? FlutterViewController else { + log("resolveFlutterRootView: FlutterViewController not found on key window") + return nil + } + + return flutterVC.view + } + + private func log(_ message: String) { + //print("[FlutterRootViewResolver]: \(message)") + } +} diff --git a/ios/screen_protector/Sources/screen_protector/ScreenProtectorPlugin.swift b/ios/screen_protector/Sources/screen_protector/ScreenProtectorPlugin.swift new file mode 100644 index 0000000..27e94b3 --- /dev/null +++ b/ios/screen_protector/Sources/screen_protector/ScreenProtectorPlugin.swift @@ -0,0 +1,216 @@ +import Flutter +import UIKit +import ScreenProtectorKit + +enum ScrennProtectorMethod: String { + case protectDataLeakageWithBlur + case protectDataLeakageWithBlurOff + case protectDataLeakageWithImage + case protectDataLeakageWithImageOff + case protectDataLeakageWithColor + case protectDataLeakageWithColorOff + case protectDataLeakageOff + case preventScreenshotOn + case preventScreenshotOff + case preventScreenRecordOn + case preventScreenRecordOff + case addListener + case removeListener + case isRecording +} + +public class ScreenProtectorPlugin: NSObject, FlutterPlugin { + private static var channel: FlutterMethodChannel? = nil + private let protectKit: ScreenProtectorKit + private var protectMode: ScreenProtectorMode = .none + private var isPreventScreenshotEnabled = false + + init(_ screenProtector: ScreenProtectorKit) { + self.protectKit = screenProtector + } + + public static func register(with registrar: FlutterPluginRegistrar) { + ScreenProtectorPlugin.channel = FlutterMethodChannel(name: "screen_protector", binaryMessenger: registrar.messenger()) + + let kit = ScreenProtectorKit(window: ScreenProtectorPlugin.keyWindow()) + kit.setRootViewResolver(FlutterRootViewResolver()) + ScreenProtectorKit.initial(with: kit.window?.rootViewController?.view) + let instance = ScreenProtectorPlugin(kit) + + registrar.addMethodCallDelegate(instance, channel: ScreenProtectorPlugin.channel!) + registrar.addApplicationDelegate(instance) + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + if Thread.isMainThread { + handleFunc(call, result: result) + return + } + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + self.handleFunc(call, result: result) + } + } + + public func applicationWillResignActive(_ application: UIApplication) { + updateWindowIfNeeded() + applyDataLeakageProtection() + } + + public func applicationDidBecomeActive(_ application: UIApplication) { + updateWindowIfNeeded() + clearDataLeakageProtection() + } + + deinit { + updateWindowIfNeeded() + protectKit.removeAllObserver() + protectKit.disablePreventScreenshot() + protectKit.disablePreventScreenRecording() + clearDataLeakageProtection() + } +} + +private extension ScreenProtectorPlugin { + func handleFunc(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let args = call.arguments as? Dictionary + + switch ScrennProtectorMethod(rawValue: call.method) { + case .protectDataLeakageWithBlur: + setDataLeakageProtectMode(.blur) + result(true) + break + case .protectDataLeakageWithBlurOff: + if case .blur = protectMode { + protectMode = .none + } + protectKit.disableBlurScreen() + result(true) + break + case .protectDataLeakageWithImage: + setDataLeakageProtectMode(.image(name: args?["name"] ?? "LaunchImage")) + result(true) + break + case .protectDataLeakageWithImageOff: + if case .image = protectMode { + protectMode = .none + } + protectKit.disableImageScreen() + result(true) + break + case .protectDataLeakageWithColor: + guard let hexColor = args?["hexColor"] else { + result(false) + return + } + setDataLeakageProtectMode(.color(hex: hexColor)) + result(true) + break + case .protectDataLeakageWithColorOff: + if case .color = protectMode { + protectMode = .none + } + protectKit.disableColorScreen() + result(true) + break + case .protectDataLeakageOff: + protectMode = .none + clearDataLeakageProtection() + result(true) + break + case .preventScreenshotOn: + isPreventScreenshotEnabled = true + updateWindowIfNeeded() + protectKit.enabledPreventScreenshot() + result(true) + break + case .preventScreenshotOff: + isPreventScreenshotEnabled = false + updateWindowIfNeeded() + protectKit.disablePreventScreenshot() + result(true) + break + case .preventScreenRecordOn: + updateWindowIfNeeded() + protectKit.enabledPreventScreenRecording() + result(true) + break + case .preventScreenRecordOff: + updateWindowIfNeeded() + protectKit.disablePreventScreenRecording() + result(true) + break + case .addListener: + protectKit.screenshotObserver { [weak channel = ScreenProtectorPlugin.channel] in + channel?.invokeMethod("onScreenshot", arguments: nil) + } + if #available(iOS 11.0, *) { + protectKit.screenRecordObserver { [weak channel = ScreenProtectorPlugin.channel] isCaptured in + channel?.invokeMethod("onScreenRecord", arguments: isCaptured) + } + } + result("listened") + break + case .removeListener: + protectKit.removeAllObserver() + result("removed") + break + case .isRecording: + if #available(iOS 11.0, *) { + result(protectKit.screenIsRecording()) + } else { + result(false) + } + break + default: + result(false) + break + } + } + + func updateWindowIfNeeded() { + if let window = Self.keyWindow() { + protectKit.window = window + } + } + + func applyDataLeakageProtection() { + updateWindowIfNeeded() + clearDataLeakageProtection() + switch protectMode { + case .blur: + protectKit.enabledBlurScreen() + case let .image(name): + protectKit.enabledImageScreen(named: name) + case let .color(hex): + protectKit.enabledColorScreen(hexColor: hex) + case .none: + break + } + } + + func clearDataLeakageProtection() { + protectKit.disableBlurScreen() + protectKit.disableImageScreen() + protectKit.disableColorScreen() + } + + func setDataLeakageProtectMode(_ mode: ScreenProtectorMode) { + protectMode = mode + if UIApplication.shared.applicationState != .active { + applyDataLeakageProtection() + } else { + clearDataLeakageProtection() + } + } + + static func keyWindow() -> UIWindow? { + if #available(iOS 13.0, *) { + return UIApplication.shared.connectedScenes + .compactMap { $0 as? UIWindowScene } + .flatMap { $0.windows } + .first { $0.isKeyWindow } + } + return UIApplication.shared.keyWindow + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 2653fe0..d81fd1a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: screen_protector description: Safe Data Leakage via Application Background Screenshot and Prevent Screenshot for Android and iOS. -version: 1.5.1 +version: 1.5.2 homepage: https://github.com/prongbang/screen_protector environment: