replace plugin
This commit is contained in:
parent
793b7f0562
commit
e0c6a9617a
53 changed files with 1173 additions and 4496 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
|
@ -1 +0,0 @@
|
|||
rootProject.name = 'no_screenshot'
|
||||
|
|
@ -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 17–30: 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
#import <Flutter/Flutter.h>
|
||||
|
||||
@interface NoScreenshotPlugin : NSObject<FlutterPlugin>
|
||||
@end
|
||||
|
|
@ -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
|
||||
|
|
@ -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")
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
|
|
@ -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";
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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.');
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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 {}
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -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>());
|
||||
});
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -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})'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
@ -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
201
screen_protector/LICENSE
Normal 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.
|
||||
|
|
@ -6,4 +6,3 @@
|
|||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.cxx
|
||||
48
screen_protector/android/build.gradle
Normal file
48
screen_protector/android/build.gradle
Normal 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'
|
||||
}
|
||||
}
|
||||
BIN
screen_protector/android/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
screen_protector/android/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
13
screen_protector/android/settings.gradle
Normal file
13
screen_protector/android/settings.gradle
Normal 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'
|
||||
|
|
@ -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>
|
||||
|
|
@ -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() {}
|
||||
|
||||
}
|
||||
39
screen_protector/ios/Classes/FlutterRootViewResolver.swift
Normal file
39
screen_protector/ios/Classes/FlutterRootViewResolver.swift
Normal 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)")
|
||||
}
|
||||
}
|
||||
4
screen_protector/ios/Classes/ScreenProtectorPlugin.h
Normal file
4
screen_protector/ios/Classes/ScreenProtectorPlugin.h
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
#import <Flutter/Flutter.h>
|
||||
|
||||
@interface ScreenProtectorPlugin : NSObject<FlutterPlugin>
|
||||
@end
|
||||
|
|
@ -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
|
||||
216
screen_protector/ios/Classes/SwiftScreenProtectorPlugin.swift
Normal file
216
screen_protector/ios/Classes/SwiftScreenProtectorPlugin.swift
Normal 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
|
||||
}
|
||||
}
|
||||
22
screen_protector/ios/screen_protector.podspec
Normal file
22
screen_protector/ios/screen_protector.podspec
Normal 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
|
||||
18
screen_protector/lib/extension/color_extension.dart
Normal file
18
screen_protector/lib/extension/color_extension.dart
Normal 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')}';
|
||||
}
|
||||
68
screen_protector/lib/lifecycle/legacy_lifecycle_state.dart
Normal file
68
screen_protector/lib/lifecycle/legacy_lifecycle_state.dart
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
135
screen_protector/lib/lifecycle/lifecycle_state.dart
Normal file
135
screen_protector/lib/lifecycle/lifecycle_state.dart
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
111
screen_protector/lib/screen_protector.dart
Normal file
111
screen_protector/lib/screen_protector.dart
Normal 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');
|
||||
}
|
||||
}
|
||||
65
screen_protector/pubspec.yaml
Normal file
65
screen_protector/pubspec.yaml
Normal 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
|
||||
22
screen_protector/test/screen_protector_test.dart
Normal file
22
screen_protector/test/screen_protector_test.dart
Normal 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');
|
||||
});
|
||||
}
|
||||
Loading…
Reference in a new issue