diff --git a/CHANGELOG.md b/CHANGELOG.md index 591341c..878af04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [7.2.2] - 2025-10-09 +- iOS SDK version: 6.12.1 +- Android SDK version: 16.0.1 + +### Android + +#### Fixed +- Fixed an issue with crashing screen protector + ## [7.2.1] - 2025-07-18 - iOS SDK version: 6.12.1 - Android SDK version: 16.0.1 diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/FreeraspPlugin.kt b/android/src/main/kotlin/com/aheaditec/freerasp/FreeraspPlugin.kt index ae3effa..8fdcfc0 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/FreeraspPlugin.kt +++ b/android/src/main/kotlin/com/aheaditec/freerasp/FreeraspPlugin.kt @@ -1,5 +1,6 @@ package com.aheaditec.freerasp +import android.app.Activity import android.content.Context import android.os.Build import androidx.lifecycle.Lifecycle @@ -18,15 +19,15 @@ class FreeraspPlugin : FlutterPlugin, ActivityAware, LifecycleEventObserver { private var streamHandler: StreamHandler = StreamHandler() private var methodCallHandler: MethodCallHandler = MethodCallHandler() private var screenProtector: ScreenProtector? = - if (Build.VERSION.SDK_INT >= 34) ScreenProtector() else null + if (Build.VERSION.SDK_INT >= 34) ScreenProtector else null private var context: Context? = null private var lifecycle: Lifecycle? = null + private var activity: Activity? = null override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { val messenger = flutterPluginBinding.binaryMessenger context = flutterPluginBinding.applicationContext - screenProtector?.enable() methodCallHandler.createMethodChannel(messenger, flutterPluginBinding.applicationContext) streamHandler.createEventChannel(messenger) } @@ -41,32 +42,31 @@ class FreeraspPlugin : FlutterPlugin, ActivityAware, LifecycleEventObserver { lifecycle = FlutterLifecycleAdapter.getActivityLifecycle(binding).also { it.addObserver(this) } + screenProtector?.register(binding.activity) methodCallHandler.activity = binding.activity - screenProtector?.activity = binding.activity - screenProtector?.let { lifecycle?.addObserver(it) } + activity = binding.activity } override fun onDetachedFromActivity() { lifecycle?.removeObserver(this) + screenProtector?.unregister(activity!!) methodCallHandler.activity = null - screenProtector?.let { lifecycle?.removeObserver(it) } - screenProtector?.activity = null + activity = null } override fun onDetachedFromActivityForConfigChanges() { lifecycle?.removeObserver(this) + screenProtector?.unregister(activity!!) methodCallHandler.activity = null - screenProtector?.let { lifecycle?.removeObserver(it) } - screenProtector?.activity = null + activity = null } override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { lifecycle = FlutterLifecycleAdapter.getActivityLifecycle(binding) - lifecycle?.addObserver(this) + activity = binding.activity methodCallHandler.activity = binding.activity - screenProtector?.activity = binding.activity - screenProtector?.let { lifecycle?.addObserver(it) } - + lifecycle?.addObserver(this) + screenProtector?.register(binding.activity) } override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/ScreenProtector.kt b/android/src/main/kotlin/com/aheaditec/freerasp/ScreenProtector.kt index bd0977d..83ea7d0 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/ScreenProtector.kt +++ b/android/src/main/kotlin/com/aheaditec/freerasp/ScreenProtector.kt @@ -15,42 +15,39 @@ import com.aheaditec.talsec_security.security.api.Talsec import io.flutter.Log import java.util.function.Consumer -internal class ScreenProtector : DefaultLifecycleObserver { - companion object { - private const val TAG = "ScreenProtector" - private const val SCREEN_CAPTURE_PERMISSION = "android.permission.DETECT_SCREEN_CAPTURE" - private const val SCREEN_RECORDING_PERMISSION = "android.permission.DETECT_SCREEN_RECORDING" - } +@SuppressLint("StaticFieldLeak") +internal object ScreenProtector { + private const val TAG = "TalsecScreenProtector" + private const val SCREEN_CAPTURE_PERMISSION = "android.permission.DETECT_SCREEN_CAPTURE" + private const val SCREEN_RECORDING_PERMISSION = "android.permission.DETECT_SCREEN_RECORDING" - internal var activity: Activity? = null private var isEnabled = false + private var isRegistered = false - private val screenCaptureCallback = ScreenCaptureCallback { Talsec.onScreenshotDetected() } + private val cachedThreats = mutableSetOf() + private val screenCaptureCallback = ScreenCaptureCallback { handleThreat(Threat.Screenshot) } private val screenRecordCallback: Consumer = Consumer { state -> - if (state == SCREEN_RECORDING_STATE_VISIBLE) { - Talsec.onScreenRecordingDetected() - Log.e("ScreenProtector", "Screen recording detected") - } + if (state == SCREEN_RECORDING_STATE_VISIBLE) handleThreat(Threat.ScreenRecording) } - internal fun enable() { + fun enable() { if (isEnabled) return + isEnabled = true + cachedThreats.forEach { handleThreat(it) } + cachedThreats.clear() } - override fun onStart(owner: LifecycleOwner) { - super.onStart(owner) - - if (isEnabled) activity?.let { register(it) } + fun disable() { + isEnabled = false } - override fun onStop(owner: LifecycleOwner) { - super.onStop(owner) - - if (isEnabled) activity?.let { unregister(it) } - } + internal fun register(activity: Activity) { + if (isRegistered) { + android.util.Log.w(TAG, "ScreenProtector is already registered.") + return + } - private fun register(activity: Activity) { if (Build.VERSION.SDK_INT >= 34) { registerScreenCapture(activity) } @@ -58,27 +55,8 @@ internal class ScreenProtector : DefaultLifecycleObserver { if (Build.VERSION.SDK_INT >= 35) { registerScreenRecording(activity) } - } - - // Missing permission is suppressed because the decision to use the screen capture API is made - // by developer, and not enforced by the library. - @SuppressLint("MissingPermission") - private fun unregister(currentActivity: Activity) { - val context = currentActivity.applicationContext - - if (Build.VERSION.SDK_INT >= 34 && hasPermission( - context, SCREEN_CAPTURE_PERMISSION - ) - ) { - currentActivity.unregisterScreenCaptureCallback(screenCaptureCallback) - } - if (Build.VERSION.SDK_INT >= 35 && hasPermission( - context, SCREEN_RECORDING_PERMISSION - ) - ) { - currentActivity.windowManager?.removeScreenRecordingCallback(screenRecordCallback) - } + isRegistered = true } // Missing permission is suppressed because the decision to use the screen capture API is made @@ -112,7 +90,34 @@ internal class ScreenProtector : DefaultLifecycleObserver { context.mainExecutor, screenRecordCallback ) screenRecordCallback.accept(initialState) + } + // Missing permission is suppressed because the decision to use the screen capture API is made + // by developer, and not enforced by the library. + @SuppressLint("MissingPermission") + internal fun unregister(currentActivity: Activity) { + if (!isRegistered) { + android.util.Log.w(TAG, "ScreenProtector is not registered.") + return + } + + val context = currentActivity.applicationContext + + if (Build.VERSION.SDK_INT >= 34 && hasPermission( + context, SCREEN_CAPTURE_PERMISSION + ) + ) { + currentActivity.unregisterScreenCaptureCallback(screenCaptureCallback) + } + + if (Build.VERSION.SDK_INT >= 35 && hasPermission( + context, SCREEN_RECORDING_PERMISSION + ) + ) { + currentActivity.windowManager?.removeScreenRecordingCallback(screenRecordCallback) + } + + isRegistered = false } private fun hasPermission(context: Context, permission: String): Boolean { @@ -122,9 +127,22 @@ internal class ScreenProtector : DefaultLifecycleObserver { } private fun reportMissingPermission(protectionType: String, permission: String) { - Log.e( + android.util.Log.e( TAG, "Failed to register $protectionType callback. Check if $permission permission is granted in AndroidManifest.xml" ) } -} \ No newline at end of file + + private fun handleThreat(threat: Threat) { + if (!isEnabled) { + cachedThreats.add(threat) + return + } + + when (threat) { + Threat.Screenshot -> Talsec.onScreenshotDetected() + Threat.ScreenRecording -> Talsec.onScreenRecordingDetected() + else -> throw IllegalArgumentException("Unexpected Threat type: $threat") + } + } +} diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/TalsecThreatHandler.kt b/android/src/main/kotlin/com/aheaditec/freerasp/handlers/TalsecThreatHandler.kt index fa6b214..f3b49a9 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/TalsecThreatHandler.kt +++ b/android/src/main/kotlin/com/aheaditec/freerasp/handlers/TalsecThreatHandler.kt @@ -26,6 +26,9 @@ internal object TalsecThreatHandler { internal fun start(context: Context, config: TalsecConfig) { attachListener(context) Talsec.start(context, config) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + ScreenProtector.enable() + } } /** diff --git a/pubspec.yaml b/pubspec.yaml index 9de8314..3e370bc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: freerasp description: Flutter library for improving app security and threat monitoring on Android and iOS mobile devices. Learn more about provided features on the freeRASP's homepage first. -version: 7.2.1 +version: 7.2.2 homepage: https://www.talsec.app/freerasp-in-app-protection-security-talsec repository: https://github.com/talsec/Free-RASP-Flutter