Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 12 additions & 12 deletions android/src/main/kotlin/com/aheaditec/freerasp/FreeraspPlugin.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.aheaditec.freerasp

import android.app.Activity
import android.content.Context
import android.os.Build
import androidx.lifecycle.Lifecycle
Expand All @@ -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)
}
Expand All @@ -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) {
Expand Down
108 changes: 63 additions & 45 deletions android/src/main/kotlin/com/aheaditec/freerasp/ScreenProtector.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,70 +15,48 @@ 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<Threat>()
private val screenCaptureCallback = ScreenCaptureCallback { handleThreat(Threat.Screenshot) }
private val screenRecordCallback: Consumer<Int> = Consumer<Int> { 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)
}

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
Expand Down Expand Up @@ -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 {
Expand All @@ -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"
)
}
}

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")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}

/**
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -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

Expand Down