Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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 example/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ android {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.aheaditec.freerasp_example"
// Talsec library needs higher version than default (16)
minSdkVersion 23
minSdkVersion flutter.minSdkVersion
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
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
Loading