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
|
libsignal_protocol_dart: c95a1586057022acdbb9c76b1692d94cc549bcc7
|
||||||
lottie: 4f1a5a52bdf1e1c1e12fa97c96174dcb05419e19
|
lottie: 4f1a5a52bdf1e1c1e12fa97c96174dcb05419e19
|
||||||
mutex: 84ca903a3ac863735e3228c75a212133621f680f
|
mutex: 84ca903a3ac863735e3228c75a212133621f680f
|
||||||
no_screenshot: daf759e30219224630b4af0b82061d25a457a393
|
no_screenshot: fbfa2ed7ec4db782797fa6a7de8f207a2cba00bb
|
||||||
optional: 71c638891ce4f2aff35c7387727989f31f9d877d
|
optional: 71c638891ce4f2aff35c7387727989f31f9d877d
|
||||||
photo_view: a13ca2fc387a3fb1276126959e092c44d0029987
|
photo_view: a13ca2fc387a3fb1276126959e092c44d0029987
|
||||||
pointycastle: bbd8569f68a7fccbdf0b92d0b44a9219c126c8dd
|
pointycastle: bbd8569f68a7fccbdf0b92d0b44a9219c126c8dd
|
||||||
qr: 7b1e9665ca976f484e7975356cf26fc7a0ccf02e
|
qr: 7b1e9665ca976f484e7975356cf26fc7a0ccf02e
|
||||||
qr_flutter: d5e7206396105d643113618290bbcc755d05f492
|
qr_flutter: d5e7206396105d643113618290bbcc755d05f492
|
||||||
restart_app: 66897cb67e235bab85421647bfae036acb4438cb
|
restart_app: 66897cb67e235bab85421647bfae036acb4438cb
|
||||||
|
screen_protector: 019c04d622d7b610d2903d3a347edc3ba76a6ed0
|
||||||
x25519: ecb1d357714537bba6e276ef45f093846d4beaee
|
x25519: ecb1d357714537bba6e276ef45f093846d4beaee
|
||||||
|
|
|
||||||
|
|
@ -54,8 +54,8 @@ flutter_sharing_intent:
|
||||||
restart_app:
|
restart_app:
|
||||||
git: https://github.com/gabrimatic/restart_app
|
git: https://github.com/gabrimatic/restart_app
|
||||||
|
|
||||||
no_screenshot:
|
screen_protector:
|
||||||
git: https://github.com/FlutterPlaza/no_screenshot.git
|
git: https://github.com/prongbang/screen_protector.git
|
||||||
|
|
||||||
|
|
||||||
flutter_markdown_plus:
|
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
|
path: ./dependencies/lottie
|
||||||
mutex:
|
mutex:
|
||||||
path: ./dependencies/mutex
|
path: ./dependencies/mutex
|
||||||
no_screenshot:
|
|
||||||
path: ./dependencies/no_screenshot
|
|
||||||
optional:
|
optional:
|
||||||
path: ./dependencies/optional
|
path: ./dependencies/optional
|
||||||
photo_view:
|
photo_view:
|
||||||
|
|
@ -37,5 +35,7 @@ dependency_overrides:
|
||||||
path: ./dependencies/qr_flutter
|
path: ./dependencies/qr_flutter
|
||||||
restart_app:
|
restart_app:
|
||||||
path: ./dependencies/restart_app
|
path: ./dependencies/restart_app
|
||||||
|
screen_protector:
|
||||||
|
path: ./dependencies/screen_protector
|
||||||
x25519:
|
x25519:
|
||||||
path: ./dependencies/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
|
.DS_Store
|
||||||
/build
|
/build
|
||||||
/captures
|
/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
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
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
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|
@ -15,6 +15,8 @@
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
#
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
#
|
#
|
||||||
|
|
@ -55,7 +57,7 @@
|
||||||
# Darwin, MinGW, and NonStop.
|
# Darwin, MinGW, and NonStop.
|
||||||
#
|
#
|
||||||
# (3) This script is generated from the Groovy template
|
# (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.
|
# within the Gradle project.
|
||||||
#
|
#
|
||||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
|
@ -80,13 +82,11 @@ do
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
# This is normally unused
|
||||||
|
# shellcheck disable=SC2034
|
||||||
APP_NAME="Gradle"
|
|
||||||
APP_BASE_NAME=${0##*/}
|
APP_BASE_NAME=${0##*/}
|
||||||
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
MAX_FD=maximum
|
MAX_FD=maximum
|
||||||
|
|
@ -133,22 +133,29 @@ location of your Java installation."
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
JAVACMD=java
|
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
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
location of your Java installation."
|
location of your Java installation."
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Increase the maximum file descriptors if we can.
|
# Increase the maximum file descriptors if we can.
|
||||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
case $MAX_FD in #(
|
case $MAX_FD in #(
|
||||||
max*)
|
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 ) ||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
warn "Could not query maximum file descriptor limit"
|
warn "Could not query maximum file descriptor limit"
|
||||||
esac
|
esac
|
||||||
case $MAX_FD in #(
|
case $MAX_FD in #(
|
||||||
'' | soft) :;; #(
|
'' | 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" ||
|
ulimit -n "$MAX_FD" ||
|
||||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
esac
|
esac
|
||||||
|
|
@ -193,11 +200,15 @@ if "$cygwin" || "$msys" ; then
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Collect all arguments for the java command;
|
|
||||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
# shell script including quotes and variable substitutions, so put them in
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
# double quotes to make sure that they get re-expanded; and
|
|
||||||
# * put everything else in single quotes, so that it's not re-expanded.
|
# 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 -- \
|
set -- \
|
||||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
|
@ -205,6 +216,12 @@ set -- \
|
||||||
org.gradle.wrapper.GradleWrapperMain \
|
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.
|
# Use "xargs" to parse quoted args.
|
||||||
#
|
#
|
||||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||||
|
|
@ -1,89 +1,94 @@
|
||||||
@rem
|
@rem
|
||||||
@rem Copyright 2015 the original author or authors.
|
@rem Copyright 2015 the original author or authors.
|
||||||
@rem
|
@rem
|
||||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
@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 not use this file except in compliance with the License.
|
||||||
@rem You may obtain a copy of the License at
|
@rem You may obtain a copy of the License at
|
||||||
@rem
|
@rem
|
||||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
@rem
|
@rem
|
||||||
@rem Unless required by applicable law or agreed to in writing, software
|
@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 distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
@rem See the License for the specific language governing permissions and
|
@rem See the License for the specific language governing permissions and
|
||||||
@rem limitations under the License.
|
@rem limitations under the License.
|
||||||
@rem
|
@rem
|
||||||
|
@rem SPDX-License-Identifier: Apache-2.0
|
||||||
@if "%DEBUG%" == "" @echo off
|
@rem
|
||||||
@rem ##########################################################################
|
|
||||||
@rem
|
@if "%DEBUG%"=="" @echo off
|
||||||
@rem Gradle startup script for Windows
|
@rem ##########################################################################
|
||||||
@rem
|
@rem
|
||||||
@rem ##########################################################################
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
@rem Set local scope for the variables with windows NT shell
|
@rem ##########################################################################
|
||||||
if "%OS%"=="Windows_NT" setlocal
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
set DIRNAME=%~dp0
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
if "%DIRNAME%" == "" set DIRNAME=.
|
|
||||||
set APP_BASE_NAME=%~n0
|
set DIRNAME=%~dp0
|
||||||
set APP_HOME=%DIRNAME%
|
if "%DIRNAME%"=="" set DIRNAME=.
|
||||||
|
@rem This is normally unused
|
||||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
set APP_BASE_NAME=%~n0
|
||||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
@rem Find java.exe
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
set JAVA_EXE=java.exe
|
@rem Find java.exe
|
||||||
%JAVA_EXE% -version >NUL 2>&1
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
if "%ERRORLEVEL%" == "0" goto execute
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
echo.
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
if %ERRORLEVEL% equ 0 goto execute
|
||||||
echo.
|
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the
|
echo. 1>&2
|
||||||
echo location of your Java installation.
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||||
|
echo. 1>&2
|
||||||
goto fail
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
|
echo location of your Java installation. 1>&2
|
||||||
:findJavaFromJavaHome
|
|
||||||
set JAVA_HOME=%JAVA_HOME:"=%
|
goto fail
|
||||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
if exist "%JAVA_EXE%" goto execute
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
echo.
|
|
||||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
if exist "%JAVA_EXE%" goto execute
|
||||||
echo.
|
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the
|
echo. 1>&2
|
||||||
echo location of your Java installation.
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||||
|
echo. 1>&2
|
||||||
goto fail
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
|
echo location of your Java installation. 1>&2
|
||||||
:execute
|
|
||||||
@rem Setup the command line
|
goto fail
|
||||||
|
|
||||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
@rem Execute Gradle
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
|
||||||
|
|
||||||
:end
|
@rem Execute Gradle
|
||||||
@rem End local scope for the variables with windows NT shell
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
|
||||||
|
:end
|
||||||
:fail
|
@rem End local scope for the variables with windows NT shell
|
||||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||||
rem the _cmd.exe /c_ return code!
|
|
||||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
:fail
|
||||||
exit /b 1
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
:mainEnd
|
set EXIT_CODE=%ERRORLEVEL%
|
||||||
if "%OS%"=="Windows_NT" endlocal
|
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||||
|
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||||
:omega
|
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>
|
</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"
|
#import "ScreenProtectorPlugin.h"
|
||||||
#if __has_include(<no_screenshot/no_screenshot-Swift.h>)
|
#if __has_include(<screen_protector/screen_protector-Swift.h>)
|
||||||
#import <no_screenshot/no_screenshot-Swift.h>
|
#import <screen_protector/screen_protector-Swift.h>
|
||||||
#else
|
#else
|
||||||
// Support project import fallback if the generated compatibility header
|
// Support project import fallback if the generated compatibility header
|
||||||
// is not copied when this plugin is created as a library.
|
// 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
|
// 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
|
#endif
|
||||||
|
|
||||||
@implementation NoScreenshotPlugin
|
@implementation ScreenProtectorPlugin
|
||||||
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
|
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
|
||||||
[IOSNoScreenshotPlugin registerWithRegistrar:registrar];
|
[SwiftScreenProtectorPlugin registerWithRegistrar:registrar];
|
||||||
}
|
}
|
||||||
@end
|
@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