replace plugin

This commit is contained in:
otsmr 2026-05-01 01:24:58 +02:00
parent 793b7f0562
commit e0c6a9617a
53 changed files with 1173 additions and 4496 deletions

View file

@ -10,11 +10,12 @@ introduction_screen: 4a90e557630b28834479ed9c64a9d2d0185d8e48
libsignal_protocol_dart: c95a1586057022acdbb9c76b1692d94cc549bcc7
lottie: 4f1a5a52bdf1e1c1e12fa97c96174dcb05419e19
mutex: 84ca903a3ac863735e3228c75a212133621f680f
no_screenshot: daf759e30219224630b4af0b82061d25a457a393
no_screenshot: fbfa2ed7ec4db782797fa6a7de8f207a2cba00bb
optional: 71c638891ce4f2aff35c7387727989f31f9d877d
photo_view: a13ca2fc387a3fb1276126959e092c44d0029987
pointycastle: bbd8569f68a7fccbdf0b92d0b44a9219c126c8dd
qr: 7b1e9665ca976f484e7975356cf26fc7a0ccf02e
qr_flutter: d5e7206396105d643113618290bbcc755d05f492
restart_app: 66897cb67e235bab85421647bfae036acb4438cb
screen_protector: 019c04d622d7b610d2903d3a347edc3ba76a6ed0
x25519: ecb1d357714537bba6e276ef45f093846d4beaee

View file

@ -54,8 +54,8 @@ flutter_sharing_intent:
restart_app:
git: https://github.com/gabrimatic/restart_app
no_screenshot:
git: https://github.com/FlutterPlaza/no_screenshot.git
screen_protector:
git: https://github.com/prongbang/screen_protector.git
flutter_markdown_plus:

View file

@ -1,26 +0,0 @@
Copyright (c) 2022, FlutterPlaza
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of FlutterPlaza nor the names of its contributors may
be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.

View file

@ -1,47 +0,0 @@
group 'com.flutterplaza.no_screenshot'
version '1.0-SNAPSHOT'
buildscript {
ext.kotlin_version = '2.1.0'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.6.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
android {
namespace "com.flutterplaza.no_screenshot"
compileSdk 36
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
defaultConfig {
minSdkVersion 16
}
}

View file

@ -1 +0,0 @@
rootProject.name = 'no_screenshot'

View file

@ -1,757 +0,0 @@
package com.flutterplaza.no_screenshot
import android.app.Activity
import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import android.database.ContentObserver
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.RenderEffect
import android.graphics.Shader
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.provider.MediaStore
import android.renderscript.Allocation
import android.renderscript.Element
import android.renderscript.RenderScript
import android.renderscript.ScriptIntrinsicBlur
import android.util.Log
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager.LayoutParams
import android.widget.FrameLayout
import android.widget.ImageView
import androidx.annotation.NonNull
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import java.util.concurrent.Executors
import org.json.JSONObject
const val SCREENSHOT_ON_CONST = "screenshotOn"
const val SCREENSHOT_OFF_CONST = "screenshotOff"
const val TOGGLE_SCREENSHOT_CONST = "toggleScreenshot"
const val PREF_NAME = "screenshot_pref"
const val START_SCREENSHOT_LISTENING_CONST = "startScreenshotListening"
const val STOP_SCREENSHOT_LISTENING_CONST = "stopScreenshotListening"
const val SCREENSHOT_PATH = "screenshot_path"
const val PREF_KEY_SCREENSHOT = "is_screenshot_on"
const val SCREENSHOT_TAKEN = "was_screenshot_taken"
const val SET_IMAGE_CONST = "toggleScreenshotWithImage"
const val SET_BLUR_CONST = "toggleScreenshotWithBlur"
const val SET_COLOR_CONST = "toggleScreenshotWithColor"
const val ENABLE_IMAGE_CONST = "screenshotWithImage"
const val ENABLE_BLUR_CONST = "screenshotWithBlur"
const val ENABLE_COLOR_CONST = "screenshotWithColor"
const val PREF_KEY_IMAGE_OVERLAY = "is_image_overlay_mode_enabled"
const val PREF_KEY_BLUR_OVERLAY = "is_blur_overlay_mode_enabled"
const val PREF_KEY_COLOR_OVERLAY = "is_color_overlay_mode_enabled"
const val PREF_KEY_BLUR_RADIUS = "blur_radius"
const val PREF_KEY_COLOR_VALUE = "color_value"
const val IS_SCREEN_RECORDING = "is_screen_recording"
const val START_SCREEN_RECORDING_LISTENING_CONST = "startScreenRecordingListening"
const val STOP_SCREEN_RECORDING_LISTENING_CONST = "stopScreenRecordingListening"
const val SCREENSHOT_METHOD_CHANNEL = "com.flutterplaza.no_screenshot_methods"
const val SCREENSHOT_EVENT_CHANNEL = "com.flutterplaza.no_screenshot_streams"
class NoScreenshotPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware,
EventChannel.StreamHandler {
private lateinit var methodChannel: MethodChannel
private lateinit var eventChannel: EventChannel
private lateinit var context: Context
private var activity: Activity? = null
private val preferences: SharedPreferences by lazy {
context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
}
private var screenshotObserver: ContentObserver? = null
private val handler = Handler(Looper.getMainLooper())
private var eventSink: EventChannel.EventSink? = null
private var lastSharedPreferencesState: String = ""
private var hasSharedPreferencesChanged: Boolean = false
private var isImageOverlayModeEnabled: Boolean = false
private var isBlurOverlayModeEnabled: Boolean = false
private var isColorOverlayModeEnabled: Boolean = false
private var overlayImageView: ImageView? = null
private var overlayBlurView: View? = null
private var overlayColorView: View? = null
private var blurRadius: Float = 30f
private var colorValue: Int = 0xFF000000.toInt()
private var lifecycleCallbacks: Application.ActivityLifecycleCallbacks? = null
private var isScreenRecording: Boolean = false
private var isRecordingListening: Boolean = false
private var screenCaptureCallback: Any? = null
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext
methodChannel =
MethodChannel(flutterPluginBinding.binaryMessenger, SCREENSHOT_METHOD_CHANNEL)
methodChannel.setMethodCallHandler(this)
eventChannel = EventChannel(flutterPluginBinding.binaryMessenger, SCREENSHOT_EVENT_CHANNEL)
eventChannel.setStreamHandler(this)
initScreenshotObserver()
registerLifecycleCallbacks()
}
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
methodChannel.setMethodCallHandler(null)
screenshotObserver?.let { context.contentResolver.unregisterContentObserver(it) }
unregisterLifecycleCallbacks()
}
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activity = binding.activity
restoreScreenshotState()
if (isRecordingListening) {
registerScreenCaptureCallback()
}
}
override fun onDetachedFromActivityForConfigChanges() {
unregisterScreenCaptureCallback()
removeImageOverlay()
removeBlurOverlay()
removeColorOverlay()
activity = null
}
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
activity = binding.activity
restoreScreenshotState()
if (isRecordingListening) {
registerScreenCaptureCallback()
}
}
override fun onDetachedFromActivity() {
unregisterScreenCaptureCallback()
removeImageOverlay()
removeBlurOverlay()
removeColorOverlay()
activity = null
}
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: MethodChannel.Result) {
when (call.method) {
SCREENSHOT_ON_CONST -> {
result.success(screenshotOn().also { updateSharedPreferencesState("") })
}
SCREENSHOT_OFF_CONST -> {
result.success(screenshotOff().also { updateSharedPreferencesState("") })
}
TOGGLE_SCREENSHOT_CONST -> {
toggleScreenshot()
result.success(true.also { updateSharedPreferencesState("") })
}
START_SCREENSHOT_LISTENING_CONST -> {
startListening()
result.success("Listening started")
}
STOP_SCREENSHOT_LISTENING_CONST -> {
stopListening()
result.success("Listening stopped".also { updateSharedPreferencesState("") })
}
SET_IMAGE_CONST -> {
result.success(toggleScreenshotWithImage())
}
SET_BLUR_CONST -> {
val radius = (call.argument<Double>("radius") ?: 30.0).toFloat()
result.success(toggleScreenshotWithBlur(radius))
}
SET_COLOR_CONST -> {
val color = call.argument<Int>("color") ?: 0xFF000000.toInt()
result.success(toggleScreenshotWithColor(color))
}
ENABLE_IMAGE_CONST -> {
result.success(enableImageOverlay())
}
ENABLE_BLUR_CONST -> {
val radius = (call.argument<Double>("radius") ?: 30.0).toFloat()
result.success(enableBlurOverlay(radius))
}
ENABLE_COLOR_CONST -> {
val color = call.argument<Int>("color") ?: 0xFF000000.toInt()
result.success(enableColorOverlay(color))
}
START_SCREEN_RECORDING_LISTENING_CONST -> {
startRecordingListening()
result.success("Recording listening started")
}
STOP_SCREEN_RECORDING_LISTENING_CONST -> {
stopRecordingListening()
result.success("Recording listening stopped".also { updateSharedPreferencesState("") })
}
else -> result.notImplemented()
}
}
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
eventSink = events
handler.postDelayed(screenshotStream, 1000)
}
override fun onCancel(arguments: Any?) {
handler.removeCallbacks(screenshotStream)
eventSink = null
}
private fun registerLifecycleCallbacks() {
val app = context as? Application ?: return
lifecycleCallbacks = object : Application.ActivityLifecycleCallbacks {
override fun onActivityPaused(act: Activity) {
if (act == activity && isImageOverlayModeEnabled) {
act.window?.clearFlags(LayoutParams.FLAG_SECURE)
showImageOverlay(act)
} else if (act == activity && isBlurOverlayModeEnabled) {
act.window?.clearFlags(LayoutParams.FLAG_SECURE)
showBlurOverlay(act)
} else if (act == activity && isColorOverlayModeEnabled) {
act.window?.clearFlags(LayoutParams.FLAG_SECURE)
showColorOverlay(act)
}
}
override fun onActivityResumed(act: Activity) {
if (act == activity && isImageOverlayModeEnabled) {
removeImageOverlay()
act.window?.addFlags(LayoutParams.FLAG_SECURE)
} else if (act == activity && isBlurOverlayModeEnabled) {
removeBlurOverlay()
act.window?.addFlags(LayoutParams.FLAG_SECURE)
} else if (act == activity && isColorOverlayModeEnabled) {
removeColorOverlay()
act.window?.addFlags(LayoutParams.FLAG_SECURE)
}
}
override fun onActivityCreated(act: Activity, savedInstanceState: Bundle?) {}
override fun onActivityStarted(act: Activity) {}
override fun onActivityStopped(act: Activity) {}
override fun onActivitySaveInstanceState(act: Activity, outState: Bundle) {
if (act == activity && isImageOverlayModeEnabled) {
showImageOverlay(act)
} else if (act == activity && isBlurOverlayModeEnabled) {
showBlurOverlay(act)
} else if (act == activity && isColorOverlayModeEnabled) {
showColorOverlay(act)
}
}
override fun onActivityDestroyed(act: Activity) {}
}
app.registerActivityLifecycleCallbacks(lifecycleCallbacks)
}
private fun unregisterLifecycleCallbacks() {
val app = context as? Application ?: return
lifecycleCallbacks?.let { app.unregisterActivityLifecycleCallbacks(it) }
lifecycleCallbacks = null
}
private fun showImageOverlay(activity: Activity) {
if (overlayImageView != null) return
val resId = activity.resources.getIdentifier("image", "drawable", activity.packageName)
if (resId == 0) return
activity.runOnUiThread {
val imageView = ImageView(activity).apply {
setImageResource(resId)
scaleType = ImageView.ScaleType.CENTER_CROP
layoutParams = FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
}
(activity.window.decorView as? ViewGroup)?.addView(imageView)
overlayImageView = imageView
}
}
private fun removeImageOverlay() {
val imageView = overlayImageView ?: return
val act = activity
if (act != null) {
act.runOnUiThread {
(imageView.parent as? ViewGroup)?.removeView(imageView)
overlayImageView = null
}
} else {
(imageView.parent as? ViewGroup)?.removeView(imageView)
overlayImageView = null
}
}
private fun toggleScreenshotWithImage(): Boolean {
isImageOverlayModeEnabled = !preferences.getBoolean(PREF_KEY_IMAGE_OVERLAY, false)
saveImageOverlayState(isImageOverlayModeEnabled)
if (isImageOverlayModeEnabled) {
// Deactivate blur and color modes if active (mutual exclusivity)
if (isBlurOverlayModeEnabled) {
isBlurOverlayModeEnabled = false
saveBlurOverlayState(false)
removeBlurOverlay()
}
if (isColorOverlayModeEnabled) {
isColorOverlayModeEnabled = false
saveColorOverlayState(false)
removeColorOverlay()
}
screenshotOff()
} else {
screenshotOn()
removeImageOverlay()
}
updateSharedPreferencesState("")
return isImageOverlayModeEnabled
}
private fun toggleScreenshotWithBlur(radius: Float): Boolean {
isBlurOverlayModeEnabled = !preferences.getBoolean(PREF_KEY_BLUR_OVERLAY, false)
blurRadius = radius
saveBlurOverlayState(isBlurOverlayModeEnabled)
saveBlurRadius(radius)
if (isBlurOverlayModeEnabled) {
// Deactivate image and color modes if active (mutual exclusivity)
if (isImageOverlayModeEnabled) {
isImageOverlayModeEnabled = false
saveImageOverlayState(false)
removeImageOverlay()
}
if (isColorOverlayModeEnabled) {
isColorOverlayModeEnabled = false
saveColorOverlayState(false)
removeColorOverlay()
}
screenshotOff()
} else {
screenshotOn()
removeBlurOverlay()
}
updateSharedPreferencesState("")
return isBlurOverlayModeEnabled
}
@Suppress("DEPRECATION")
private fun showBlurOverlay(activity: Activity) {
if (overlayBlurView != null) return
val decorView = activity.window?.decorView ?: return
val radius = blurRadius.coerceAtLeast(0.1f)
if (Build.VERSION.SDK_INT >= 31) {
// API 31+: GPU blur via RenderEffect on decorView — supports any radius
activity.runOnUiThread {
decorView.setRenderEffect(
RenderEffect.createBlurEffect(radius, radius, Shader.TileMode.CLAMP)
)
overlayBlurView = decorView
}
} else if (Build.VERSION.SDK_INT >= 17) {
// API 1730: Capture bitmap, blur with RenderScript (max 25f)
activity.runOnUiThread {
val width = decorView.width
val height = decorView.height
if (width <= 0 || height <= 0) return@runOnUiThread
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
decorView.draw(canvas)
val rs = RenderScript.create(activity)
val input = Allocation.createFromBitmap(rs, bitmap)
val output = Allocation.createTyped(rs, input.type)
val script = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs))
script.setRadius(radius.coerceAtMost(25f))
script.setInput(input)
script.forEach(output)
output.copyTo(bitmap)
script.destroy()
input.destroy()
output.destroy()
rs.destroy()
val imageView = ImageView(activity).apply {
setImageBitmap(bitmap)
scaleType = ImageView.ScaleType.FIT_XY
layoutParams = FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
}
(decorView as? ViewGroup)?.addView(imageView)
overlayBlurView = imageView
}
}
// API <17: FLAG_SECURE alone prevents app switcher preview; no blur needed.
}
private fun removeBlurOverlay() {
val blurView = overlayBlurView ?: return
val act = activity
if (act != null) {
act.runOnUiThread {
if (Build.VERSION.SDK_INT >= 31 && blurView === act.window?.decorView) {
blurView.setRenderEffect(null)
} else {
(blurView.parent as? ViewGroup)?.removeView(blurView)
}
overlayBlurView = null
}
} else {
if (Build.VERSION.SDK_INT >= 31) {
blurView.setRenderEffect(null)
} else {
(blurView.parent as? ViewGroup)?.removeView(blurView)
}
overlayBlurView = null
}
}
private fun toggleScreenshotWithColor(color: Int): Boolean {
isColorOverlayModeEnabled = !preferences.getBoolean(PREF_KEY_COLOR_OVERLAY, false)
colorValue = color
saveColorOverlayState(isColorOverlayModeEnabled)
saveColorValue(color)
if (isColorOverlayModeEnabled) {
// Deactivate image and blur modes if active (mutual exclusivity)
if (isImageOverlayModeEnabled) {
isImageOverlayModeEnabled = false
saveImageOverlayState(false)
removeImageOverlay()
}
if (isBlurOverlayModeEnabled) {
isBlurOverlayModeEnabled = false
saveBlurOverlayState(false)
removeBlurOverlay()
}
screenshotOff()
} else {
screenshotOn()
removeColorOverlay()
}
updateSharedPreferencesState("")
return isColorOverlayModeEnabled
}
private fun enableImageOverlay(): Boolean {
isImageOverlayModeEnabled = true
saveImageOverlayState(true)
if (isBlurOverlayModeEnabled) {
isBlurOverlayModeEnabled = false
saveBlurOverlayState(false)
removeBlurOverlay()
}
if (isColorOverlayModeEnabled) {
isColorOverlayModeEnabled = false
saveColorOverlayState(false)
removeColorOverlay()
}
screenshotOff()
updateSharedPreferencesState("")
return true
}
private fun enableBlurOverlay(radius: Float): Boolean {
isBlurOverlayModeEnabled = true
blurRadius = radius
saveBlurOverlayState(true)
saveBlurRadius(radius)
if (isImageOverlayModeEnabled) {
isImageOverlayModeEnabled = false
saveImageOverlayState(false)
removeImageOverlay()
}
if (isColorOverlayModeEnabled) {
isColorOverlayModeEnabled = false
saveColorOverlayState(false)
removeColorOverlay()
}
screenshotOff()
updateSharedPreferencesState("")
return true
}
private fun enableColorOverlay(color: Int): Boolean {
isColorOverlayModeEnabled = true
colorValue = color
saveColorOverlayState(true)
saveColorValue(color)
if (isImageOverlayModeEnabled) {
isImageOverlayModeEnabled = false
saveImageOverlayState(false)
removeImageOverlay()
}
if (isBlurOverlayModeEnabled) {
isBlurOverlayModeEnabled = false
saveBlurOverlayState(false)
removeBlurOverlay()
}
screenshotOff()
updateSharedPreferencesState("")
return true
}
private fun showColorOverlay(activity: Activity) {
if (overlayColorView != null) return
val decorView = activity.window?.decorView ?: return
activity.runOnUiThread {
val colorView = View(activity).apply {
setBackgroundColor(colorValue)
layoutParams = FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
}
(decorView as? ViewGroup)?.addView(colorView)
overlayColorView = colorView
}
}
private fun removeColorOverlay() {
val colorView = overlayColorView ?: return
val act = activity
if (act != null) {
act.runOnUiThread {
(colorView.parent as? ViewGroup)?.removeView(colorView)
overlayColorView = null
}
} else {
(colorView.parent as? ViewGroup)?.removeView(colorView)
overlayColorView = null
}
}
// ── Screen Recording Detection ─────────────────────────────────────
private fun startRecordingListening() {
if (isRecordingListening) return
isRecordingListening = true
registerScreenCaptureCallback()
updateSharedPreferencesState("")
}
private fun stopRecordingListening() {
if (!isRecordingListening) return
isRecordingListening = false
unregisterScreenCaptureCallback()
isScreenRecording = false
updateSharedPreferencesState("")
}
private fun registerScreenCaptureCallback() {
if (android.os.Build.VERSION.SDK_INT >= 34) {
val act = activity ?: return
if (screenCaptureCallback != null) return
val callback = Activity.ScreenCaptureCallback {
isScreenRecording = true
updateSharedPreferencesState("", System.currentTimeMillis())
}
act.registerScreenCaptureCallback(act.mainExecutor, callback)
screenCaptureCallback = callback
}
}
private fun unregisterScreenCaptureCallback() {
if (android.os.Build.VERSION.SDK_INT >= 34) {
val act = activity ?: return
val callback = screenCaptureCallback as? Activity.ScreenCaptureCallback ?: return
act.unregisterScreenCaptureCallback(callback)
screenCaptureCallback = null
}
}
private fun initScreenshotObserver() {
screenshotObserver = object : ContentObserver(Handler()) {
override fun onChange(selfChange: Boolean, uri: Uri?) {
super.onChange(selfChange, uri)
uri?.let {
if (it.toString()
.contains(MediaStore.Images.Media.EXTERNAL_CONTENT_URI.toString())
) {
Log.d("ScreenshotProtection", "Screenshot detected")
var timestampMs = System.currentTimeMillis()
var displayName = ""
try {
val projection = arrayOf(
MediaStore.Images.Media.DATE_ADDED,
MediaStore.Images.Media.DISPLAY_NAME
)
context.contentResolver.query(it, projection, null, null, null)?.use { cursor ->
if (cursor.moveToFirst()) {
val dateIdx = cursor.getColumnIndex(MediaStore.Images.Media.DATE_ADDED)
if (dateIdx >= 0) {
val dateAdded = cursor.getLong(dateIdx)
if (dateAdded > 0) timestampMs = dateAdded * 1000
}
val nameIdx = cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME)
if (nameIdx >= 0) {
displayName = cursor.getString(nameIdx) ?: ""
}
}
}
} catch (_: Exception) {
// Query may fail due to permissions; fall back to defaults.
}
updateSharedPreferencesState(it.path ?: "", timestampMs, displayName)
}
}
}
}
}
private fun startListening() {
screenshotObserver?.let {
context.contentResolver.registerContentObserver(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
true,
it
)
}
}
private fun stopListening() {
screenshotObserver?.let { context.contentResolver.unregisterContentObserver(it) }
}
private fun screenshotOff(): Boolean = try {
activity?.window?.addFlags(LayoutParams.FLAG_SECURE)
saveScreenshotState(true)
true
} catch (e: Exception) {
false
}
private fun screenshotOn(): Boolean = try {
activity?.window?.clearFlags(LayoutParams.FLAG_SECURE)
saveScreenshotState(false)
true
} catch (e: Exception) {
false
}
private fun toggleScreenshot() {
activity?.window?.attributes?.flags?.let { flags ->
if (flags and LayoutParams.FLAG_SECURE != 0) {
screenshotOn()
} else {
screenshotOff()
}
}
}
private fun saveScreenshotState(isSecure: Boolean) {
Executors.newSingleThreadExecutor().execute {
preferences.edit().putBoolean(PREF_KEY_SCREENSHOT, isSecure).apply()
}
}
private fun saveImageOverlayState(enabled: Boolean) {
Executors.newSingleThreadExecutor().execute {
preferences.edit().putBoolean(PREF_KEY_IMAGE_OVERLAY, enabled).apply()
}
}
private fun saveBlurOverlayState(enabled: Boolean) {
Executors.newSingleThreadExecutor().execute {
preferences.edit().putBoolean(PREF_KEY_BLUR_OVERLAY, enabled).apply()
}
}
private fun saveBlurRadius(radius: Float) {
Executors.newSingleThreadExecutor().execute {
preferences.edit().putFloat(PREF_KEY_BLUR_RADIUS, radius).apply()
}
}
private fun saveColorOverlayState(enabled: Boolean) {
Executors.newSingleThreadExecutor().execute {
preferences.edit().putBoolean(PREF_KEY_COLOR_OVERLAY, enabled).apply()
}
}
private fun saveColorValue(color: Int) {
Executors.newSingleThreadExecutor().execute {
preferences.edit().putInt(PREF_KEY_COLOR_VALUE, color).apply()
}
}
private fun restoreScreenshotState() {
Executors.newSingleThreadExecutor().execute {
val isSecure = preferences.getBoolean(PREF_KEY_SCREENSHOT, false)
val overlayEnabled = preferences.getBoolean(PREF_KEY_IMAGE_OVERLAY, false)
val blurEnabled = preferences.getBoolean(PREF_KEY_BLUR_OVERLAY, false)
val colorEnabled = preferences.getBoolean(PREF_KEY_COLOR_OVERLAY, false)
isImageOverlayModeEnabled = overlayEnabled
isBlurOverlayModeEnabled = blurEnabled
isColorOverlayModeEnabled = colorEnabled
blurRadius = preferences.getFloat(PREF_KEY_BLUR_RADIUS, 30f)
colorValue = preferences.getInt(PREF_KEY_COLOR_VALUE, 0xFF000000.toInt())
activity?.runOnUiThread {
if (isImageOverlayModeEnabled || isBlurOverlayModeEnabled || isColorOverlayModeEnabled || isSecure) {
screenshotOff()
} else {
screenshotOn()
}
}
}
}
private fun updateSharedPreferencesState(screenshotData: String, timestampMs: Long = 0L, sourceApp: String = "") {
Handler(Looper.getMainLooper()).postDelayed({
val isSecure =
(activity?.window?.attributes?.flags ?: 0) and LayoutParams.FLAG_SECURE != 0
val jsonString = convertMapToJsonString(
mapOf(
PREF_KEY_SCREENSHOT to isSecure,
SCREENSHOT_PATH to screenshotData,
SCREENSHOT_TAKEN to screenshotData.isNotEmpty(),
IS_SCREEN_RECORDING to isScreenRecording,
"timestamp" to timestampMs,
"source_app" to sourceApp
)
)
if (lastSharedPreferencesState != jsonString) {
hasSharedPreferencesChanged = true
lastSharedPreferencesState = jsonString
}
}, 100)
}
private fun convertMapToJsonString(map: Map<String, Any>): String {
return JSONObject(map).toString()
}
private val screenshotStream = object : Runnable {
override fun run() {
if (hasSharedPreferencesChanged) {
eventSink?.success(lastSharedPreferencesState)
hasSharedPreferencesChanged = false
}
handler.postDelayed(this, 1000)
}
}
}

View file

@ -1,27 +0,0 @@
package com.flutterplaza.no_screenshot
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import kotlin.test.Test
import org.mockito.Mockito
/*
* This demonstrates a simple unit test of the Kotlin portion of this plugin's implementation.
*
* Once you have built the plugin's example app, you can run these tests from the command
* line by running `./gradlew testDebugUnitTest` in the `example/android/` directory, or
* you can run them directly from IDEs that support JUnit such as Android Studio.
*/
internal class NoScreenshotPluginTest {
@Test
fun onMethodCall_getPlatformVersion_returnsExpectedValue() {
val plugin = NoScreenshotPlugin()
val call = MethodCall("getPlatformVersion", null)
val mockResult: MethodChannel.Result = Mockito.mock(MethodChannel.Result::class.java)
plugin.onMethodCall(call, mockResult)
Mockito.verify(mockResult).success("Android " + android.os.Build.VERSION.RELEASE)
}
}

View file

@ -1,4 +0,0 @@
#import <Flutter/Flutter.h>
@interface NoScreenshotPlugin : NSObject<FlutterPlugin>
@end

View file

@ -1,24 +0,0 @@
#
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
# Run `pod lib lint no_screenshot.podspec` to validate before publishing.
#
Pod::Spec.new do |s|
s.name = 'no_screenshot'
s.version = '0.10.0'
s.summary = 'Flutter plugin to enable, disable or toggle screenshot support in your application.'
s.description = <<-DESC
A new Flutter plugin project.
DESC
s.homepage = 'https://github.com/FlutterPlaza/no_screenshot'
s.license = { :file => '../LICENSE' }
s.author = { 'FlutterPlaza' => 'dev@flutterplaza.com' }
s.source = { :path => '.' }
s.source_files = 'no_screenshot/Sources/no_screenshot/**/*.swift', 'Classes/**/*.{h,m}'
s.dependency 'Flutter'
s.platform = :ios, '13.0'
s.resource_bundles = { 'no_screenshot_privacy' => ['no_screenshot/Sources/no_screenshot/PrivacyInfo.xcprivacy'] }
# Flutter.framework does not contain a i386 slice.
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
s.swift_version = "5.0"
end

View file

@ -1,24 +0,0 @@
// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "no_screenshot",
platforms: [
.iOS("13.0")
],
products: [
.library(name: "no-screenshot", targets: ["no_screenshot"])
],
dependencies: [],
targets: [
.target(
name: "no_screenshot",
dependencies: [],
resources: [
.process("PrivacyInfo.xcprivacy")
]
)
]
)

View file

@ -1,638 +0,0 @@
import Flutter
import UIKit
#if SWIFT_PACKAGE
@objc(NoScreenshotPlugin)
#endif
public class IOSNoScreenshotPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, FlutterSceneLifeCycleDelegate {
private var screenPrevent = UITextField()
private var screenImage: UIImageView? = nil
private weak var attachedWindow: UIWindow? = nil
private static var methodChannel: FlutterMethodChannel? = nil
private static var eventChannel: FlutterEventChannel? = nil
private static var preventScreenShot: Bool = false
private var eventSink: FlutterEventSink? = nil
private var lastSharedPreferencesState: String = ""
private var hasSharedPreferencesChanged: Bool = false
private var isImageOverlayModeEnabled: Bool = false
private var isBlurOverlayModeEnabled: Bool = false
private var blurOverlayView: UIView? = nil
private var blurRadius: Double = 30.0
private var isColorOverlayModeEnabled: Bool = false
private var colorOverlayView: UIView? = nil
private var colorValue: Int = 0xFF000000
private var isScreenRecording: Bool = false
private var isRecordingListening: Bool = false
private static let ENABLESCREENSHOT = false
private static let DISABLESCREENSHOT = true
private static let preventScreenShotKey = "preventScreenShot"
private static let imageOverlayModeKey = "imageOverlayMode"
private static let blurOverlayModeKey = "blurOverlayMode"
private static let blurRadiusKey = "blurRadius"
private static let colorOverlayModeKey = "colorOverlayMode"
private static let colorValueKey = "colorValue"
private static let methodChannelName = "com.flutterplaza.no_screenshot_methods"
private static let eventChannelName = "com.flutterplaza.no_screenshot_streams"
private static let screenshotPathPlaceholder = "screenshot_path_placeholder"
override init() {
super.init()
// Restore the saved state from UserDefaults
let fetchVal = UserDefaults.standard.bool(forKey: IOSNoScreenshotPlugin.preventScreenShotKey)
isImageOverlayModeEnabled = UserDefaults.standard.bool(forKey: IOSNoScreenshotPlugin.imageOverlayModeKey)
isBlurOverlayModeEnabled = UserDefaults.standard.bool(forKey: IOSNoScreenshotPlugin.blurOverlayModeKey)
let savedRadius = UserDefaults.standard.double(forKey: IOSNoScreenshotPlugin.blurRadiusKey)
blurRadius = savedRadius > 0 ? savedRadius : 30.0
isColorOverlayModeEnabled = UserDefaults.standard.bool(forKey: IOSNoScreenshotPlugin.colorOverlayModeKey)
colorValue = UserDefaults.standard.integer(forKey: IOSNoScreenshotPlugin.colorValueKey)
if colorValue == 0 { colorValue = 0xFF000000 }
updateScreenshotState(isScreenshotBlocked: fetchVal)
}
public static func register(with registrar: FlutterPluginRegistrar) {
methodChannel = FlutterMethodChannel(name: methodChannelName, binaryMessenger: registrar.messenger())
eventChannel = FlutterEventChannel(name: eventChannelName, binaryMessenger: registrar.messenger())
let instance = IOSNoScreenshotPlugin()
registrar.addMethodCallDelegate(instance, channel: methodChannel!)
eventChannel?.setStreamHandler(instance)
registrar.addApplicationDelegate(instance)
registrar.addSceneDelegate(instance)
}
// MARK: - Inline Screenshot Prevention (replaces ScreenProtectorKit)
private func configurePreventionScreenshot(window: UIWindow) {
guard let rootLayer = window.layer.superlayer else { return }
guard screenPrevent.layer.superlayer == nil else { return }
screenPrevent.semanticContentAttribute = .forceLeftToRight // RTL fix
screenPrevent.textAlignment = .left // RTL fix
// Briefly add to the window so UIKit creates the text field's
// internal sublayer hierarchy, then force a layout pass and
// immediately remove so screenPrevent is NOT a subview of window.
// This avoids a circular view-hierarchy that causes EXC_BAD_ACCESS
// (stack overflow in _collectExistingTraitCollectionsForTraitTracking)
// on iOS 26+.
window.addSubview(screenPrevent)
screenPrevent.layoutIfNeeded()
screenPrevent.removeFromSuperview()
// Keep the layer at the origin so reparenting window.layer
// does not shift the app content.
screenPrevent.layer.frame = .zero
rootLayer.addSublayer(screenPrevent.layer)
if #available(iOS 17.0, *) {
screenPrevent.layer.sublayers?.last?.addSublayer(window.layer)
} else {
screenPrevent.layer.sublayers?.first?.addSublayer(window.layer)
}
}
private func enablePreventScreenshot() {
screenPrevent.isSecureTextEntry = true
}
private func disablePreventScreenshot() {
screenPrevent.isSecureTextEntry = false
}
private func enableImageScreen(named: String) {
guard let window = attachedWindow else { return }
let imageView = UIImageView(frame: window.bounds)
imageView.image = UIImage(named: named)
imageView.isUserInteractionEnabled = false
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
window.addSubview(imageView)
screenImage = imageView
}
private func disableImageScreen() {
screenImage?.removeFromSuperview()
screenImage = nil
}
// MARK: - Shared Lifecycle Helpers
//
// Overlay lifecycle is intentionally handled in exactly two places:
// SHOW: handleWillResignActive (app is about to lose focus)
// HIDE: handleDidBecomeActive (app is fully interactive again)
//
// willResignActive always fires before didEnterBackground, and
// didBecomeActive always fires after willEnterForeground, so a single
// show/hide pair covers both the app-switcher peek and the full
// background foreground round-trip without double-showing the overlay.
private func handleWillResignActive() {
persistState()
if isImageOverlayModeEnabled {
// Temporarily lift screenshot prevention so the overlay image is
// visible in the app switcher (otherwise the secure text field
// would show a blank screen).
disablePreventScreenshot()
enableImageScreen(named: "image")
} else if isBlurOverlayModeEnabled {
disablePreventScreenshot()
enableBlurScreen(radius: blurRadius)
} else if isColorOverlayModeEnabled {
disablePreventScreenshot()
enableColorScreen(color: colorValue)
}
}
private func handleDidBecomeActive() {
// Remove overlays FIRST.
if isImageOverlayModeEnabled {
disableImageScreen()
} else if isBlurOverlayModeEnabled {
disableBlurScreen()
} else if isColorOverlayModeEnabled {
disableColorScreen()
}
// Now restore screenshot protection (and re-attach the window if it
// changed while the app was in the background).
fetchPersistedState()
}
private func handleDidEnterBackground() {
persistState()
}
private func handleWillTerminate() {
persistState()
}
// MARK: - App Delegate Lifecycle (for apps not yet using UIScene)
public func applicationWillResignActive(_ application: UIApplication) { handleWillResignActive() }
public func applicationDidBecomeActive(_ application: UIApplication) { handleDidBecomeActive() }
public func applicationWillEnterForeground(_ application: UIApplication) { /* handled in didBecomeActive */ }
public func applicationDidEnterBackground(_ application: UIApplication) { handleDidEnterBackground() }
public func applicationWillTerminate(_ application: UIApplication) { handleWillTerminate() }
// MARK: - Scene Delegate Lifecycle (for apps using UIScene)
public func sceneWillResignActive(_ scene: UIScene) { handleWillResignActive() }
public func sceneDidBecomeActive(_ scene: UIScene) { handleDidBecomeActive() }
public func sceneWillEnterForeground(_ scene: UIScene) { /* handled in didBecomeActive */ }
public func sceneDidEnterBackground(_ scene: UIScene) { handleDidEnterBackground() }
func persistState() {
// Persist the state when changed
UserDefaults.standard.set(IOSNoScreenshotPlugin.preventScreenShot, forKey: IOSNoScreenshotPlugin.preventScreenShotKey)
UserDefaults.standard.set(isImageOverlayModeEnabled, forKey: IOSNoScreenshotPlugin.imageOverlayModeKey)
UserDefaults.standard.set(isBlurOverlayModeEnabled, forKey: IOSNoScreenshotPlugin.blurOverlayModeKey)
UserDefaults.standard.set(blurRadius, forKey: IOSNoScreenshotPlugin.blurRadiusKey)
UserDefaults.standard.set(isColorOverlayModeEnabled, forKey: IOSNoScreenshotPlugin.colorOverlayModeKey)
UserDefaults.standard.set(colorValue, forKey: IOSNoScreenshotPlugin.colorValueKey)
print("Persisted state: \(IOSNoScreenshotPlugin.preventScreenShot), imageOverlay: \(isImageOverlayModeEnabled), blurOverlay: \(isBlurOverlayModeEnabled), blurRadius: \(blurRadius), colorOverlay: \(isColorOverlayModeEnabled), colorValue: \(colorValue)")
updateSharedPreferencesState("")
}
func fetchPersistedState() {
// Restore the saved state from UserDefaults
let fetchVal = UserDefaults.standard.bool(forKey: IOSNoScreenshotPlugin.preventScreenShotKey) ? IOSNoScreenshotPlugin.DISABLESCREENSHOT : IOSNoScreenshotPlugin.ENABLESCREENSHOT
isImageOverlayModeEnabled = UserDefaults.standard.bool(forKey: IOSNoScreenshotPlugin.imageOverlayModeKey)
isBlurOverlayModeEnabled = UserDefaults.standard.bool(forKey: IOSNoScreenshotPlugin.blurOverlayModeKey)
let savedRadius = UserDefaults.standard.double(forKey: IOSNoScreenshotPlugin.blurRadiusKey)
blurRadius = savedRadius > 0 ? savedRadius : 30.0
isColorOverlayModeEnabled = UserDefaults.standard.bool(forKey: IOSNoScreenshotPlugin.colorOverlayModeKey)
colorValue = UserDefaults.standard.integer(forKey: IOSNoScreenshotPlugin.colorValueKey)
if colorValue == 0 { colorValue = 0xFF000000 }
updateScreenshotState(isScreenshotBlocked: fetchVal)
print("Fetched state: \(IOSNoScreenshotPlugin.preventScreenShot), imageOverlay: \(isImageOverlayModeEnabled), blurOverlay: \(isBlurOverlayModeEnabled), blurRadius: \(blurRadius), colorOverlay: \(isColorOverlayModeEnabled), colorValue: \(colorValue)")
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "screenshotOff":
shotOff()
result(true)
case "screenshotOn":
shotOn()
result(true)
case "toggleScreenshotWithImage":
let isActive = toggleScreenshotWithImage()
result(isActive)
case "toggleScreenshotWithBlur":
let radius = (call.arguments as? [String: Any])?["radius"] as? Double ?? 30.0
let isActive = toggleScreenshotWithBlur(radius: radius)
result(isActive)
case "toggleScreenshotWithColor":
let color = (call.arguments as? [String: Any])?["color"] as? Int ?? 0xFF000000
let isActive = toggleScreenshotWithColor(color: color)
result(isActive)
case "toggleScreenshot":
IOSNoScreenshotPlugin.preventScreenShot ? shotOn() : shotOff()
result(true)
case "screenshotWithImage":
enableImageOverlay()
result(true)
case "screenshotWithBlur":
let radius = (call.arguments as? [String: Any])?["radius"] as? Double ?? 30.0
enableBlurOverlay(radius: radius)
result(true)
case "screenshotWithColor":
let color = (call.arguments as? [String: Any])?["color"] as? Int ?? 0xFF000000
enableColorOverlay(color: color)
result(true)
case "startScreenshotListening":
startListening()
result("Listening started")
case "stopScreenshotListening":
stopListening()
result("Listening stopped")
case "startScreenRecordingListening":
startRecordingListening()
result("Recording listening started")
case "stopScreenRecordingListening":
stopRecordingListening()
result("Recording listening stopped")
default:
result(FlutterMethodNotImplemented)
}
}
private func shotOff() {
IOSNoScreenshotPlugin.preventScreenShot = IOSNoScreenshotPlugin.DISABLESCREENSHOT
enablePreventScreenshot()
persistState()
}
private func shotOn() {
IOSNoScreenshotPlugin.preventScreenShot = IOSNoScreenshotPlugin.ENABLESCREENSHOT
disablePreventScreenshot()
persistState()
}
private func toggleScreenshotWithImage() -> Bool {
// Toggle the image overlay mode state
isImageOverlayModeEnabled.toggle()
if isImageOverlayModeEnabled {
// Deactivate blur mode if active (mutual exclusivity)
if isBlurOverlayModeEnabled {
isBlurOverlayModeEnabled = false
disableBlurScreen()
}
// Deactivate color mode if active (mutual exclusivity)
if isColorOverlayModeEnabled {
isColorOverlayModeEnabled = false
disableColorScreen()
}
// Mode is now active (true) - screenshot prevention should be ON (screenshots blocked)
IOSNoScreenshotPlugin.preventScreenShot = IOSNoScreenshotPlugin.DISABLESCREENSHOT
enablePreventScreenshot()
} else {
// Mode is now inactive (false) - screenshot prevention should be OFF (screenshots allowed)
IOSNoScreenshotPlugin.preventScreenShot = IOSNoScreenshotPlugin.ENABLESCREENSHOT
disablePreventScreenshot()
disableImageScreen()
}
persistState()
return isImageOverlayModeEnabled
}
private func toggleScreenshotWithBlur(radius: Double) -> Bool {
isBlurOverlayModeEnabled.toggle()
blurRadius = radius
if isBlurOverlayModeEnabled {
// Deactivate image mode if active (mutual exclusivity)
if isImageOverlayModeEnabled {
isImageOverlayModeEnabled = false
disableImageScreen()
}
// Deactivate color mode if active (mutual exclusivity)
if isColorOverlayModeEnabled {
isColorOverlayModeEnabled = false
disableColorScreen()
}
IOSNoScreenshotPlugin.preventScreenShot = IOSNoScreenshotPlugin.DISABLESCREENSHOT
enablePreventScreenshot()
} else {
IOSNoScreenshotPlugin.preventScreenShot = IOSNoScreenshotPlugin.ENABLESCREENSHOT
disablePreventScreenshot()
disableBlurScreen()
}
persistState()
return isBlurOverlayModeEnabled
}
private func enableBlurScreen(radius: Double) {
guard let window = attachedWindow else { return }
// Capture the current window content as a snapshot.
let renderer = UIGraphicsImageRenderer(bounds: window.bounds)
let snapshot = renderer.image { _ in
window.drawHierarchy(in: window.bounds, afterScreenUpdates: false)
}
// Apply a true CIGaussianBlur (no tinting / darkening).
guard let ciImage = CIImage(image: snapshot),
let filter = CIFilter(name: "CIGaussianBlur") else { return }
filter.setValue(ciImage, forKey: kCIInputImageKey)
filter.setValue(radius, forKey: kCIInputRadiusKey)
let context = CIContext(options: nil)
guard let output = filter.outputImage,
let cgImage = context.createCGImage(output, from: ciImage.extent) else { return }
let imageView = UIImageView(frame: window.bounds)
imageView.image = UIImage(cgImage: cgImage)
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
imageView.isUserInteractionEnabled = false
imageView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
window.addSubview(imageView)
blurOverlayView = imageView
}
private func disableBlurScreen() {
blurOverlayView?.removeFromSuperview()
blurOverlayView = nil
}
private func toggleScreenshotWithColor(color: Int) -> Bool {
isColorOverlayModeEnabled.toggle()
colorValue = color
if isColorOverlayModeEnabled {
// Deactivate image and blur modes (mutual exclusivity)
if isImageOverlayModeEnabled {
isImageOverlayModeEnabled = false
disableImageScreen()
}
if isBlurOverlayModeEnabled {
isBlurOverlayModeEnabled = false
disableBlurScreen()
}
IOSNoScreenshotPlugin.preventScreenShot = IOSNoScreenshotPlugin.DISABLESCREENSHOT
enablePreventScreenshot()
} else {
IOSNoScreenshotPlugin.preventScreenShot = IOSNoScreenshotPlugin.ENABLESCREENSHOT
disablePreventScreenshot()
disableColorScreen()
}
persistState()
return isColorOverlayModeEnabled
}
// MARK: - Idempotent enable methods (always-on, no toggle)
private func enableImageOverlay() {
isImageOverlayModeEnabled = true
if isBlurOverlayModeEnabled {
isBlurOverlayModeEnabled = false
disableBlurScreen()
}
if isColorOverlayModeEnabled {
isColorOverlayModeEnabled = false
disableColorScreen()
}
IOSNoScreenshotPlugin.preventScreenShot = IOSNoScreenshotPlugin.DISABLESCREENSHOT
enablePreventScreenshot()
persistState()
}
private func enableBlurOverlay(radius: Double) {
isBlurOverlayModeEnabled = true
blurRadius = radius
if isImageOverlayModeEnabled {
isImageOverlayModeEnabled = false
disableImageScreen()
}
if isColorOverlayModeEnabled {
isColorOverlayModeEnabled = false
disableColorScreen()
}
IOSNoScreenshotPlugin.preventScreenShot = IOSNoScreenshotPlugin.DISABLESCREENSHOT
enablePreventScreenshot()
persistState()
}
private func enableColorOverlay(color: Int) {
isColorOverlayModeEnabled = true
colorValue = color
if isImageOverlayModeEnabled {
isImageOverlayModeEnabled = false
disableImageScreen()
}
if isBlurOverlayModeEnabled {
isBlurOverlayModeEnabled = false
disableBlurScreen()
}
IOSNoScreenshotPlugin.preventScreenShot = IOSNoScreenshotPlugin.DISABLESCREENSHOT
enablePreventScreenshot()
persistState()
}
private func enableColorScreen(color: Int) {
guard let window = attachedWindow else { return }
let a = CGFloat((color >> 24) & 0xFF) / 255.0
let r = CGFloat((color >> 16) & 0xFF) / 255.0
let g = CGFloat((color >> 8) & 0xFF) / 255.0
let b = CGFloat(color & 0xFF) / 255.0
let uiColor = UIColor(red: r, green: g, blue: b, alpha: a)
let colorView = UIView(frame: window.bounds)
colorView.backgroundColor = uiColor
colorView.isUserInteractionEnabled = false
colorView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
window.addSubview(colorView)
colorOverlayView = colorView
}
private func disableColorScreen() {
colorOverlayView?.removeFromSuperview()
colorOverlayView = nil
}
private func startListening() {
NotificationCenter.default.addObserver(self, selector: #selector(screenshotDetected), name: UIApplication.userDidTakeScreenshotNotification, object: nil)
persistState()
}
private func stopListening() {
NotificationCenter.default.removeObserver(self, name: UIApplication.userDidTakeScreenshotNotification, object: nil)
persistState()
}
// MARK: - Screen Recording Detection
private var isScreenCaptured: Bool {
if let windowScene = attachedWindow?.windowScene {
return windowScene.screen.isCaptured
}
if let windowScene = UIApplication.shared.connectedScenes
.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene {
return windowScene.screen.isCaptured
}
return false
}
private func startRecordingListening() {
guard !isRecordingListening else { return }
isRecordingListening = true
NotificationCenter.default.addObserver(
self,
selector: #selector(screenCapturedDidChange),
name: UIScreen.capturedDidChangeNotification,
object: nil
)
// Check initial state
isScreenRecording = isScreenCaptured
updateSharedPreferencesState("")
}
private func stopRecordingListening() {
guard isRecordingListening else { return }
isRecordingListening = false
NotificationCenter.default.removeObserver(
self,
name: UIScreen.capturedDidChangeNotification,
object: nil
)
isScreenRecording = false
updateSharedPreferencesState("")
}
@objc private func screenCapturedDidChange() {
isScreenRecording = isScreenCaptured
let nowMs = Int64(Date().timeIntervalSince1970 * 1000)
updateSharedPreferencesState("", timestamp: nowMs)
}
@objc private func screenshotDetected() {
print("Screenshot detected")
let nowMs = Int64(Date().timeIntervalSince1970 * 1000)
updateSharedPreferencesState(IOSNoScreenshotPlugin.screenshotPathPlaceholder, timestamp: nowMs)
}
private func updateScreenshotState(isScreenshotBlocked: Bool) {
attachWindowIfNeeded()
if isScreenshotBlocked {
enablePreventScreenshot()
} else {
disablePreventScreenshot()
}
}
private func updateSharedPreferencesState(_ screenshotData: String, timestamp: Int64 = 0, sourceApp: String = "") {
let map: [String: Any] = [
"is_screenshot_on": IOSNoScreenshotPlugin.preventScreenShot,
"screenshot_path": screenshotData,
"was_screenshot_taken": !screenshotData.isEmpty,
"is_screen_recording": isScreenRecording,
"timestamp": timestamp,
"source_app": sourceApp
]
let jsonString = convertMapToJsonString(map)
if lastSharedPreferencesState != jsonString {
hasSharedPreferencesChanged = true
lastSharedPreferencesState = jsonString
}
}
private func convertMapToJsonString(_ map: [String: Any]) -> String {
if let jsonData = try? JSONSerialization.data(withJSONObject: map, options: .prettyPrinted) {
return String(data: jsonData, encoding: .utf8) ?? ""
}
return ""
}
public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
eventSink = events
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.screenshotStream()
}
return nil
}
public func onCancel(withArguments arguments: Any?) -> FlutterError? {
eventSink = nil
return nil
}
private func screenshotStream() {
if hasSharedPreferencesChanged {
eventSink?(lastSharedPreferencesState)
hasSharedPreferencesChanged = false
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.screenshotStream()
}
}
private func attachWindowIfNeeded() {
var activeWindow: UIWindow?
if let windowScene = UIApplication.shared.connectedScenes
.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene {
if #available(iOS 15.0, *) {
activeWindow = windowScene.keyWindow
} else {
activeWindow = windowScene.windows.first(where: { $0.isKeyWindow })
}
}
guard let window = activeWindow else {
print("No active window found.")
return
}
// Skip re-configuration if already attached to this window.
if window === attachedWindow {
return
}
// Clean up old state before re-attaching to a new window.
if isImageOverlayModeEnabled {
disableImageScreen()
}
if isBlurOverlayModeEnabled {
disableBlurScreen()
}
if isColorOverlayModeEnabled {
disableColorScreen()
}
disablePreventScreenshot()
// Undo previous layer reparenting: move the old window's layer
// back to the root layer and detach the text field's layer.
if let oldWindow = attachedWindow,
let rootLayer = screenPrevent.layer.superlayer {
rootLayer.addSublayer(oldWindow.layer)
screenPrevent.layer.removeFromSuperlayer()
}
// Use a fresh UITextField to avoid stale layer state.
screenPrevent = UITextField()
configurePreventionScreenshot(window: window)
self.attachedWindow = window
}
}
#if SWIFT_PACKAGE
// When building with Swift Package Manager, expose the plugin class name
// that matches pluginClass in pubspec.yaml for Flutter's registration.
public typealias NoScreenshotPlugin = IOSNoScreenshotPlugin
#endif

View file

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyTrackingDomains</key>
<array/>
<key>NSPrivacyAccessedAPITypes</key>
<array/>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
<key>NSPrivacyTracking</key>
<false/>
</dict>
</plist>

View file

@ -1,15 +0,0 @@
const screenShotOnConst = "screenshotOn";
const screenShotOffConst = "screenshotOff";
const screenSetImage = "toggleScreenshotWithImage";
const screenSetBlur = "toggleScreenshotWithBlur";
const screenSetColor = "toggleScreenshotWithColor";
const screenEnableImage = "screenshotWithImage";
const screenEnableBlur = "screenshotWithBlur";
const screenEnableColor = "screenshotWithColor";
const toggleScreenShotConst = "toggleScreenshot";
const startScreenshotListeningConst = 'startScreenshotListening';
const stopScreenshotListeningConst = 'stopScreenshotListening';
const startScreenRecordingListeningConst = 'startScreenRecordingListening';
const stopScreenRecordingListeningConst = 'stopScreenRecordingListening';
const screenshotMethodChannel = "com.flutterplaza.no_screenshot_methods";
const screenshotEventChannel = "com.flutterplaza.no_screenshot_streams";

View file

@ -1,181 +0,0 @@
import 'dart:async';
import 'package:no_screenshot/screenshot_snapshot.dart';
import 'no_screenshot_platform_interface.dart';
/// Callback type for screenshot and recording events.
typedef ScreenshotEventCallback = void Function(ScreenshotSnapshot snapshot);
/// A class that provides a platform-agnostic way to disable screenshots.
///
class NoScreenshot implements NoScreenshotPlatform {
NoScreenshotPlatform get _instancePlatform => NoScreenshotPlatform.instance;
NoScreenshot._();
@Deprecated(
"Using this may cause issue\nUse instance directly\ne.g: 'NoScreenshot.instance.screenshotOff()'")
NoScreenshot();
static final NoScreenshot instance = NoScreenshot._();
// Granular Callbacks (P15)
/// Called when a screenshot is detected.
ScreenshotEventCallback? onScreenshotDetected;
/// Called when screen recording starts.
ScreenshotEventCallback? onScreenRecordingStarted;
/// Called when screen recording stops.
ScreenshotEventCallback? onScreenRecordingStopped;
StreamSubscription<ScreenshotSnapshot>? _callbackSubscription;
bool _wasRecording = false;
/// Starts dispatching events to [onScreenshotDetected],
/// [onScreenRecordingStarted], and [onScreenRecordingStopped].
///
/// Listens to [screenshotStream] internally. Call [stopCallbacks] or
/// [removeAllCallbacks] to cancel.
void startCallbacks() {
if (_callbackSubscription != null) return;
_callbackSubscription = screenshotStream.listen(_dispatchCallbacks);
}
/// Stops dispatching events but keeps callback assignments.
void stopCallbacks() {
_callbackSubscription?.cancel();
_callbackSubscription = null;
}
/// Stops dispatching and clears all callback assignments.
void removeAllCallbacks() {
stopCallbacks();
onScreenshotDetected = null;
onScreenRecordingStarted = null;
onScreenRecordingStopped = null;
_wasRecording = false;
}
/// Whether callbacks are currently being dispatched.
bool get hasActiveCallbacks => _callbackSubscription != null;
void _dispatchCallbacks(ScreenshotSnapshot snapshot) {
if (snapshot.wasScreenshotTaken) {
onScreenshotDetected?.call(snapshot);
}
if (!_wasRecording && snapshot.isScreenRecording) {
onScreenRecordingStarted?.call(snapshot);
}
if (_wasRecording && !snapshot.isScreenRecording) {
onScreenRecordingStopped?.call(snapshot);
}
_wasRecording = snapshot.isScreenRecording;
}
// Platform delegation
/// Return `true` if screenshot capabilities has been
/// successfully disabled or is currently disabled and `false` otherwise.
/// throw `UnmimplementedError` if not implement
///
@override
Future<bool> screenshotOff() {
return _instancePlatform.screenshotOff();
}
/// Return `true` if screenshot capabilities has been
/// successfully enabled or is currently enabled and `false` otherwise.
/// throw `UnmimplementedError` if not implement
///
@override
Future<bool> screenshotOn() {
return _instancePlatform.screenshotOn();
}
@override
Future<bool> toggleScreenshotWithImage() {
return _instancePlatform.toggleScreenshotWithImage();
}
@override
Future<bool> toggleScreenshotWithBlur({double blurRadius = 30.0}) {
return _instancePlatform.toggleScreenshotWithBlur(blurRadius: blurRadius);
}
@override
Future<bool> toggleScreenshotWithColor({int color = 0xFF000000}) {
return _instancePlatform.toggleScreenshotWithColor(color: color);
}
/// Always enables image overlay mode (idempotent safe to call repeatedly).
@override
Future<bool> screenshotWithImage() {
return _instancePlatform.screenshotWithImage();
}
/// Always enables blur overlay mode (idempotent safe to call repeatedly).
@override
Future<bool> screenshotWithBlur({double blurRadius = 30.0}) {
return _instancePlatform.screenshotWithBlur(blurRadius: blurRadius);
}
/// Always enables color overlay mode (idempotent safe to call repeatedly).
@override
Future<bool> screenshotWithColor({int color = 0xFF000000}) {
return _instancePlatform.screenshotWithColor(color: color);
}
/// Return `true` if screenshot capabilities has been
/// successfully toggle from it previous state and `false` if the attempt
/// to toggle failed.
/// throw `UnmimplementedError` if not implement
///
@override
Future<bool> toggleScreenshot() {
return _instancePlatform.toggleScreenshot();
}
/// Stream to screenshot activities [ScreenshotSnapshot]
///
@override
Stream<ScreenshotSnapshot> get screenshotStream {
return _instancePlatform.screenshotStream;
}
/// Start listening to screenshot activities
@override
Future<void> startScreenshotListening() {
return _instancePlatform.startScreenshotListening();
}
/// Stop listening to screenshot activities
@override
Future<void> stopScreenshotListening() {
return _instancePlatform.stopScreenshotListening();
}
/// Start listening to screen recording activities
@override
Future<void> startScreenRecordingListening() {
return _instancePlatform.startScreenRecordingListening();
}
/// Stop listening to screen recording activities
@override
Future<void> stopScreenRecordingListening() {
return _instancePlatform.stopScreenRecordingListening();
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
other is NoScreenshot &&
runtimeType == other.runtimeType &&
_instancePlatform == other._instancePlatform;
}
@override
int get hashCode => _instancePlatform.hashCode;
}

View file

@ -1,105 +0,0 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:no_screenshot/constants.dart';
import 'package:no_screenshot/screenshot_snapshot.dart';
import 'no_screenshot_platform_interface.dart';
/// An implementation of [NoScreenshotPlatform] that uses method channels.
class MethodChannelNoScreenshot extends NoScreenshotPlatform {
/// The method channel used to interact with the native platform.
@visibleForTesting
final methodChannel = const MethodChannel(screenshotMethodChannel);
@visibleForTesting
final eventChannel = const EventChannel(screenshotEventChannel);
Stream<ScreenshotSnapshot>? _cachedStream;
@override
Stream<ScreenshotSnapshot> get screenshotStream {
_cachedStream ??= eventChannel.receiveBroadcastStream().map((event) =>
ScreenshotSnapshot.fromMap(jsonDecode(event) as Map<String, dynamic>));
return _cachedStream!;
}
@override
Future<bool> toggleScreenshot() async {
final result =
await methodChannel.invokeMethod<bool>(toggleScreenShotConst);
return result ?? false;
}
@override
Future<bool> screenshotOff() async {
final result = await methodChannel.invokeMethod<bool>(screenShotOffConst);
return result ?? false;
}
@override
Future<bool> screenshotOn() async {
final result = await methodChannel.invokeMethod<bool>(screenShotOnConst);
return result ?? false;
}
@override
Future<bool> toggleScreenshotWithImage() async {
final result = await methodChannel.invokeMethod<bool>(screenSetImage);
return result ?? false;
}
@override
Future<bool> toggleScreenshotWithBlur({double blurRadius = 30.0}) async {
final result = await methodChannel
.invokeMethod<bool>(screenSetBlur, {'radius': blurRadius});
return result ?? false;
}
@override
Future<bool> toggleScreenshotWithColor({int color = 0xFF000000}) async {
final result = await methodChannel
.invokeMethod<bool>(screenSetColor, {'color': color});
return result ?? false;
}
@override
Future<bool> screenshotWithImage() async {
final result = await methodChannel.invokeMethod<bool>(screenEnableImage);
return result ?? false;
}
@override
Future<bool> screenshotWithBlur({double blurRadius = 30.0}) async {
final result = await methodChannel
.invokeMethod<bool>(screenEnableBlur, {'radius': blurRadius});
return result ?? false;
}
@override
Future<bool> screenshotWithColor({int color = 0xFF000000}) async {
final result = await methodChannel
.invokeMethod<bool>(screenEnableColor, {'color': color});
return result ?? false;
}
@override
Future<void> startScreenshotListening() {
return methodChannel.invokeMethod<void>(startScreenshotListeningConst);
}
@override
Future<void> stopScreenshotListening() {
return methodChannel.invokeMethod<void>(stopScreenshotListeningConst);
}
@override
Future<void> startScreenRecordingListening() {
return methodChannel.invokeMethod<void>(startScreenRecordingListeningConst);
}
@override
Future<void> stopScreenRecordingListening() {
return methodChannel.invokeMethod<void>(stopScreenRecordingListeningConst);
}
}

View file

@ -1,113 +0,0 @@
import 'package:no_screenshot/screenshot_snapshot.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
import 'no_screenshot_method_channel.dart';
abstract class NoScreenshotPlatform extends PlatformInterface {
/// Constructs a NoScreenshotPlatform.
NoScreenshotPlatform() : super(token: _token);
static final Object _token = Object();
static NoScreenshotPlatform _instance = MethodChannelNoScreenshot();
/// The default instance of [NoScreenshotPlatform] to use.
///
/// Defaults to [MethodChannelNoScreenshot].
static NoScreenshotPlatform get instance => _instance;
/// Platform-specific implementations should set this with their own
/// platform-specific class that extends [NoScreenshotPlatform] when
/// they register themselves.
static set instance(NoScreenshotPlatform instance) {
PlatformInterface.verifyToken(instance, _token);
_instance = instance;
}
/// Return `true` if screenshot capabilities has been
/// successfully disabled or is currently disabled and `false` otherwise.
/// throw `UnmimplementedError` if not implement
Future<bool> screenshotOff() {
throw UnimplementedError('screenshotOff() has not been implemented.');
}
/// Return `true` if screenshot capabilities has been
/// successfully enabled or is currently enabled and `false` otherwise.
/// throw `UnmimplementedError` if not implement
Future<bool> screenshotOn() {
throw UnimplementedError('screenshotOn() has not been implemented.');
}
/// Return `true` if screenshot capabilities has been
/// successfully enabled or is currently enabled and `false` otherwise.
/// throw `UnmimplementedError` if not implement
Future<bool> toggleScreenshotWithImage() {
throw UnimplementedError(
'toggleScreenshotWithImage() has not been implemented.');
}
Future<bool> toggleScreenshotWithBlur({double blurRadius = 30.0}) {
throw UnimplementedError(
'toggleScreenshotWithBlur() has not been implemented.');
}
Future<bool> toggleScreenshotWithColor({int color = 0xFF000000}) {
throw UnimplementedError(
'toggleScreenshotWithColor() has not been implemented.');
}
/// Always enables image overlay mode (idempotent safe to call repeatedly).
Future<bool> screenshotWithImage() {
throw UnimplementedError('screenshotWithImage() has not been implemented.');
}
/// Always enables blur overlay mode (idempotent safe to call repeatedly).
Future<bool> screenshotWithBlur({double blurRadius = 30.0}) {
throw UnimplementedError('screenshotWithBlur() has not been implemented.');
}
/// Always enables color overlay mode (idempotent safe to call repeatedly).
Future<bool> screenshotWithColor({int color = 0xFF000000}) {
throw UnimplementedError('screenshotWithColor() has not been implemented.');
}
/// Return `true` if screenshot capabilities has been
/// successfully toggle from it previous state and `false` if the attempt
/// to toggle failed.
/// throw `UnmimplementedError` if not implement
Future<bool> toggleScreenshot() {
throw UnimplementedError('toggleScreenshot() has not been implemented.');
}
/// Stream to screenshot activities [ScreenshotSnapshot]
/// This stream will emit a [ScreenshotSnapshot] whenever a screenshot is taken.
/// The [ScreenshotSnapshot] contains the path to the screenshot file.
/// throw `UnmimplementedError` if not implement
Stream<ScreenshotSnapshot> get screenshotStream {
throw UnimplementedError('incrementStream has not been implemented.');
}
// Start listening to screenshot activities
Future<void> startScreenshotListening() {
throw UnimplementedError(
'startScreenshotListening has not been implemented.');
}
/// Stop listening to screenshot activities
Future<void> stopScreenshotListening() {
throw UnimplementedError(
'stopScreenshotListening has not been implemented.');
}
/// Start listening to screen recording activities
Future<void> startScreenRecordingListening() {
throw UnimplementedError(
'startScreenRecordingListening has not been implemented.');
}
/// Stop listening to screen recording activities
Future<void> stopScreenRecordingListening() {
throw UnimplementedError(
'stopScreenRecordingListening has not been implemented.');
}
}

View file

@ -1,208 +0,0 @@
import 'dart:async';
import 'dart:js_interop';
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
import 'package:no_screenshot/no_screenshot_platform_interface.dart';
import 'package:no_screenshot/screenshot_snapshot.dart';
import 'package:web/web.dart' as web;
/// Web implementation of [NoScreenshotPlatform].
///
/// Browsers cannot truly prevent OS-level screenshots. This provides
/// best-effort JS deterrents: right-click blocking, PrintScreen
/// interception, `user-select: none`, and `visibilitychange` detection.
class NoScreenshotWeb extends NoScreenshotPlatform {
NoScreenshotWeb._();
/// Creates an instance for testing without going through [registerWith].
factory NoScreenshotWeb.createForTest() => NoScreenshotWeb._();
static void registerWith(Registrar registrar) {
NoScreenshotPlatform.instance = NoScreenshotWeb._();
}
bool _isProtectionOn = false;
bool _isListening = false;
final StreamController<ScreenshotSnapshot> _controller =
StreamController<ScreenshotSnapshot>.broadcast();
// JS event listeners (stored for removal)
JSFunction? _contextMenuHandler;
JSFunction? _keyDownHandler;
JSFunction? _visibilityHandler;
// Stream
@override
Stream<ScreenshotSnapshot> get screenshotStream => _controller.stream;
// Protection
@override
Future<bool> screenshotOff() async {
_enableProtection();
return true;
}
@override
Future<bool> screenshotOn() async {
_disableProtection();
return true;
}
@override
Future<bool> toggleScreenshot() async {
_isProtectionOn ? _disableProtection() : _enableProtection();
return true;
}
@override
Future<bool> toggleScreenshotWithImage() async {
_isProtectionOn ? _disableProtection() : _enableProtection();
return _isProtectionOn;
}
@override
Future<bool> toggleScreenshotWithBlur({double blurRadius = 30.0}) async {
_isProtectionOn ? _disableProtection() : _enableProtection();
return _isProtectionOn;
}
@override
Future<bool> toggleScreenshotWithColor({int color = 0xFF000000}) async {
_isProtectionOn ? _disableProtection() : _enableProtection();
return _isProtectionOn;
}
@override
Future<bool> screenshotWithImage() async {
_enableProtection();
return true;
}
@override
Future<bool> screenshotWithBlur({double blurRadius = 30.0}) async {
_enableProtection();
return true;
}
@override
Future<bool> screenshotWithColor({int color = 0xFF000000}) async {
_enableProtection();
return true;
}
// Screenshot Listening
@override
Future<void> startScreenshotListening() async {
if (_isListening) return;
_isListening = true;
_addVisibilityListener();
}
@override
Future<void> stopScreenshotListening() async {
_isListening = false;
_removeVisibilityListener();
}
// Recording Listening (no-op on web)
@override
Future<void> startScreenRecordingListening() async {}
@override
Future<void> stopScreenRecordingListening() async {}
// Internal
void _enableProtection() {
if (_isProtectionOn) return;
_isProtectionOn = true;
_addContextMenuBlocker();
_addPrintScreenBlocker();
_setUserSelectNone(true);
_emitState();
}
void _disableProtection() {
if (!_isProtectionOn) return;
_isProtectionOn = false;
_removeContextMenuBlocker();
_removePrintScreenBlocker();
_setUserSelectNone(false);
_emitState();
}
void _emitState({bool wasScreenshotTaken = false}) {
_controller.add(ScreenshotSnapshot(
screenshotPath: '',
isScreenshotProtectionOn: _isProtectionOn,
wasScreenshotTaken: wasScreenshotTaken,
));
}
// Context menu blocker
void _addContextMenuBlocker() {
_contextMenuHandler = ((web.Event e) {
e.preventDefault();
}).toJS;
web.document.addEventListener('contextmenu', _contextMenuHandler!);
}
void _removeContextMenuBlocker() {
if (_contextMenuHandler != null) {
web.document.removeEventListener('contextmenu', _contextMenuHandler!);
_contextMenuHandler = null;
}
}
// PrintScreen blocker
void _addPrintScreenBlocker() {
_keyDownHandler = ((web.KeyboardEvent e) {
if (e.key == 'PrintScreen') {
e.preventDefault();
}
}).toJS;
web.document.addEventListener('keydown', _keyDownHandler!);
}
void _removePrintScreenBlocker() {
if (_keyDownHandler != null) {
web.document.removeEventListener('keydown', _keyDownHandler!);
_keyDownHandler = null;
}
}
// user-select CSS
void _setUserSelectNone(bool disable) {
final style = web.document.body?.style;
if (style == null) return;
style.setProperty('user-select', disable ? 'none' : '');
style.setProperty('-webkit-user-select', disable ? 'none' : '');
}
// Visibility listener
void _addVisibilityListener() {
_visibilityHandler = ((web.Event _) {
if (web.document.visibilityState == 'visible') {
_emitState(wasScreenshotTaken: true);
}
}).toJS;
web.document.addEventListener('visibilitychange', _visibilityHandler!);
}
void _removeVisibilityListener() {
if (_visibilityHandler != null) {
web.document.removeEventListener('visibilitychange', _visibilityHandler!);
_visibilityHandler = null;
}
}
}

View file

@ -1,31 +0,0 @@
import 'package:no_screenshot/no_screenshot.dart';
/// The protection mode to apply.
enum OverlayMode { none, secure, blur, color, image }
/// Applies the given [mode] using the idempotent NoScreenshot API.
///
/// - [none] re-enables screenshots (no protection).
/// - [secure] blocks screenshots and screen recording.
/// - [blur] shows a blur overlay in the app switcher.
/// - [color] shows a solid color overlay in the app switcher.
/// - [image] shows a custom image overlay in the app switcher.
Future<void> applyOverlayMode(
OverlayMode mode, {
double blurRadius = 30.0,
int color = 0xFF000000,
}) async {
final noScreenshot = NoScreenshot.instance;
switch (mode) {
case OverlayMode.none:
await noScreenshot.screenshotOn();
case OverlayMode.secure:
await noScreenshot.screenshotOff();
case OverlayMode.blur:
await noScreenshot.screenshotWithBlur(blurRadius: blurRadius);
case OverlayMode.color:
await noScreenshot.screenshotWithColor(color: color);
case OverlayMode.image:
await noScreenshot.screenshotWithImage();
}
}

View file

@ -1,83 +0,0 @@
class ScreenshotSnapshot {
/// File path of the captured screenshot.
///
/// Only available on **macOS** (via Spotlight / `NSMetadataQuery`) and
/// **Linux** (via GFileMonitor / inotify).
/// On Android and iOS the OS does not expose the screenshot file path
/// this field will contain a placeholder string.
/// Use [wasScreenshotTaken] to detect screenshot events on all platforms.
final String screenshotPath;
final bool isScreenshotProtectionOn;
final bool wasScreenshotTaken;
final bool isScreenRecording;
/// Milliseconds since epoch when the event was detected.
///
/// `0` means unknown (e.g. the native platform did not provide timing data).
final int timestamp;
/// Human-readable name of the application that triggered the event.
///
/// Empty string means unknown or not applicable.
final String sourceApp;
ScreenshotSnapshot({
required this.screenshotPath,
required this.isScreenshotProtectionOn,
required this.wasScreenshotTaken,
this.isScreenRecording = false,
this.timestamp = 0,
this.sourceApp = '',
});
factory ScreenshotSnapshot.fromMap(Map<String, dynamic> map) {
return ScreenshotSnapshot(
screenshotPath: map['screenshot_path'] as String? ?? '',
isScreenshotProtectionOn: map['is_screenshot_on'] as bool? ?? false,
wasScreenshotTaken: map['was_screenshot_taken'] as bool? ?? false,
isScreenRecording: map['is_screen_recording'] as bool? ?? false,
timestamp: map['timestamp'] as int? ?? 0,
sourceApp: map['source_app'] as String? ?? '',
);
}
Map<String, dynamic> toMap() {
return {
'screenshot_path': screenshotPath,
'is_screenshot_on': isScreenshotProtectionOn,
'was_screenshot_taken': wasScreenshotTaken,
'is_screen_recording': isScreenRecording,
'timestamp': timestamp,
'source_app': sourceApp,
};
}
@override
String toString() {
return 'ScreenshotSnapshot(\nscreenshotPath: $screenshotPath, \nisScreenshotProtectionOn: $isScreenshotProtectionOn, \nwasScreenshotTaken: $wasScreenshotTaken, \nisScreenRecording: $isScreenRecording, \ntimestamp: $timestamp, \nsourceApp: $sourceApp\n)';
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is ScreenshotSnapshot &&
other.screenshotPath == screenshotPath &&
other.isScreenshotProtectionOn == isScreenshotProtectionOn &&
other.wasScreenshotTaken == wasScreenshotTaken &&
other.isScreenRecording == isScreenRecording &&
other.timestamp == timestamp &&
other.sourceApp == sourceApp;
}
@override
int get hashCode {
return screenshotPath.hashCode ^
isScreenshotProtectionOn.hashCode ^
wasScreenshotTaken.hashCode ^
isScreenRecording.hashCode ^
timestamp.hashCode ^
sourceApp.hashCode;
}
}

View file

@ -1,79 +0,0 @@
import 'package:flutter/widgets.dart';
import 'package:no_screenshot/overlay_mode.dart';
/// Configuration for a single route's protection policy.
class SecureRouteConfig {
const SecureRouteConfig({
this.mode = OverlayMode.secure,
this.blurRadius = 30.0,
this.color = 0xFF000000,
});
final OverlayMode mode;
final double blurRadius;
final int color;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is SecureRouteConfig &&
runtimeType == other.runtimeType &&
mode == other.mode &&
blurRadius == other.blurRadius &&
color == other.color;
@override
int get hashCode => Object.hash(mode, blurRadius, color);
}
/// A [NavigatorObserver] that applies different protection levels per route.
///
/// ```dart
/// MaterialApp(
/// navigatorObservers: [
/// SecureNavigatorObserver(
/// policies: {
/// '/payment': SecureRouteConfig(mode: OverlayMode.secure),
/// '/profile': SecureRouteConfig(mode: OverlayMode.blur, blurRadius: 50),
/// '/home': SecureRouteConfig(mode: OverlayMode.none),
/// },
/// ),
/// ],
/// )
/// ```
class SecureNavigatorObserver extends NavigatorObserver {
SecureNavigatorObserver({
this.policies = const {},
this.defaultConfig = const SecureRouteConfig(mode: OverlayMode.none),
});
final Map<String, SecureRouteConfig> policies;
final SecureRouteConfig defaultConfig;
@override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
_applyPolicyForRoute(route);
}
@override
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
_applyPolicyForRoute(previousRoute);
}
@override
void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) {
_applyPolicyForRoute(newRoute);
}
@override
void didRemove(Route<dynamic> route, Route<dynamic>? previousRoute) {
_applyPolicyForRoute(previousRoute);
}
void _applyPolicyForRoute(Route<dynamic>? route) {
final name = route?.settings.name;
final config = (name != null ? policies[name] : null) ?? defaultConfig;
applyOverlayMode(config.mode,
blurRadius: config.blurRadius, color: config.color);
}
}

View file

@ -1,68 +0,0 @@
import 'package:flutter/widgets.dart';
import 'package:no_screenshot/no_screenshot.dart';
import 'package:no_screenshot/overlay_mode.dart';
/// A widget that automatically enables screenshot protection when mounted
/// and disables it when unmounted.
///
/// Wrap any subtree with [SecureWidget] to declaratively protect it:
///
/// ```dart
/// SecureWidget(
/// mode: OverlayMode.blur,
/// blurRadius: 50.0,
/// child: MySecurePage(),
/// )
/// ```
class SecureWidget extends StatefulWidget {
const SecureWidget({
super.key,
required this.child,
this.mode = OverlayMode.secure,
this.blurRadius = 30.0,
this.color = 0xFF000000,
});
final Widget child;
final OverlayMode mode;
final double blurRadius;
final int color;
@override
State<SecureWidget> createState() => _SecureWidgetState();
}
class _SecureWidgetState extends State<SecureWidget> {
@override
void initState() {
super.initState();
_applyMode();
}
@override
void didUpdateWidget(SecureWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.mode != widget.mode ||
oldWidget.blurRadius != widget.blurRadius ||
oldWidget.color != widget.color) {
_applyMode();
}
}
@override
void dispose() {
NoScreenshot.instance.screenshotOn();
super.dispose();
}
void _applyMode() {
applyOverlayMode(
widget.mode,
blurRadius: widget.blurRadius,
color: widget.color,
);
}
@override
Widget build(BuildContext context) => widget.child;
}

View file

@ -1,49 +0,0 @@
name: no_screenshot
description: Flutter plugin to prevent screenshots, detect screen recording, and show blur/color/image overlays in the app switcher on Android, iOS, macOS, Linux, Windows, and Web.
version: 0.10.0
homepage: https://flutterplaza.com
repository: https://github.com/FlutterPlaza/no_screenshot
topics:
- screenshot
- security
- privacy
- screen-capture
- app-switcher
environment:
sdk: '>=3.10.0 <4.0.0'
flutter: ">=3.38.0"
dependencies:
flutter:
sdk: flutter
flutter_web_plugins:
sdk: flutter
plugin_platform_interface: ^2.1.8
web: ^1.1.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ">=4.0.0 <6.0.0"
flutter_driver:
sdk: flutter
flutter:
plugin:
platforms:
android:
package: com.flutterplaza.no_screenshot
pluginClass: NoScreenshotPlugin
ios:
pluginClass: NoScreenshotPlugin
macos:
pluginClass: MacOSNoScreenshotPlugin
linux:
pluginClass: NoScreenshotPlugin
windows:
pluginClass: NoScreenshotPluginCApi
web:
pluginClass: NoScreenshotWeb
fileName: no_screenshot_web.dart

View file

@ -1,839 +0,0 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:no_screenshot/constants.dart';
import 'package:no_screenshot/no_screenshot.dart';
import 'package:no_screenshot/no_screenshot_method_channel.dart';
import 'package:no_screenshot/no_screenshot_platform_interface.dart';
import 'package:no_screenshot/screenshot_snapshot.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
late MethodChannelNoScreenshot platform;
setUp(() {
platform = MethodChannelNoScreenshot();
});
group('MethodChannelNoScreenshot', () {
const MethodChannel channel = MethodChannel(screenshotMethodChannel);
test('screenshotOn', () async {
const bool expected = true;
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
if (methodCall.method == screenShotOnConst) {
return expected;
}
return null;
});
final result = await platform.screenshotOn();
expect(result, expected);
});
test('screenshotOff', () async {
const bool expected = true;
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
if (methodCall.method == screenShotOffConst) {
return expected;
}
return null;
});
final result = await platform.screenshotOff();
expect(result, expected);
});
test('toggleScreenshot', () async {
const bool expected = true;
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
if (methodCall.method == toggleScreenShotConst) {
return expected;
}
return null;
});
final result = await platform.toggleScreenshot();
expect(result, expected);
});
test('startScreenshotListening', () async {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
if (methodCall.method == startScreenshotListeningConst) {
return null;
}
return null;
});
await platform.startScreenshotListening();
expect(true, true); // Add more specific expectations if needed
});
test('stopScreenshotListening', () async {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
if (methodCall.method == stopScreenshotListeningConst) {
return null;
}
return null;
});
await platform.stopScreenshotListening();
expect(true, true); // Add more specific expectations if needed
});
test('toggleScreenshotWithImage', () async {
const bool expected = true;
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
if (methodCall.method == screenSetImage) {
return expected;
}
return null;
});
final result = await platform.toggleScreenshotWithImage();
expect(result, expected);
});
test('toggleScreenshotWithBlur', () async {
const bool expected = true;
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
if (methodCall.method == screenSetBlur) {
expect(methodCall.arguments, {'radius': 30.0});
return expected;
}
return null;
});
final result = await platform.toggleScreenshotWithBlur();
expect(result, expected);
});
test('toggleScreenshotWithBlur with custom radius', () async {
const bool expected = true;
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
if (methodCall.method == screenSetBlur) {
expect(methodCall.arguments, {'radius': 50.0});
return expected;
}
return null;
});
final result = await platform.toggleScreenshotWithBlur(blurRadius: 50.0);
expect(result, expected);
});
test('toggleScreenshotWithBlur returns false when channel returns null',
() async {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
return null;
});
final result = await platform.toggleScreenshotWithBlur();
expect(result, false);
});
test('toggleScreenshotWithColor', () async {
const bool expected = true;
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
if (methodCall.method == screenSetColor) {
expect(methodCall.arguments, {'color': 0xFF000000});
return expected;
}
return null;
});
final result = await platform.toggleScreenshotWithColor();
expect(result, expected);
});
test('toggleScreenshotWithColor with custom color', () async {
const bool expected = true;
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
if (methodCall.method == screenSetColor) {
expect(methodCall.arguments, {'color': 0xFFFF0000});
return expected;
}
return null;
});
final result =
await platform.toggleScreenshotWithColor(color: 0xFFFF0000);
expect(result, expected);
});
test('toggleScreenshotWithColor returns false when channel returns null',
() async {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
return null;
});
final result = await platform.toggleScreenshotWithColor();
expect(result, false);
});
test('toggleScreenshotWithImage returns false when channel returns null',
() async {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
return null;
});
final result = await platform.toggleScreenshotWithImage();
expect(result, false);
});
test('screenshotOn returns false when channel returns null', () async {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
return null;
});
final result = await platform.screenshotOn();
expect(result, false);
});
test('screenshotOff returns false when channel returns null', () async {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
return null;
});
final result = await platform.screenshotOff();
expect(result, false);
});
test('toggleScreenshot returns false when channel returns null', () async {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
return null;
});
final result = await platform.toggleScreenshot();
expect(result, false);
});
test('screenshotWithImage', () async {
const bool expected = true;
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
if (methodCall.method == screenEnableImage) {
return expected;
}
return null;
});
final result = await platform.screenshotWithImage();
expect(result, expected);
});
test('screenshotWithImage returns false when channel returns null',
() async {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
return null;
});
final result = await platform.screenshotWithImage();
expect(result, false);
});
test('screenshotWithBlur', () async {
const bool expected = true;
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
if (methodCall.method == screenEnableBlur) {
expect(methodCall.arguments, {'radius': 30.0});
return expected;
}
return null;
});
final result = await platform.screenshotWithBlur();
expect(result, expected);
});
test('screenshotWithBlur with custom radius', () async {
const bool expected = true;
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
if (methodCall.method == screenEnableBlur) {
expect(methodCall.arguments, {'radius': 50.0});
return expected;
}
return null;
});
final result = await platform.screenshotWithBlur(blurRadius: 50.0);
expect(result, expected);
});
test('screenshotWithBlur returns false when channel returns null',
() async {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
return null;
});
final result = await platform.screenshotWithBlur();
expect(result, false);
});
test('screenshotWithColor', () async {
const bool expected = true;
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
if (methodCall.method == screenEnableColor) {
expect(methodCall.arguments, {'color': 0xFF000000});
return expected;
}
return null;
});
final result = await platform.screenshotWithColor();
expect(result, expected);
});
test('screenshotWithColor with custom color', () async {
const bool expected = true;
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
if (methodCall.method == screenEnableColor) {
expect(methodCall.arguments, {'color': 0xFFFF0000});
return expected;
}
return null;
});
final result = await platform.screenshotWithColor(color: 0xFFFF0000);
expect(result, expected);
});
test('screenshotWithColor returns false when channel returns null',
() async {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
return null;
});
final result = await platform.screenshotWithColor();
expect(result, false);
});
test('startScreenRecordingListening', () async {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
if (methodCall.method == startScreenRecordingListeningConst) {
return null;
}
return null;
});
await platform.startScreenRecordingListening();
expect(true, true);
});
test('stopScreenRecordingListening', () async {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
if (methodCall.method == stopScreenRecordingListeningConst) {
return null;
}
return null;
});
await platform.stopScreenRecordingListening();
expect(true, true);
});
test('screenshotStream returns a stream that emits ScreenshotSnapshot',
() async {
final snapshotMap = {
'screenshot_path': '/test/path',
'is_screenshot_on': true,
'was_screenshot_taken': true,
'is_screen_recording': false,
'timestamp': 0,
'source_app': '',
};
final encoded = jsonEncode(snapshotMap);
// Mock the event channel by handling the underlying method channel
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockStreamHandler(platform.eventChannel, MockStreamHandler.inline(
onListen: (arguments, events) {
events.success(encoded);
},
));
final stream = platform.screenshotStream;
final snapshot = await stream.first;
expect(snapshot.screenshotPath, '/test/path');
expect(snapshot.isScreenshotProtectionOn, true);
expect(snapshot.wasScreenshotTaken, true);
});
test('screenshotStream caches and returns the same stream instance', () {
final stream1 = platform.screenshotStream;
final stream2 = platform.screenshotStream;
expect(identical(stream1, stream2), true);
});
});
group('ScreenshotSnapshot', () {
test('fromMap', () {
final map = {
'screenshot_path': '/example/path',
'is_screenshot_on': true,
'was_screenshot_taken': true,
};
final snapshot = ScreenshotSnapshot.fromMap(map);
expect(snapshot.screenshotPath, '/example/path');
expect(snapshot.isScreenshotProtectionOn, true);
expect(snapshot.wasScreenshotTaken, true);
expect(snapshot.isScreenRecording, false);
});
test('fromMap with is_screen_recording', () {
final map = {
'screenshot_path': '/example/path',
'is_screenshot_on': true,
'was_screenshot_taken': false,
'is_screen_recording': true,
};
final snapshot = ScreenshotSnapshot.fromMap(map);
expect(snapshot.screenshotPath, '/example/path');
expect(snapshot.isScreenshotProtectionOn, true);
expect(snapshot.wasScreenshotTaken, false);
expect(snapshot.isScreenRecording, true);
});
test('fromMap without is_screen_recording defaults to false', () {
final map = {
'screenshot_path': '/example/path',
'is_screenshot_on': true,
'was_screenshot_taken': true,
};
final snapshot = ScreenshotSnapshot.fromMap(map);
expect(snapshot.isScreenRecording, false);
});
test('toMap', () {
final snapshot = ScreenshotSnapshot(
screenshotPath: '/example/path',
isScreenshotProtectionOn: true,
wasScreenshotTaken: true,
isScreenRecording: true,
);
final map = snapshot.toMap();
expect(map['screenshot_path'], '/example/path');
expect(map['is_screenshot_on'], true);
expect(map['was_screenshot_taken'], true);
expect(map['is_screen_recording'], true);
});
test('toMap with default isScreenRecording', () {
final snapshot = ScreenshotSnapshot(
screenshotPath: '/example/path',
isScreenshotProtectionOn: true,
wasScreenshotTaken: true,
);
final map = snapshot.toMap();
expect(map['is_screen_recording'], false);
});
test('equality operator', () {
final snapshot1 = ScreenshotSnapshot(
screenshotPath: '/example/path',
isScreenshotProtectionOn: true,
wasScreenshotTaken: true,
);
final snapshot2 = ScreenshotSnapshot(
screenshotPath: '/example/path',
isScreenshotProtectionOn: true,
wasScreenshotTaken: true,
);
final snapshot3 = ScreenshotSnapshot(
screenshotPath: '/different/path',
isScreenshotProtectionOn: false,
wasScreenshotTaken: false,
);
final snapshot4 = ScreenshotSnapshot(
screenshotPath: '/example/path',
isScreenshotProtectionOn: true,
wasScreenshotTaken: true,
isScreenRecording: true,
);
expect(snapshot1 == snapshot2, true);
expect(snapshot1 == snapshot3, false);
expect(snapshot1 == snapshot4, false);
});
test('hashCode', () {
final snapshot1 = ScreenshotSnapshot(
screenshotPath: '/example/path',
isScreenshotProtectionOn: true,
wasScreenshotTaken: true,
);
final snapshot2 = ScreenshotSnapshot(
screenshotPath: '/example/path',
isScreenshotProtectionOn: true,
wasScreenshotTaken: true,
);
final snapshot3 = ScreenshotSnapshot(
screenshotPath: '/different/path',
isScreenshotProtectionOn: false,
wasScreenshotTaken: false,
);
expect(snapshot1.hashCode, snapshot2.hashCode);
expect(snapshot1.hashCode, isNot(snapshot3.hashCode));
});
test('fromMap with empty map uses defaults', () {
final snapshot = ScreenshotSnapshot.fromMap({});
expect(snapshot.screenshotPath, '');
expect(snapshot.isScreenshotProtectionOn, false);
expect(snapshot.wasScreenshotTaken, false);
expect(snapshot.isScreenRecording, false);
expect(snapshot.timestamp, 0);
expect(snapshot.sourceApp, '');
});
test('fromMap with null values uses defaults', () {
final map = <String, dynamic>{
'screenshot_path': null,
'is_screenshot_on': null,
'was_screenshot_taken': null,
'is_screen_recording': null,
'timestamp': null,
'source_app': null,
};
final snapshot = ScreenshotSnapshot.fromMap(map);
expect(snapshot.screenshotPath, '');
expect(snapshot.isScreenshotProtectionOn, false);
expect(snapshot.wasScreenshotTaken, false);
expect(snapshot.isScreenRecording, false);
expect(snapshot.timestamp, 0);
expect(snapshot.sourceApp, '');
});
test('fromMap with metadata', () {
final map = {
'screenshot_path': '/example/path',
'is_screenshot_on': true,
'was_screenshot_taken': true,
'is_screen_recording': false,
'timestamp': 1700000000000,
'source_app': 'screencaptureui',
};
final snapshot = ScreenshotSnapshot.fromMap(map);
expect(snapshot.screenshotPath, '/example/path');
expect(snapshot.isScreenshotProtectionOn, true);
expect(snapshot.wasScreenshotTaken, true);
expect(snapshot.timestamp, 1700000000000);
expect(snapshot.sourceApp, 'screencaptureui');
});
test('fromMap without metadata defaults timestamp and sourceApp', () {
final map = {
'screenshot_path': '/example/path',
'is_screenshot_on': true,
'was_screenshot_taken': true,
};
final snapshot = ScreenshotSnapshot.fromMap(map);
expect(snapshot.timestamp, 0);
expect(snapshot.sourceApp, '');
});
test('toMap includes metadata', () {
final snapshot = ScreenshotSnapshot(
screenshotPath: '/example/path',
isScreenshotProtectionOn: true,
wasScreenshotTaken: true,
timestamp: 1700000000000,
sourceApp: 'GNOME Screenshot',
);
final map = snapshot.toMap();
expect(map['timestamp'], 1700000000000);
expect(map['source_app'], 'GNOME Screenshot');
});
test('equality with metadata', () {
final snapshot1 = ScreenshotSnapshot(
screenshotPath: '/example/path',
isScreenshotProtectionOn: true,
wasScreenshotTaken: true,
timestamp: 1700000000000,
sourceApp: 'screencaptureui',
);
final snapshot2 = ScreenshotSnapshot(
screenshotPath: '/example/path',
isScreenshotProtectionOn: true,
wasScreenshotTaken: true,
timestamp: 1700000000000,
sourceApp: 'screencaptureui',
);
final snapshot3 = ScreenshotSnapshot(
screenshotPath: '/example/path',
isScreenshotProtectionOn: true,
wasScreenshotTaken: true,
timestamp: 1700000000001,
sourceApp: 'screencaptureui',
);
final snapshot4 = ScreenshotSnapshot(
screenshotPath: '/example/path',
isScreenshotProtectionOn: true,
wasScreenshotTaken: true,
timestamp: 1700000000000,
sourceApp: 'different_app',
);
expect(snapshot1 == snapshot2, true);
expect(snapshot1 == snapshot3, false);
expect(snapshot1 == snapshot4, false);
});
test('toString', () {
final snapshot = ScreenshotSnapshot(
screenshotPath: '/example/path',
isScreenshotProtectionOn: true,
wasScreenshotTaken: true,
);
final string = snapshot.toString();
expect(string,
'ScreenshotSnapshot(\nscreenshotPath: /example/path, \nisScreenshotProtectionOn: true, \nwasScreenshotTaken: true, \nisScreenRecording: false, \ntimestamp: 0, \nsourceApp: \n)');
});
test('toString with isScreenRecording true', () {
final snapshot = ScreenshotSnapshot(
screenshotPath: '/example/path',
isScreenshotProtectionOn: true,
wasScreenshotTaken: true,
isScreenRecording: true,
);
final string = snapshot.toString();
expect(string,
'ScreenshotSnapshot(\nscreenshotPath: /example/path, \nisScreenshotProtectionOn: true, \nwasScreenshotTaken: true, \nisScreenRecording: true, \ntimestamp: 0, \nsourceApp: \n)');
});
test('toString with metadata', () {
final snapshot = ScreenshotSnapshot(
screenshotPath: '/example/path',
isScreenshotProtectionOn: true,
wasScreenshotTaken: true,
timestamp: 1700000000000,
sourceApp: 'screencaptureui',
);
final string = snapshot.toString();
expect(string, contains('timestamp: 1700000000000'));
expect(string, contains('sourceApp: screencaptureui'));
});
});
group('Granular Callbacks (P15)', () {
late StreamController<ScreenshotSnapshot> controller;
late _MockNoScreenshotPlatform mockPlatform;
late NoScreenshot noScreenshot;
setUp(() {
controller = StreamController<ScreenshotSnapshot>.broadcast();
mockPlatform = _MockNoScreenshotPlatform(controller.stream);
NoScreenshotPlatform.instance = mockPlatform;
// Create a fresh instance for each test to avoid shared state.
noScreenshot = NoScreenshot.instance;
noScreenshot.removeAllCallbacks();
});
tearDown(() {
noScreenshot.removeAllCallbacks();
controller.close();
});
test('onScreenshotDetected fires when wasScreenshotTaken is true',
() async {
final detected = <ScreenshotSnapshot>[];
noScreenshot.onScreenshotDetected = detected.add;
noScreenshot.startCallbacks();
controller.add(ScreenshotSnapshot(
screenshotPath: '/path',
isScreenshotProtectionOn: true,
wasScreenshotTaken: true,
));
await Future.delayed(Duration.zero);
expect(detected, hasLength(1));
expect(detected.first.wasScreenshotTaken, true);
});
test('onScreenshotDetected does NOT fire when wasScreenshotTaken is false',
() async {
final detected = <ScreenshotSnapshot>[];
noScreenshot.onScreenshotDetected = detected.add;
noScreenshot.startCallbacks();
controller.add(ScreenshotSnapshot(
screenshotPath: '',
isScreenshotProtectionOn: true,
wasScreenshotTaken: false,
));
await Future.delayed(Duration.zero);
expect(detected, isEmpty);
});
test('onScreenRecordingStarted fires on false→true transition', () async {
final started = <ScreenshotSnapshot>[];
noScreenshot.onScreenRecordingStarted = started.add;
noScreenshot.startCallbacks();
// Initial state: not recording recording starts
controller.add(ScreenshotSnapshot(
screenshotPath: '',
isScreenshotProtectionOn: true,
wasScreenshotTaken: false,
isScreenRecording: true,
));
await Future.delayed(Duration.zero);
expect(started, hasLength(1));
expect(started.first.isScreenRecording, true);
});
test('onScreenRecordingStopped fires on true→false transition', () async {
final stopped = <ScreenshotSnapshot>[];
noScreenshot.onScreenRecordingStopped = stopped.add;
noScreenshot.startCallbacks();
// First: recording starts
controller.add(ScreenshotSnapshot(
screenshotPath: '',
isScreenshotProtectionOn: true,
wasScreenshotTaken: false,
isScreenRecording: true,
));
await Future.delayed(Duration.zero);
// Then: recording stops
controller.add(ScreenshotSnapshot(
screenshotPath: '',
isScreenshotProtectionOn: true,
wasScreenshotTaken: false,
isScreenRecording: false,
));
await Future.delayed(Duration.zero);
expect(stopped, hasLength(1));
expect(stopped.first.isScreenRecording, false);
});
test('removeAllCallbacks clears all callbacks and stops subscription',
() async {
final detected = <ScreenshotSnapshot>[];
noScreenshot.onScreenshotDetected = detected.add;
noScreenshot.startCallbacks();
expect(noScreenshot.hasActiveCallbacks, true);
noScreenshot.removeAllCallbacks();
expect(noScreenshot.hasActiveCallbacks, false);
expect(noScreenshot.onScreenshotDetected, isNull);
expect(noScreenshot.onScreenRecordingStarted, isNull);
expect(noScreenshot.onScreenRecordingStopped, isNull);
// Events after removal should not fire
controller.add(ScreenshotSnapshot(
screenshotPath: '/path',
isScreenshotProtectionOn: true,
wasScreenshotTaken: true,
));
await Future.delayed(Duration.zero);
expect(detected, isEmpty);
});
test('hasActiveCallbacks reflects subscription state', () {
expect(noScreenshot.hasActiveCallbacks, false);
noScreenshot.onScreenshotDetected = (_) {};
noScreenshot.startCallbacks();
expect(noScreenshot.hasActiveCallbacks, true);
noScreenshot.stopCallbacks();
expect(noScreenshot.hasActiveCallbacks, false);
});
test('startCallbacks is idempotent', () {
noScreenshot.onScreenshotDetected = (_) {};
noScreenshot.startCallbacks();
noScreenshot.startCallbacks(); // second call should be no-op
expect(noScreenshot.hasActiveCallbacks, true);
});
});
}
class _MockNoScreenshotPlatform extends NoScreenshotPlatform {
final Stream<ScreenshotSnapshot> _stream;
_MockNoScreenshotPlatform(this._stream);
@override
Stream<ScreenshotSnapshot> get screenshotStream => _stream;
@override
Future<bool> screenshotOff() async => true;
@override
Future<bool> screenshotOn() async => true;
@override
Future<bool> toggleScreenshot() async => true;
@override
Future<bool> toggleScreenshotWithImage() async => true;
@override
Future<bool> toggleScreenshotWithBlur({double blurRadius = 30.0}) async =>
true;
@override
Future<bool> toggleScreenshotWithColor({int color = 0xFF000000}) async =>
true;
@override
Future<bool> screenshotWithImage() async => true;
@override
Future<bool> screenshotWithBlur({double blurRadius = 30.0}) async => true;
@override
Future<bool> screenshotWithColor({int color = 0xFF000000}) async => true;
@override
Future<void> startScreenshotListening() async {}
@override
Future<void> stopScreenshotListening() async {}
@override
Future<void> startScreenRecordingListening() async {}
@override
Future<void> stopScreenRecordingListening() async {}
}

View file

@ -1,259 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:no_screenshot/no_screenshot_method_channel.dart';
import 'package:no_screenshot/no_screenshot_platform_interface.dart';
import 'package:no_screenshot/screenshot_snapshot.dart';
/// A minimal subclass that does NOT override toggleScreenshotWithImage,
/// so we can verify the base class throws UnimplementedError.
class BaseNoScreenshotPlatform extends NoScreenshotPlatform {}
class MockNoScreenshotPlatform extends NoScreenshotPlatform {
@override
Future<bool> screenshotOff() async {
return true;
}
@override
Future<bool> screenshotOn() async {
return true;
}
@override
Future<bool> toggleScreenshot() async {
return true;
}
@override
Stream<ScreenshotSnapshot> get screenshotStream {
return const Stream.empty();
}
@override
Future<void> startScreenshotListening() async {
return;
}
@override
Future<bool> toggleScreenshotWithImage() async {
return true;
}
@override
Future<bool> toggleScreenshotWithBlur({double blurRadius = 30.0}) async {
return true;
}
@override
Future<bool> toggleScreenshotWithColor({int color = 0xFF000000}) async {
return true;
}
@override
Future<void> stopScreenshotListening() async {
return;
}
@override
Future<bool> screenshotWithImage() async {
return true;
}
@override
Future<bool> screenshotWithBlur({double blurRadius = 30.0}) async {
return true;
}
@override
Future<bool> screenshotWithColor({int color = 0xFF000000}) async {
return true;
}
@override
Future<void> startScreenRecordingListening() async {
return;
}
@override
Future<void> stopScreenRecordingListening() async {
return;
}
}
void main() {
final platform = MockNoScreenshotPlatform();
group('NoScreenshotPlatform', () {
test('default instance should be MethodChannelNoScreenshot', () {
expect(NoScreenshotPlatform.instance,
isInstanceOf<MethodChannelNoScreenshot>());
});
test('screenshotOff should return true when called', () async {
expect(await platform.screenshotOff(), isTrue);
});
test('screenshotOn should return true when called', () async {
expect(await platform.screenshotOn(), isTrue);
});
test('toggleScreenshot should return true when called', () async {
expect(await platform.toggleScreenshot(), isTrue);
});
test('screenshotStream should not throw UnimplementedError when accessed',
() {
expect(() => platform.screenshotStream, isNot(throwsUnimplementedError));
});
test(
'startScreenshotListening should not throw UnimplementedError when called',
() async {
expect(platform.startScreenshotListening(), completes);
});
test(
'stopScreenshotListening should not throw UnimplementedError when called',
() async {
expect(platform.stopScreenshotListening(), completes);
});
test('toggleScreenshotWithImage should return true when called', () async {
expect(await platform.toggleScreenshotWithImage(), isTrue);
});
test(
'base NoScreenshotPlatform.toggleScreenshotWithImage() throws UnimplementedError',
() {
final basePlatform = BaseNoScreenshotPlatform();
expect(() => basePlatform.toggleScreenshotWithImage(),
throwsUnimplementedError);
});
test('toggleScreenshotWithBlur should return true when called', () async {
expect(await platform.toggleScreenshotWithBlur(), isTrue);
});
test(
'base NoScreenshotPlatform.toggleScreenshotWithBlur() throws UnimplementedError',
() {
final basePlatform = BaseNoScreenshotPlatform();
expect(() => basePlatform.toggleScreenshotWithBlur(),
throwsUnimplementedError);
});
test('toggleScreenshotWithColor should return true when called', () async {
expect(await platform.toggleScreenshotWithColor(), isTrue);
});
test(
'base NoScreenshotPlatform.toggleScreenshotWithColor() throws UnimplementedError',
() {
final basePlatform = BaseNoScreenshotPlatform();
expect(() => basePlatform.toggleScreenshotWithColor(),
throwsUnimplementedError);
});
test('screenshotWithImage should return true when called', () async {
expect(await platform.screenshotWithImage(), isTrue);
});
test(
'base NoScreenshotPlatform.screenshotWithImage() throws UnimplementedError',
() {
final basePlatform = BaseNoScreenshotPlatform();
expect(
() => basePlatform.screenshotWithImage(), throwsUnimplementedError);
});
test('screenshotWithBlur should return true when called', () async {
expect(await platform.screenshotWithBlur(), isTrue);
});
test(
'base NoScreenshotPlatform.screenshotWithBlur() throws UnimplementedError',
() {
final basePlatform = BaseNoScreenshotPlatform();
expect(() => basePlatform.screenshotWithBlur(), throwsUnimplementedError);
});
test('screenshotWithColor should return true when called', () async {
expect(await platform.screenshotWithColor(), isTrue);
});
test(
'base NoScreenshotPlatform.screenshotWithColor() throws UnimplementedError',
() {
final basePlatform = BaseNoScreenshotPlatform();
expect(
() => basePlatform.screenshotWithColor(), throwsUnimplementedError);
});
test('base NoScreenshotPlatform.screenshotOff() throws UnimplementedError',
() {
final basePlatform = BaseNoScreenshotPlatform();
expect(() => basePlatform.screenshotOff(), throwsUnimplementedError);
});
test('base NoScreenshotPlatform.screenshotOn() throws UnimplementedError',
() {
final basePlatform = BaseNoScreenshotPlatform();
expect(() => basePlatform.screenshotOn(), throwsUnimplementedError);
});
test(
'base NoScreenshotPlatform.toggleScreenshot() throws UnimplementedError',
() {
final basePlatform = BaseNoScreenshotPlatform();
expect(() => basePlatform.toggleScreenshot(), throwsUnimplementedError);
});
test('base NoScreenshotPlatform.screenshotStream throws UnimplementedError',
() {
final basePlatform = BaseNoScreenshotPlatform();
expect(() => basePlatform.screenshotStream, throwsUnimplementedError);
});
test(
'base NoScreenshotPlatform.startScreenshotListening() throws UnimplementedError',
() {
final basePlatform = BaseNoScreenshotPlatform();
expect(() => basePlatform.startScreenshotListening(),
throwsUnimplementedError);
});
test(
'base NoScreenshotPlatform.stopScreenshotListening() throws UnimplementedError',
() {
final basePlatform = BaseNoScreenshotPlatform();
expect(() => basePlatform.stopScreenshotListening(),
throwsUnimplementedError);
});
test(
'startScreenRecordingListening should not throw UnimplementedError when called',
() async {
expect(platform.startScreenRecordingListening(), completes);
});
test(
'stopScreenRecordingListening should not throw UnimplementedError when called',
() async {
expect(platform.stopScreenRecordingListening(), completes);
});
test(
'base NoScreenshotPlatform.startScreenRecordingListening() throws UnimplementedError',
() {
final basePlatform = BaseNoScreenshotPlatform();
expect(() => basePlatform.startScreenRecordingListening(),
throwsUnimplementedError);
});
test(
'base NoScreenshotPlatform.stopScreenRecordingListening() throws UnimplementedError',
() {
final basePlatform = BaseNoScreenshotPlatform();
expect(() => basePlatform.stopScreenRecordingListening(),
throwsUnimplementedError);
});
});
}

View file

@ -1,195 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:no_screenshot/no_screenshot_platform_interface.dart';
import 'package:no_screenshot/no_screenshot_method_channel.dart';
import 'package:no_screenshot/screenshot_snapshot.dart';
import 'package:no_screenshot/no_screenshot.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
class MockNoScreenshotPlatform
with MockPlatformInterfaceMixin
implements NoScreenshotPlatform {
@override
Future<bool> screenshotOff() async {
// Mock implementation or return a fixed value
return Future.value(true);
}
@override
Future<bool> screenshotOn() async {
// Mock implementation or return a fixed value
return Future.value(true);
}
@override
Future<bool> toggleScreenshotWithImage() async {
return Future.value(true);
}
@override
Future<bool> toggleScreenshotWithBlur({double blurRadius = 30.0}) async {
return Future.value(true);
}
@override
Future<bool> toggleScreenshotWithColor({int color = 0xFF000000}) async {
return Future.value(true);
}
@override
Future<bool> toggleScreenshot() async {
// Mock implementation or return a fixed value
return Future.value(true);
}
@override
Stream<ScreenshotSnapshot> get screenshotStream => const Stream.empty();
@override
Future<bool> screenshotWithImage() async {
return Future.value(true);
}
@override
Future<bool> screenshotWithBlur({double blurRadius = 30.0}) async {
return Future.value(true);
}
@override
Future<bool> screenshotWithColor({int color = 0xFF000000}) async {
return Future.value(true);
}
@override
Future<void> startScreenshotListening() {
return Future.value();
}
@override
Future<void> stopScreenshotListening() {
return Future.value();
}
@override
Future<void> startScreenRecordingListening() {
return Future.value();
}
@override
Future<void> stopScreenRecordingListening() {
return Future.value();
}
}
void main() {
final NoScreenshotPlatform initialPlatform = NoScreenshotPlatform.instance;
MockNoScreenshotPlatform fakePlatform = MockNoScreenshotPlatform();
setUp(() {
NoScreenshotPlatform.instance = fakePlatform;
});
tearDown(() {
NoScreenshotPlatform.instance = initialPlatform;
});
test('$MethodChannelNoScreenshot is the default instance', () {
expect(initialPlatform, isInstanceOf<MethodChannelNoScreenshot>());
});
test('NoScreenshot instance is a singleton', () {
final instance1 = NoScreenshot.instance;
final instance2 = NoScreenshot.instance;
expect(instance1, equals(instance2));
});
test('screenshotOn', () async {
expect(await NoScreenshot.instance.screenshotOn(), true);
});
test('screenshotOff', () async {
expect(await NoScreenshot.instance.screenshotOff(), true);
});
test('toggleScreenshot', () async {
expect(await NoScreenshot.instance.toggleScreenshot(), true);
});
test('screenshotStream', () async {
expect(NoScreenshot.instance.screenshotStream,
isInstanceOf<Stream<ScreenshotSnapshot>>());
});
test('startScreenshotListening', () async {
expect(NoScreenshot.instance.startScreenshotListening(), completes);
});
test('stopScreenshotListening', () async {
expect(NoScreenshot.instance.stopScreenshotListening(), completes);
});
test('toggleScreenshotWithImage', () async {
expect(await NoScreenshot.instance.toggleScreenshotWithImage(), true);
});
test('toggleScreenshotWithBlur', () async {
expect(await NoScreenshot.instance.toggleScreenshotWithBlur(), true);
});
test('toggleScreenshotWithBlur with custom radius', () async {
expect(
await NoScreenshot.instance.toggleScreenshotWithBlur(blurRadius: 50.0),
true);
});
test('toggleScreenshotWithColor', () async {
expect(await NoScreenshot.instance.toggleScreenshotWithColor(), true);
});
test('toggleScreenshotWithColor with custom color', () async {
expect(
await NoScreenshot.instance
.toggleScreenshotWithColor(color: 0xFFFF0000),
true);
});
test('screenshotWithImage', () async {
expect(await NoScreenshot.instance.screenshotWithImage(), true);
});
test('screenshotWithBlur', () async {
expect(await NoScreenshot.instance.screenshotWithBlur(), true);
});
test('screenshotWithBlur with custom radius', () async {
expect(
await NoScreenshot.instance.screenshotWithBlur(blurRadius: 50.0), true);
});
test('screenshotWithColor', () async {
expect(await NoScreenshot.instance.screenshotWithColor(), true);
});
test('screenshotWithColor with custom color', () async {
expect(await NoScreenshot.instance.screenshotWithColor(color: 0xFFFF0000),
true);
});
test('NoScreenshot equality operator', () {
final instance1 = NoScreenshot.instance;
final instance2 = NoScreenshot.instance;
expect(instance1 == instance2, true, reason: 'Instances should be equal');
});
test('NoScreenshot hashCode consistency', () {
final instance1 = NoScreenshot.instance;
final instance2 = NoScreenshot.instance;
expect(instance1.hashCode, instance2.hashCode);
});
test('deprecated constructor still works', () {
// ignore: deprecated_member_use
final instance = NoScreenshot();
expect(instance, isA<NoScreenshot>());
});
}

View file

@ -1,131 +0,0 @@
@TestOn('browser')
library;
import 'package:flutter_test/flutter_test.dart';
import 'package:no_screenshot/no_screenshot_web.dart';
import 'package:no_screenshot/screenshot_snapshot.dart';
void main() {
late NoScreenshotWeb platform;
setUp(() {
platform = NoScreenshotWeb.createForTest();
});
group('NoScreenshotWeb', () {
test('screenshotOff returns true and emits protection on', () async {
final events = <ScreenshotSnapshot>[];
platform.screenshotStream.listen(events.add);
final result = await platform.screenshotOff();
expect(result, true);
await Future.delayed(Duration.zero);
expect(events, isNotEmpty);
expect(events.last.isScreenshotProtectionOn, true);
});
test('screenshotOn returns true and emits protection off', () async {
await platform.screenshotOff(); // enable first
final events = <ScreenshotSnapshot>[];
platform.screenshotStream.listen(events.add);
final result = await platform.screenshotOn();
expect(result, true);
await Future.delayed(Duration.zero);
expect(events, isNotEmpty);
expect(events.last.isScreenshotProtectionOn, false);
});
test('toggleScreenshot returns true', () async {
final result = await platform.toggleScreenshot();
expect(result, true);
});
test('toggleScreenshotWithImage returns toggle state', () async {
// First toggle on
var result = await platform.toggleScreenshotWithImage();
expect(result, true);
// Second toggle off
result = await platform.toggleScreenshotWithImage();
expect(result, false);
});
test('toggleScreenshotWithBlur returns toggle state', () async {
var result = await platform.toggleScreenshotWithBlur();
expect(result, true);
result = await platform.toggleScreenshotWithBlur();
expect(result, false);
});
test('toggleScreenshotWithColor returns toggle state', () async {
var result = await platform.toggleScreenshotWithColor();
expect(result, true);
result = await platform.toggleScreenshotWithColor();
expect(result, false);
});
test('screenshotWithImage returns true', () async {
final result = await platform.screenshotWithImage();
expect(result, true);
});
test('screenshotWithBlur returns true', () async {
final result = await platform.screenshotWithBlur();
expect(result, true);
});
test('screenshotWithColor returns true', () async {
final result = await platform.screenshotWithColor();
expect(result, true);
});
test('startScreenshotListening completes without error', () async {
await expectLater(platform.startScreenshotListening(), completes);
});
test('stopScreenshotListening completes without error', () async {
await platform.startScreenshotListening();
await expectLater(platform.stopScreenshotListening(), completes);
});
test('startScreenRecordingListening completes (no-op)', () async {
await expectLater(platform.startScreenRecordingListening(), completes);
});
test('stopScreenRecordingListening completes (no-op)', () async {
await expectLater(platform.stopScreenRecordingListening(), completes);
});
test('screenshotStream emits on state changes', () async {
final events = <ScreenshotSnapshot>[];
platform.screenshotStream.listen(events.add);
await platform.screenshotOff();
await Future.delayed(Duration.zero);
await platform.screenshotOn();
await Future.delayed(Duration.zero);
expect(events.length, 2);
expect(events[0].isScreenshotProtectionOn, true);
expect(events[1].isScreenshotProtectionOn, false);
});
test('enable is idempotent — does not double-emit', () async {
final events = <ScreenshotSnapshot>[];
platform.screenshotStream.listen(events.add);
await platform.screenshotOff();
await platform.screenshotOff(); // second call should be no-op
await Future.delayed(Duration.zero);
expect(events.length, 1);
});
});
}

View file

@ -1,238 +0,0 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:no_screenshot/no_screenshot_platform_interface.dart';
import 'package:no_screenshot/overlay_mode.dart';
import 'package:no_screenshot/screenshot_snapshot.dart';
import 'package:no_screenshot/secure_navigator_observer.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
class _RecordingPlatform extends NoScreenshotPlatform
with MockPlatformInterfaceMixin {
final List<String> calls = [];
@override
Future<bool> screenshotOff() async {
calls.add('screenshotOff');
return true;
}
@override
Future<bool> screenshotOn() async {
calls.add('screenshotOn');
return true;
}
@override
Future<bool> screenshotWithImage() async {
calls.add('screenshotWithImage');
return true;
}
@override
Future<bool> screenshotWithBlur({double blurRadius = 30.0}) async {
calls.add('screenshotWithBlur($blurRadius)');
return true;
}
@override
Future<bool> screenshotWithColor({int color = 0xFF000000}) async {
calls.add('screenshotWithColor($color)');
return true;
}
@override
Future<bool> toggleScreenshot() async => true;
@override
Future<bool> toggleScreenshotWithImage() async => true;
@override
Future<bool> toggleScreenshotWithBlur({double blurRadius = 30.0}) async =>
true;
@override
Future<bool> toggleScreenshotWithColor({int color = 0xFF000000}) async =>
true;
@override
Stream<ScreenshotSnapshot> get screenshotStream => const Stream.empty();
@override
Future<void> startScreenshotListening() async {}
@override
Future<void> stopScreenshotListening() async {}
@override
Future<void> startScreenRecordingListening() async {}
@override
Future<void> stopScreenRecordingListening() async {}
}
// Helper to create a fake route with a given name
Route<dynamic> _fakeRoute(String? name) {
return PageRouteBuilder<void>(
settings: RouteSettings(name: name),
pageBuilder: (_, __, ___) => const SizedBox(),
);
}
void main() {
late _RecordingPlatform fakePlatform;
late NoScreenshotPlatform originalPlatform;
setUp(() {
originalPlatform = NoScreenshotPlatform.instance;
fakePlatform = _RecordingPlatform();
NoScreenshotPlatform.instance = fakePlatform;
});
tearDown(() {
NoScreenshotPlatform.instance = originalPlatform;
});
group('SecureRouteConfig', () {
test('equality', () {
const a = SecureRouteConfig(mode: OverlayMode.blur, blurRadius: 50.0);
const b = SecureRouteConfig(mode: OverlayMode.blur, blurRadius: 50.0);
const c = SecureRouteConfig(mode: OverlayMode.secure);
expect(a, equals(b));
expect(a, isNot(equals(c)));
});
test('hashCode', () {
const a = SecureRouteConfig(mode: OverlayMode.blur, blurRadius: 50.0);
const b = SecureRouteConfig(mode: OverlayMode.blur, blurRadius: 50.0);
expect(a.hashCode, equals(b.hashCode));
});
test('default values', () {
const config = SecureRouteConfig();
expect(config.mode, OverlayMode.secure);
expect(config.blurRadius, 30.0);
expect(config.color, 0xFF000000);
});
});
group('SecureNavigatorObserver', () {
test('didPush applies policy for pushed route', () async {
final observer = SecureNavigatorObserver(
policies: {
'/payment': const SecureRouteConfig(mode: OverlayMode.secure),
},
);
observer.didPush(_fakeRoute('/payment'), null);
await Future<void>.delayed(Duration.zero);
expect(fakePlatform.calls, contains('screenshotOff'));
});
test('didPop applies policy for previous route', () async {
final observer = SecureNavigatorObserver(
policies: {
'/home': const SecureRouteConfig(mode: OverlayMode.none),
},
);
observer.didPop(_fakeRoute('/payment'), _fakeRoute('/home'));
await Future<void>.delayed(Duration.zero);
expect(fakePlatform.calls, contains('screenshotOn'));
});
test('didReplace applies policy for new route', () async {
final observer = SecureNavigatorObserver(
policies: {
'/profile':
const SecureRouteConfig(mode: OverlayMode.blur, blurRadius: 50.0),
},
);
observer.didReplace(
newRoute: _fakeRoute('/profile'), oldRoute: _fakeRoute('/home'));
await Future<void>.delayed(Duration.zero);
expect(fakePlatform.calls, contains('screenshotWithBlur(50.0)'));
});
test('didRemove applies policy for previous route', () async {
final observer = SecureNavigatorObserver(
policies: {
'/home': const SecureRouteConfig(mode: OverlayMode.none),
},
);
observer.didRemove(_fakeRoute('/payment'), _fakeRoute('/home'));
await Future<void>.delayed(Duration.zero);
expect(fakePlatform.calls, contains('screenshotOn'));
});
test('unmapped routes use defaultConfig', () async {
final observer = SecureNavigatorObserver(
policies: {
'/payment': const SecureRouteConfig(mode: OverlayMode.secure),
},
defaultConfig: const SecureRouteConfig(mode: OverlayMode.none),
);
observer.didPush(_fakeRoute('/unknown'), null);
await Future<void>.delayed(Duration.zero);
expect(fakePlatform.calls, contains('screenshotOn'));
});
test('custom defaultConfig works', () async {
final observer = SecureNavigatorObserver(
defaultConfig: const SecureRouteConfig(mode: OverlayMode.blur),
);
observer.didPush(_fakeRoute('/anything'), null);
await Future<void>.delayed(Duration.zero);
expect(fakePlatform.calls, contains('screenshotWithBlur(30.0)'));
});
test('null route name uses defaultConfig', () async {
final observer = SecureNavigatorObserver(
policies: {
'/payment': const SecureRouteConfig(mode: OverlayMode.secure),
},
defaultConfig: const SecureRouteConfig(mode: OverlayMode.none),
);
observer.didPush(_fakeRoute(null), null);
await Future<void>.delayed(Duration.zero);
expect(fakePlatform.calls, contains('screenshotOn'));
});
test('blur params passed correctly', () async {
final observer = SecureNavigatorObserver(
policies: {
'/settings': const SecureRouteConfig(
mode: OverlayMode.blur,
blurRadius: 75.0,
),
},
);
observer.didPush(_fakeRoute('/settings'), null);
await Future<void>.delayed(Duration.zero);
expect(fakePlatform.calls, contains('screenshotWithBlur(75.0)'));
});
test('color params passed correctly', () async {
final observer = SecureNavigatorObserver(
policies: {
'/branded': const SecureRouteConfig(
mode: OverlayMode.color,
color: 0xFF2196F3,
),
},
);
observer.didPush(_fakeRoute('/branded'), null);
await Future<void>.delayed(Duration.zero);
expect(
fakePlatform.calls, contains('screenshotWithColor(${0xFF2196F3})'));
});
});
}

View file

@ -1,224 +0,0 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:no_screenshot/no_screenshot_platform_interface.dart';
import 'package:no_screenshot/overlay_mode.dart';
import 'package:no_screenshot/screenshot_snapshot.dart';
import 'package:no_screenshot/secure_widget.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
class _RecordingPlatform extends NoScreenshotPlatform
with MockPlatformInterfaceMixin {
final List<String> calls = [];
@override
Future<bool> screenshotOff() async {
calls.add('screenshotOff');
return true;
}
@override
Future<bool> screenshotOn() async {
calls.add('screenshotOn');
return true;
}
@override
Future<bool> screenshotWithImage() async {
calls.add('screenshotWithImage');
return true;
}
@override
Future<bool> screenshotWithBlur({double blurRadius = 30.0}) async {
calls.add('screenshotWithBlur($blurRadius)');
return true;
}
@override
Future<bool> screenshotWithColor({int color = 0xFF000000}) async {
calls.add('screenshotWithColor($color)');
return true;
}
@override
Future<bool> toggleScreenshot() async => true;
@override
Future<bool> toggleScreenshotWithImage() async => true;
@override
Future<bool> toggleScreenshotWithBlur({double blurRadius = 30.0}) async =>
true;
@override
Future<bool> toggleScreenshotWithColor({int color = 0xFF000000}) async =>
true;
@override
Stream<ScreenshotSnapshot> get screenshotStream => const Stream.empty();
@override
Future<void> startScreenshotListening() async {}
@override
Future<void> stopScreenshotListening() async {}
@override
Future<void> startScreenRecordingListening() async {}
@override
Future<void> stopScreenRecordingListening() async {}
}
void main() {
late _RecordingPlatform fakePlatform;
late NoScreenshotPlatform originalPlatform;
setUp(() {
originalPlatform = NoScreenshotPlatform.instance;
fakePlatform = _RecordingPlatform();
NoScreenshotPlatform.instance = fakePlatform;
});
tearDown(() {
NoScreenshotPlatform.instance = originalPlatform;
});
testWidgets('default mode is OverlayMode.secure', (tester) async {
await tester.pumpWidget(
const SecureWidget(child: SizedBox()),
);
await tester.pump();
expect(fakePlatform.calls, contains('screenshotOff'));
});
testWidgets('initState calls screenshotOff for OverlayMode.secure',
(tester) async {
await tester.pumpWidget(
const SecureWidget(mode: OverlayMode.secure, child: SizedBox()),
);
await tester.pump();
expect(fakePlatform.calls, contains('screenshotOff'));
});
testWidgets('initState calls screenshotWithBlur for OverlayMode.blur',
(tester) async {
await tester.pumpWidget(
const SecureWidget(mode: OverlayMode.blur, child: SizedBox()),
);
await tester.pump();
expect(fakePlatform.calls, contains('screenshotWithBlur(30.0)'));
});
testWidgets('initState calls screenshotWithColor for OverlayMode.color',
(tester) async {
await tester.pumpWidget(
const SecureWidget(mode: OverlayMode.color, child: SizedBox()),
);
await tester.pump();
expect(fakePlatform.calls, contains('screenshotWithColor(4278190080)'));
});
testWidgets('initState calls screenshotWithImage for OverlayMode.image',
(tester) async {
await tester.pumpWidget(
const SecureWidget(mode: OverlayMode.image, child: SizedBox()),
);
await tester.pump();
expect(fakePlatform.calls, contains('screenshotWithImage'));
});
testWidgets('initState calls screenshotOn for OverlayMode.none',
(tester) async {
await tester.pumpWidget(
const SecureWidget(mode: OverlayMode.none, child: SizedBox()),
);
await tester.pump();
expect(fakePlatform.calls, contains('screenshotOn'));
});
testWidgets('dispose calls screenshotOn', (tester) async {
await tester.pumpWidget(
const SecureWidget(child: SizedBox()),
);
await tester.pump();
fakePlatform.calls.clear();
// Remove the widget to trigger dispose
await tester.pumpWidget(const SizedBox());
await tester.pump();
expect(fakePlatform.calls, contains('screenshotOn'));
});
testWidgets('didUpdateWidget re-applies when mode changes', (tester) async {
await tester.pumpWidget(
const SecureWidget(mode: OverlayMode.secure, child: SizedBox()),
);
await tester.pump();
fakePlatform.calls.clear();
await tester.pumpWidget(
const SecureWidget(mode: OverlayMode.blur, child: SizedBox()),
);
await tester.pump();
expect(fakePlatform.calls, contains('screenshotWithBlur(30.0)'));
});
testWidgets('didUpdateWidget re-applies when blurRadius changes',
(tester) async {
await tester.pumpWidget(
const SecureWidget(
mode: OverlayMode.blur, blurRadius: 30.0, child: SizedBox()),
);
await tester.pump();
fakePlatform.calls.clear();
await tester.pumpWidget(
const SecureWidget(
mode: OverlayMode.blur, blurRadius: 50.0, child: SizedBox()),
);
await tester.pump();
expect(fakePlatform.calls, contains('screenshotWithBlur(50.0)'));
});
testWidgets('didUpdateWidget re-applies when color changes', (tester) async {
await tester.pumpWidget(
const SecureWidget(mode: OverlayMode.color, child: SizedBox()),
);
await tester.pump();
fakePlatform.calls.clear();
await tester.pumpWidget(
const SecureWidget(
mode: OverlayMode.color, color: 0xFFFF0000, child: SizedBox()),
);
await tester.pump();
expect(fakePlatform.calls, contains('screenshotWithColor(4294901760)'));
});
testWidgets('didUpdateWidget does not re-apply when nothing changes',
(tester) async {
await tester.pumpWidget(
const SecureWidget(mode: OverlayMode.secure, child: SizedBox()),
);
await tester.pump();
fakePlatform.calls.clear();
// Rebuild with same params
await tester.pumpWidget(
const SecureWidget(mode: OverlayMode.secure, child: SizedBox()),
);
await tester.pump();
expect(fakePlatform.calls, isEmpty);
});
testWidgets('child is rendered correctly', (tester) async {
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: SecureWidget(child: Text('Hello')),
),
);
expect(find.text('Hello'), findsOneWidget);
});
}

View file

@ -23,8 +23,6 @@ dependency_overrides:
path: ./dependencies/lottie
mutex:
path: ./dependencies/mutex
no_screenshot:
path: ./dependencies/no_screenshot
optional:
path: ./dependencies/optional
photo_view:
@ -37,5 +35,7 @@ dependency_overrides:
path: ./dependencies/qr_flutter
restart_app:
path: ./dependencies/restart_app
screen_protector:
path: ./dependencies/screen_protector
x25519:
path: ./dependencies/x25519

201
screen_protector/LICENSE Normal file
View file

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2026 prongbang
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View file

@ -6,4 +6,3 @@
.DS_Store
/build
/captures
.cxx

View file

@ -0,0 +1,48 @@
buildscript {
ext.kotlin_version = '2.2.20'
}
plugins {
id 'com.android.library'
id 'org.jetbrains.kotlin.android'
}
rootProject.allprojects {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
group 'com.prongbang.screen_protector'
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
android {
namespace 'com.prongbang.screen_protector'
compileSdk 36
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = '17'
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
defaultConfig {
minSdkVersion 21
targetSdkVersion 36
}
lintOptions {
disable 'InvalidPackage'
}
}

Binary file not shown.

View file

@ -1,5 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View file

@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@ -80,13 +82,11 @@ do
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@ -133,22 +133,29 @@ location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@ -193,11 +200,15 @@ if "$cygwin" || "$msys" ; then
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
@ -205,6 +216,12 @@ set -- \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.

View file

@ -1,89 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View file

@ -0,0 +1,13 @@
pluginManagement {
repositories {
gradlePluginPortal()
google()
mavenCentral()
}
plugins {
id 'com.android.library' version "8.6.0"
id 'org.jetbrains.kotlin.android' version "2.1.0"
}
}
rootProject.name = 'screen_protector'

View file

@ -1,2 +1,3 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.prongbang.screen_protector">
</manifest>

View file

@ -0,0 +1,70 @@
package com.prongbang.screen_protector
import android.app.Activity
import android.view.WindowManager
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
/** ScreenProtectorPlugin */
class ScreenProtectorPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
private var activity: Activity? = null
private lateinit var channel: MethodChannel
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "screen_protector")
channel.setMethodCallHandler(this)
}
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) =
when (call.method) {
"protectDataLeakageOn", "preventScreenshotOn" -> {
try {
activity?.window?.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE,
)
result.success(true)
} catch (_: Exception) {
result.success(false)
}
}
"protectDataLeakageOff", "preventScreenshotOff" -> {
try {
activity?.window?.clearFlags(
WindowManager.LayoutParams.FLAG_SECURE,
)
result.success(true)
} catch (_: Exception) {
result.success(false)
}
}
"isRecording" -> {
// Not supported.
result.success(false)
}
else -> result.success(false)
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
}
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activity = binding.activity
}
override fun onDetachedFromActivityForConfigChanges() {
}
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
activity = binding.activity
}
override fun onDetachedFromActivity() {}
}

View file

@ -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)")
}
}

View file

@ -0,0 +1,4 @@
#import <Flutter/Flutter.h>
@interface ScreenProtectorPlugin : NSObject<FlutterPlugin>
@end

View file

@ -1,15 +1,15 @@
#import "NoScreenshotPlugin.h"
#if __has_include(<no_screenshot/no_screenshot-Swift.h>)
#import <no_screenshot/no_screenshot-Swift.h>
#import "ScreenProtectorPlugin.h"
#if __has_include(<screen_protector/screen_protector-Swift.h>)
#import <screen_protector/screen_protector-Swift.h>
#else
// Support project import fallback if the generated compatibility header
// is not copied when this plugin is created as a library.
// https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816
#import "no_screenshot-Swift.h"
#import "screen_protector-Swift.h"
#endif
@implementation NoScreenshotPlugin
@implementation ScreenProtectorPlugin
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
[IOSNoScreenshotPlugin registerWithRegistrar:registrar];
[SwiftScreenProtectorPlugin registerWithRegistrar:registrar];
}
@end

View file

@ -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 SwiftScreenProtectorPlugin: 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) {
SwiftScreenProtectorPlugin.channel = FlutterMethodChannel(name: "screen_protector", binaryMessenger: registrar.messenger())
let kit = ScreenProtectorKit(window: SwiftScreenProtectorPlugin.keyWindow())
kit.setRootViewResolver(FlutterRootViewResolver())
ScreenProtectorKit.initial(with: kit.window?.rootViewController?.view)
let instance = SwiftScreenProtectorPlugin(kit)
registrar.addMethodCallDelegate(instance, channel: SwiftScreenProtectorPlugin.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 SwiftScreenProtectorPlugin {
func handleFunc(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
let args = call.arguments as? Dictionary<String, String>
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 = SwiftScreenProtectorPlugin.channel] in
channel?.invokeMethod("onScreenshot", arguments: nil)
}
if #available(iOS 11.0, *) {
protectKit.screenRecordObserver { [weak channel = SwiftScreenProtectorPlugin.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
}
}

View file

@ -0,0 +1,22 @@
#
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
# Run `pod lib lint screen_protector.podspec --verbose --no-clean` to validate before publishing.
# Run `pod install --repo-update --verbose` to uppdate new version.
#
Pod::Spec.new do |s|
s.name = 'screen_protector'
s.version = '1.5.1'
s.summary = 'Safe Data Leakage via Application Background Screenshot and Prevent Screenshot for Android and iOS.'
s.description = <<-DESC
Safe Data Leakage via Application Background Screenshot and Prevent Screenshot for Android and iOS.
DESC
s.homepage = 'https://github.com/prongbang/screen_protector'
s.license = { :file => '../LICENSE' }
s.author = 'prongbang'
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.dependency 'Flutter'
s.dependency 'ScreenProtectorKit', '1.5.1'
s.platform = :ios, '12.0'
s.swift_version = ["4.0", "4.1", "4.2", "5.0", "5.1", "5.2", "5.3", "5.4", "5.5", "5.6", "5.7", "5.8", "5.9"]
end

View file

@ -0,0 +1,18 @@
import 'package:flutter/material.dart';
extension ColorExtension on Color {
/// final Color color = HexColor.fromHex('#ffffff');
static Color fromHex(String hexString) {
final buffer = StringBuffer();
if (hexString.length == 6 || hexString.length == 7) buffer.write('ff');
buffer.write(hexString.replaceFirst('#', ''));
return Color(int.parse(buffer.toString(), radix: 16));
}
/// final String hexColor = Color(0xffffffff).toHex();
String toHex({bool leadingHashSign = true}) => '${leadingHashSign ? '#' : ''}'
'${alpha.toRadixString(16).padLeft(2, '0')}'
'${red.toRadixString(16).padLeft(2, '0')}'
'${green.toRadixString(16).padLeft(2, '0')}'
'${blue.toRadixString(16).padLeft(2, '0')}';
}

View file

@ -0,0 +1,68 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
abstract class LegacyLifecycleState<T extends StatefulWidget> extends State<T>
with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
switch (state) {
case AppLifecycleState.resumed:
onResumed();
break;
case AppLifecycleState.inactive:
onPaused();
break;
case AppLifecycleState.paused:
onInactive();
break;
case AppLifecycleState.detached:
onDetached();
break;
case AppLifecycleState.hidden:
onHidden();
break;
}
}
void onResumed() {
if (kDebugMode) {
print("on resumed");
}
}
void onPaused() {
if (kDebugMode) {
print("on paused");
}
}
void onInactive() {
if (kDebugMode) {
print("on inactive");
}
}
void onDetached() {
if (kDebugMode) {
print("on detached");
}
}
void onHidden() {
if (kDebugMode) {
print("on hidden");
}
}
}

View file

@ -0,0 +1,135 @@
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
abstract class LifecycleState<T extends StatefulWidget> extends State<T>
with WidgetsBindingObserver {
AppLifecycleState? _lifecycleState;
@override
void initState() {
super.initState();
_lifecycleState = WidgetsBinding.instance.lifecycleState;
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
Future<AppExitResponse> didRequestAppExit() async {
onExitRequested();
return AppExitResponse.exit;
}
/// [App Lifecycle Listener](https://miro.medium.com/v2/resize:fit:1400/0*bN0QtrIRWGDMC9LJ)
///
/// detached -> resumed -|
/// ^ v
/// | inactive
/// paused <- hidden <--|
///
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
final AppLifecycleState? previousState = _lifecycleState;
if (state == previousState) {
// Transitioning to the same state twice doesn't produce any notifications (but also won't actually occur).
return;
}
_lifecycleState = state;
switch (state) {
case AppLifecycleState.resumed:
onResumed();
break;
case AppLifecycleState.inactive:
if (previousState == AppLifecycleState.hidden) {
onShow();
} else if (previousState == null ||
previousState == AppLifecycleState.resumed) {
onInactive();
}
break;
case AppLifecycleState.hidden:
if (previousState == AppLifecycleState.paused) {
onRestart();
} else if (previousState == null ||
previousState == AppLifecycleState.inactive) {
onHidden();
}
break;
case AppLifecycleState.paused:
if (previousState == null ||
previousState == AppLifecycleState.hidden) {
onPaused();
}
break;
case AppLifecycleState.detached:
onDetached();
break;
default:
}
// At this point, it can't be null anymore.
if (_lifecycleState != null) {
onStateChange(_lifecycleState!);
}
}
void onExitRequested() {
if (kDebugMode) {
print("on exit requested");
}
}
void onStateChange(AppLifecycleState state) {
if (kDebugMode) {
print("on state change: $state");
}
}
void onShow() {
if (kDebugMode) {
print("on show");
}
}
void onHidden() {
if (kDebugMode) {
print("on hidden");
}
}
void onResumed() {
if (kDebugMode) {
print("on resumed");
}
}
void onPaused() {
if (kDebugMode) {
print("on paused");
}
}
void onInactive() {
if (kDebugMode) {
print("on inactive");
}
}
void onDetached() {
if (kDebugMode) {
print("on detached");
}
}
void onRestart() {
if (kDebugMode) {
print("on restart");
}
}
}

View file

@ -0,0 +1,111 @@
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:screen_protector/extension/color_extension.dart';
class ScreenProtector {
static const MethodChannel _channel = MethodChannel('screen_protector');
static void Function()? _onScreenshotListener;
static void Function(bool)? _onScreenRecordListener;
/// Add callback actions when screenshot or screen record events received,
/// Supported for iOS only, do nothing when run on Android.
static void addListener(
void Function()? screenshotListener,
void Function(bool)? screenRecordListener,
) async {
_onScreenshotListener = screenshotListener;
_onScreenRecordListener = screenRecordListener;
_channel.setMethodCallHandler(_methodCallHandler);
await _channel.invokeMethod('addListener');
}
/// Remove listeners
static void _removeListener() {
_onScreenshotListener = null;
_onScreenRecordListener = null;
}
/// Remove observers
/// Supported for iOS only, do nothing when run on Android.
static void removeListener() async {
_removeListener();
await _channel.invokeMethod('removeListener');
}
static Future<dynamic> _methodCallHandler(MethodCall call) async {
if (call.method == 'onScreenshot') {
if (null != _onScreenshotListener) {
_onScreenshotListener!();
}
} else if (call.method == 'onScreenRecord') {
dynamic isCaptured = call.arguments;
if (null != _onScreenRecordListener &&
isCaptured != null &&
isCaptured is bool) {
_onScreenRecordListener!(isCaptured);
}
}
}
/// Supported for Android only, do nothing when run on iOS.
static Future<void> protectDataLeakageOn() async {
return await _channel.invokeMethod('protectDataLeakageOn');
}
/// Supported for Android and iOS.
static Future<void> protectDataLeakageOff() async {
return await _channel.invokeMethod('protectDataLeakageOff');
}
/// Supported for iOS only, do nothing when run on Android.
static Future<void> protectDataLeakageWithBlur() async {
return await _channel.invokeMethod('protectDataLeakageWithBlur');
}
/// Supported for iOS only, do nothing when run on Android.
static Future<void> protectDataLeakageWithBlurOff() async {
return await _channel.invokeMethod('protectDataLeakageWithBlurOff');
}
/// Supported for iOS only, do nothing when run on Android.
static Future<void> protectDataLeakageWithImage(String imageName) async {
return await _channel.invokeMethod('protectDataLeakageWithImage', {
'name': imageName,
});
}
/// Supported for iOS only, do nothing when run on Android.
static Future<void> protectDataLeakageWithImageOff() async {
return await _channel.invokeMethod('protectDataLeakageWithImageOff');
}
/// Supported for iOS only, do nothing when run on Android.
static Future<void> protectDataLeakageWithColor(Color color) async {
return await _channel.invokeMethod('protectDataLeakageWithColor', {
'hexColor': color.toHex(),
});
}
/// Supported for iOS only, do nothing when run on Android.
static Future<void> protectDataLeakageWithColorOff() async {
return await _channel.invokeMethod('protectDataLeakageWithColorOff');
}
/// Supported for Android and iOS.
static Future<void> preventScreenshotOn() async {
return await _channel.invokeMethod('preventScreenshotOn');
}
/// Supported for Android and iOS.
static Future<void> preventScreenshotOff() async {
return await _channel.invokeMethod('preventScreenshotOff');
}
/// Supported for iOS only, do nothing when run on Android.
static Future<bool> isRecording() async {
return await _channel.invokeMethod('isRecording');
}
}

View file

@ -0,0 +1,65 @@
name: screen_protector
description: Safe Data Leakage via Application Background Screenshot and Prevent Screenshot for Android and iOS.
version: 1.5.1
homepage: https://github.com/prongbang/screen_protector
environment:
sdk: ">=2.12.0 <4.0.0"
flutter: ">=2.5.0"
dependencies:
flutter:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.3
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter.
flutter:
# This section identifies this Flutter project as a plugin project.
# The 'pluginClass' and Android 'package' identifiers should not ordinarily
# be modified. They are used by the tooling to maintain consistency when
# adding or updating assets for this project.
plugin:
platforms:
android:
package: com.prongbang.screen_protector
pluginClass: ScreenProtectorPlugin
ios:
pluginClass: ScreenProtectorPlugin
# To add assets to your plugin package, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
#
# For details regarding assets in packages, see
# https://flutter.dev/assets-and-images/#from-packages
#
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware.
# To add custom fonts to your plugin package, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts in packages, see
# https://flutter.dev/custom-fonts/#from-packages

View file

@ -0,0 +1,22 @@
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
const MethodChannel channel = MethodChannel('screen_protector');
TestWidgetsFlutterBinding.ensureInitialized();
setUp(() {
channel.setMockMethodCallHandler((MethodCall methodCall) async {
return '42';
});
});
tearDown(() {
channel.setMockMethodCallHandler(null);
});
test('Should equals 42', () async {
expect('42', '42');
});
}