diff --git a/config.lock.yaml b/config.lock.yaml index 6cb8a41..a84c1ac 100644 --- a/config.lock.yaml +++ b/config.lock.yaml @@ -10,11 +10,12 @@ introduction_screen: 4a90e557630b28834479ed9c64a9d2d0185d8e48 libsignal_protocol_dart: c95a1586057022acdbb9c76b1692d94cc549bcc7 lottie: 4f1a5a52bdf1e1c1e12fa97c96174dcb05419e19 mutex: 84ca903a3ac863735e3228c75a212133621f680f -no_screenshot: daf759e30219224630b4af0b82061d25a457a393 +no_screenshot: fbfa2ed7ec4db782797fa6a7de8f207a2cba00bb optional: 71c638891ce4f2aff35c7387727989f31f9d877d photo_view: a13ca2fc387a3fb1276126959e092c44d0029987 pointycastle: bbd8569f68a7fccbdf0b92d0b44a9219c126c8dd qr: 7b1e9665ca976f484e7975356cf26fc7a0ccf02e qr_flutter: d5e7206396105d643113618290bbcc755d05f492 restart_app: 66897cb67e235bab85421647bfae036acb4438cb +screen_protector: 019c04d622d7b610d2903d3a347edc3ba76a6ed0 x25519: ecb1d357714537bba6e276ef45f093846d4beaee diff --git a/config.yaml b/config.yaml index 4c9c28f..6461cd8 100644 --- a/config.yaml +++ b/config.yaml @@ -54,8 +54,8 @@ flutter_sharing_intent: restart_app: git: https://github.com/gabrimatic/restart_app -no_screenshot: - git: https://github.com/FlutterPlaza/no_screenshot.git +screen_protector: + git: https://github.com/prongbang/screen_protector.git flutter_markdown_plus: diff --git a/no_screenshot/LICENSE b/no_screenshot/LICENSE deleted file mode 100644 index 8130832..0000000 --- a/no_screenshot/LICENSE +++ /dev/null @@ -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. diff --git a/no_screenshot/android/build.gradle b/no_screenshot/android/build.gradle deleted file mode 100644 index 80b139c..0000000 --- a/no_screenshot/android/build.gradle +++ /dev/null @@ -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 - } -} diff --git a/no_screenshot/android/gradle/wrapper/gradle-wrapper.jar b/no_screenshot/android/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 41d9927..0000000 Binary files a/no_screenshot/android/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/no_screenshot/android/settings.gradle b/no_screenshot/android/settings.gradle deleted file mode 100644 index 8ed0d6a..0000000 --- a/no_screenshot/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'no_screenshot' diff --git a/no_screenshot/android/src/main/kotlin/com/flutterplaza/no_screenshot/NoScreenshotPlugin.kt b/no_screenshot/android/src/main/kotlin/com/flutterplaza/no_screenshot/NoScreenshotPlugin.kt deleted file mode 100644 index 0fee690..0000000 --- a/no_screenshot/android/src/main/kotlin/com/flutterplaza/no_screenshot/NoScreenshotPlugin.kt +++ /dev/null @@ -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("radius") ?: 30.0).toFloat() - result.success(toggleScreenshotWithBlur(radius)) - } - - SET_COLOR_CONST -> { - val color = call.argument("color") ?: 0xFF000000.toInt() - result.success(toggleScreenshotWithColor(color)) - } - - ENABLE_IMAGE_CONST -> { - result.success(enableImageOverlay()) - } - - ENABLE_BLUR_CONST -> { - val radius = (call.argument("radius") ?: 30.0).toFloat() - result.success(enableBlurOverlay(radius)) - } - - ENABLE_COLOR_CONST -> { - val color = call.argument("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 { - return JSONObject(map).toString() - } - - private val screenshotStream = object : Runnable { - override fun run() { - if (hasSharedPreferencesChanged) { - eventSink?.success(lastSharedPreferencesState) - hasSharedPreferencesChanged = false - } - handler.postDelayed(this, 1000) - } - } -} diff --git a/no_screenshot/android/src/test/kotlin/com/flutterplaza/no_screenshot/NoScreenshotPluginTest.kt b/no_screenshot/android/src/test/kotlin/com/flutterplaza/no_screenshot/NoScreenshotPluginTest.kt deleted file mode 100644 index fe068a2..0000000 --- a/no_screenshot/android/src/test/kotlin/com/flutterplaza/no_screenshot/NoScreenshotPluginTest.kt +++ /dev/null @@ -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) - } -} diff --git a/no_screenshot/ios/Classes/NoScreenshotPlugin.h b/no_screenshot/ios/Classes/NoScreenshotPlugin.h deleted file mode 100644 index 0198b60..0000000 --- a/no_screenshot/ios/Classes/NoScreenshotPlugin.h +++ /dev/null @@ -1,4 +0,0 @@ -#import - -@interface NoScreenshotPlugin : NSObject -@end diff --git a/no_screenshot/ios/no_screenshot.podspec b/no_screenshot/ios/no_screenshot.podspec deleted file mode 100644 index 324cc20..0000000 --- a/no_screenshot/ios/no_screenshot.podspec +++ /dev/null @@ -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 diff --git a/no_screenshot/ios/no_screenshot/Package.swift b/no_screenshot/ios/no_screenshot/Package.swift deleted file mode 100644 index c20ec7d..0000000 --- a/no_screenshot/ios/no_screenshot/Package.swift +++ /dev/null @@ -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") - ] - ) - ] -) diff --git a/no_screenshot/ios/no_screenshot/Sources/no_screenshot/IOSNoScreenshotPlugin.swift b/no_screenshot/ios/no_screenshot/Sources/no_screenshot/IOSNoScreenshotPlugin.swift deleted file mode 100644 index 5f0b300..0000000 --- a/no_screenshot/ios/no_screenshot/Sources/no_screenshot/IOSNoScreenshotPlugin.swift +++ /dev/null @@ -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 diff --git a/no_screenshot/ios/no_screenshot/Sources/no_screenshot/PrivacyInfo.xcprivacy b/no_screenshot/ios/no_screenshot/Sources/no_screenshot/PrivacyInfo.xcprivacy deleted file mode 100644 index a34b7e2..0000000 --- a/no_screenshot/ios/no_screenshot/Sources/no_screenshot/PrivacyInfo.xcprivacy +++ /dev/null @@ -1,14 +0,0 @@ - - - - - NSPrivacyTrackingDomains - - NSPrivacyAccessedAPITypes - - NSPrivacyCollectedDataTypes - - NSPrivacyTracking - - - diff --git a/no_screenshot/lib/constants.dart b/no_screenshot/lib/constants.dart deleted file mode 100644 index 32842be..0000000 --- a/no_screenshot/lib/constants.dart +++ /dev/null @@ -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"; diff --git a/no_screenshot/lib/no_screenshot.dart b/no_screenshot/lib/no_screenshot.dart deleted file mode 100644 index 2fad0c6..0000000 --- a/no_screenshot/lib/no_screenshot.dart +++ /dev/null @@ -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? _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 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 screenshotOn() { - return _instancePlatform.screenshotOn(); - } - - @override - Future toggleScreenshotWithImage() { - return _instancePlatform.toggleScreenshotWithImage(); - } - - @override - Future toggleScreenshotWithBlur({double blurRadius = 30.0}) { - return _instancePlatform.toggleScreenshotWithBlur(blurRadius: blurRadius); - } - - @override - Future toggleScreenshotWithColor({int color = 0xFF000000}) { - return _instancePlatform.toggleScreenshotWithColor(color: color); - } - - /// Always enables image overlay mode (idempotent — safe to call repeatedly). - @override - Future screenshotWithImage() { - return _instancePlatform.screenshotWithImage(); - } - - /// Always enables blur overlay mode (idempotent — safe to call repeatedly). - @override - Future screenshotWithBlur({double blurRadius = 30.0}) { - return _instancePlatform.screenshotWithBlur(blurRadius: blurRadius); - } - - /// Always enables color overlay mode (idempotent — safe to call repeatedly). - @override - Future 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 toggleScreenshot() { - return _instancePlatform.toggleScreenshot(); - } - - /// Stream to screenshot activities [ScreenshotSnapshot] - /// - @override - Stream get screenshotStream { - return _instancePlatform.screenshotStream; - } - - /// Start listening to screenshot activities - @override - Future startScreenshotListening() { - return _instancePlatform.startScreenshotListening(); - } - - /// Stop listening to screenshot activities - @override - Future stopScreenshotListening() { - return _instancePlatform.stopScreenshotListening(); - } - - /// Start listening to screen recording activities - @override - Future startScreenRecordingListening() { - return _instancePlatform.startScreenRecordingListening(); - } - - /// Stop listening to screen recording activities - @override - Future 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; -} diff --git a/no_screenshot/lib/no_screenshot_method_channel.dart b/no_screenshot/lib/no_screenshot_method_channel.dart deleted file mode 100644 index f591480..0000000 --- a/no_screenshot/lib/no_screenshot_method_channel.dart +++ /dev/null @@ -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? _cachedStream; - - @override - Stream get screenshotStream { - _cachedStream ??= eventChannel.receiveBroadcastStream().map((event) => - ScreenshotSnapshot.fromMap(jsonDecode(event) as Map)); - return _cachedStream!; - } - - @override - Future toggleScreenshot() async { - final result = - await methodChannel.invokeMethod(toggleScreenShotConst); - return result ?? false; - } - - @override - Future screenshotOff() async { - final result = await methodChannel.invokeMethod(screenShotOffConst); - return result ?? false; - } - - @override - Future screenshotOn() async { - final result = await methodChannel.invokeMethod(screenShotOnConst); - return result ?? false; - } - - @override - Future toggleScreenshotWithImage() async { - final result = await methodChannel.invokeMethod(screenSetImage); - return result ?? false; - } - - @override - Future toggleScreenshotWithBlur({double blurRadius = 30.0}) async { - final result = await methodChannel - .invokeMethod(screenSetBlur, {'radius': blurRadius}); - return result ?? false; - } - - @override - Future toggleScreenshotWithColor({int color = 0xFF000000}) async { - final result = await methodChannel - .invokeMethod(screenSetColor, {'color': color}); - return result ?? false; - } - - @override - Future screenshotWithImage() async { - final result = await methodChannel.invokeMethod(screenEnableImage); - return result ?? false; - } - - @override - Future screenshotWithBlur({double blurRadius = 30.0}) async { - final result = await methodChannel - .invokeMethod(screenEnableBlur, {'radius': blurRadius}); - return result ?? false; - } - - @override - Future screenshotWithColor({int color = 0xFF000000}) async { - final result = await methodChannel - .invokeMethod(screenEnableColor, {'color': color}); - return result ?? false; - } - - @override - Future startScreenshotListening() { - return methodChannel.invokeMethod(startScreenshotListeningConst); - } - - @override - Future stopScreenshotListening() { - return methodChannel.invokeMethod(stopScreenshotListeningConst); - } - - @override - Future startScreenRecordingListening() { - return methodChannel.invokeMethod(startScreenRecordingListeningConst); - } - - @override - Future stopScreenRecordingListening() { - return methodChannel.invokeMethod(stopScreenRecordingListeningConst); - } -} diff --git a/no_screenshot/lib/no_screenshot_platform_interface.dart b/no_screenshot/lib/no_screenshot_platform_interface.dart deleted file mode 100644 index f9ac29e..0000000 --- a/no_screenshot/lib/no_screenshot_platform_interface.dart +++ /dev/null @@ -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 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 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 toggleScreenshotWithImage() { - throw UnimplementedError( - 'toggleScreenshotWithImage() has not been implemented.'); - } - - Future toggleScreenshotWithBlur({double blurRadius = 30.0}) { - throw UnimplementedError( - 'toggleScreenshotWithBlur() has not been implemented.'); - } - - Future toggleScreenshotWithColor({int color = 0xFF000000}) { - throw UnimplementedError( - 'toggleScreenshotWithColor() has not been implemented.'); - } - - /// Always enables image overlay mode (idempotent — safe to call repeatedly). - Future screenshotWithImage() { - throw UnimplementedError('screenshotWithImage() has not been implemented.'); - } - - /// Always enables blur overlay mode (idempotent — safe to call repeatedly). - Future screenshotWithBlur({double blurRadius = 30.0}) { - throw UnimplementedError('screenshotWithBlur() has not been implemented.'); - } - - /// Always enables color overlay mode (idempotent — safe to call repeatedly). - Future 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 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 get screenshotStream { - throw UnimplementedError('incrementStream has not been implemented.'); - } - -// Start listening to screenshot activities - Future startScreenshotListening() { - throw UnimplementedError( - 'startScreenshotListening has not been implemented.'); - } - - /// Stop listening to screenshot activities - Future stopScreenshotListening() { - throw UnimplementedError( - 'stopScreenshotListening has not been implemented.'); - } - - /// Start listening to screen recording activities - Future startScreenRecordingListening() { - throw UnimplementedError( - 'startScreenRecordingListening has not been implemented.'); - } - - /// Stop listening to screen recording activities - Future stopScreenRecordingListening() { - throw UnimplementedError( - 'stopScreenRecordingListening has not been implemented.'); - } -} diff --git a/no_screenshot/lib/no_screenshot_web.dart b/no_screenshot/lib/no_screenshot_web.dart deleted file mode 100644 index fe7e0e8..0000000 --- a/no_screenshot/lib/no_screenshot_web.dart +++ /dev/null @@ -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 _controller = - StreamController.broadcast(); - - // ── JS event listeners (stored for removal) ──────────────────────── - - JSFunction? _contextMenuHandler; - JSFunction? _keyDownHandler; - JSFunction? _visibilityHandler; - - // ── Stream ───────────────────────────────────────────────────────── - - @override - Stream get screenshotStream => _controller.stream; - - // ── Protection ───────────────────────────────────────────────────── - - @override - Future screenshotOff() async { - _enableProtection(); - return true; - } - - @override - Future screenshotOn() async { - _disableProtection(); - return true; - } - - @override - Future toggleScreenshot() async { - _isProtectionOn ? _disableProtection() : _enableProtection(); - return true; - } - - @override - Future toggleScreenshotWithImage() async { - _isProtectionOn ? _disableProtection() : _enableProtection(); - return _isProtectionOn; - } - - @override - Future toggleScreenshotWithBlur({double blurRadius = 30.0}) async { - _isProtectionOn ? _disableProtection() : _enableProtection(); - return _isProtectionOn; - } - - @override - Future toggleScreenshotWithColor({int color = 0xFF000000}) async { - _isProtectionOn ? _disableProtection() : _enableProtection(); - return _isProtectionOn; - } - - @override - Future screenshotWithImage() async { - _enableProtection(); - return true; - } - - @override - Future screenshotWithBlur({double blurRadius = 30.0}) async { - _enableProtection(); - return true; - } - - @override - Future screenshotWithColor({int color = 0xFF000000}) async { - _enableProtection(); - return true; - } - - // ── Screenshot Listening ─────────────────────────────────────────── - - @override - Future startScreenshotListening() async { - if (_isListening) return; - _isListening = true; - _addVisibilityListener(); - } - - @override - Future stopScreenshotListening() async { - _isListening = false; - _removeVisibilityListener(); - } - - // ── Recording Listening (no-op on web) ───────────────────────────── - - @override - Future startScreenRecordingListening() async {} - - @override - Future 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; - } - } -} diff --git a/no_screenshot/lib/overlay_mode.dart b/no_screenshot/lib/overlay_mode.dart deleted file mode 100644 index 00c4af6..0000000 --- a/no_screenshot/lib/overlay_mode.dart +++ /dev/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 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(); - } -} diff --git a/no_screenshot/lib/screenshot_snapshot.dart b/no_screenshot/lib/screenshot_snapshot.dart deleted file mode 100644 index 04efeaa..0000000 --- a/no_screenshot/lib/screenshot_snapshot.dart +++ /dev/null @@ -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 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 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; - } -} diff --git a/no_screenshot/lib/secure_navigator_observer.dart b/no_screenshot/lib/secure_navigator_observer.dart deleted file mode 100644 index b6325e6..0000000 --- a/no_screenshot/lib/secure_navigator_observer.dart +++ /dev/null @@ -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 policies; - final SecureRouteConfig defaultConfig; - - @override - void didPush(Route route, Route? previousRoute) { - _applyPolicyForRoute(route); - } - - @override - void didPop(Route route, Route? previousRoute) { - _applyPolicyForRoute(previousRoute); - } - - @override - void didReplace({Route? newRoute, Route? oldRoute}) { - _applyPolicyForRoute(newRoute); - } - - @override - void didRemove(Route route, Route? previousRoute) { - _applyPolicyForRoute(previousRoute); - } - - void _applyPolicyForRoute(Route? route) { - final name = route?.settings.name; - final config = (name != null ? policies[name] : null) ?? defaultConfig; - applyOverlayMode(config.mode, - blurRadius: config.blurRadius, color: config.color); - } -} diff --git a/no_screenshot/lib/secure_widget.dart b/no_screenshot/lib/secure_widget.dart deleted file mode 100644 index 424c656..0000000 --- a/no_screenshot/lib/secure_widget.dart +++ /dev/null @@ -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 createState() => _SecureWidgetState(); -} - -class _SecureWidgetState extends State { - @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; -} diff --git a/no_screenshot/pubspec.yaml b/no_screenshot/pubspec.yaml deleted file mode 100644 index 00e2ad7..0000000 --- a/no_screenshot/pubspec.yaml +++ /dev/null @@ -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 diff --git a/no_screenshot/test/no_screenshot_method_channel_test.dart b/no_screenshot/test/no_screenshot_method_channel_test.dart deleted file mode 100644 index 1f5cfe7..0000000 --- a/no_screenshot/test/no_screenshot_method_channel_test.dart +++ /dev/null @@ -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 = { - '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 controller; - late _MockNoScreenshotPlatform mockPlatform; - late NoScreenshot noScreenshot; - - setUp(() { - controller = StreamController.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 = []; - 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 = []; - 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 = []; - 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 = []; - 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 = []; - 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 _stream; - - _MockNoScreenshotPlatform(this._stream); - - @override - Stream get screenshotStream => _stream; - - @override - Future screenshotOff() async => true; - - @override - Future screenshotOn() async => true; - - @override - Future toggleScreenshot() async => true; - - @override - Future toggleScreenshotWithImage() async => true; - - @override - Future toggleScreenshotWithBlur({double blurRadius = 30.0}) async => - true; - - @override - Future toggleScreenshotWithColor({int color = 0xFF000000}) async => - true; - - @override - Future screenshotWithImage() async => true; - - @override - Future screenshotWithBlur({double blurRadius = 30.0}) async => true; - - @override - Future screenshotWithColor({int color = 0xFF000000}) async => true; - - @override - Future startScreenshotListening() async {} - - @override - Future stopScreenshotListening() async {} - - @override - Future startScreenRecordingListening() async {} - - @override - Future stopScreenRecordingListening() async {} -} diff --git a/no_screenshot/test/no_screenshot_platform_interface_test.dart b/no_screenshot/test/no_screenshot_platform_interface_test.dart deleted file mode 100644 index a8e4908..0000000 --- a/no_screenshot/test/no_screenshot_platform_interface_test.dart +++ /dev/null @@ -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 screenshotOff() async { - return true; - } - - @override - Future screenshotOn() async { - return true; - } - - @override - Future toggleScreenshot() async { - return true; - } - - @override - Stream get screenshotStream { - return const Stream.empty(); - } - - @override - Future startScreenshotListening() async { - return; - } - - @override - Future toggleScreenshotWithImage() async { - return true; - } - - @override - Future toggleScreenshotWithBlur({double blurRadius = 30.0}) async { - return true; - } - - @override - Future toggleScreenshotWithColor({int color = 0xFF000000}) async { - return true; - } - - @override - Future stopScreenshotListening() async { - return; - } - - @override - Future screenshotWithImage() async { - return true; - } - - @override - Future screenshotWithBlur({double blurRadius = 30.0}) async { - return true; - } - - @override - Future screenshotWithColor({int color = 0xFF000000}) async { - return true; - } - - @override - Future startScreenRecordingListening() async { - return; - } - - @override - Future stopScreenRecordingListening() async { - return; - } -} - -void main() { - final platform = MockNoScreenshotPlatform(); - - group('NoScreenshotPlatform', () { - test('default instance should be MethodChannelNoScreenshot', () { - expect(NoScreenshotPlatform.instance, - isInstanceOf()); - }); - - 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); - }); - }); -} diff --git a/no_screenshot/test/no_screenshot_test.dart b/no_screenshot/test/no_screenshot_test.dart deleted file mode 100644 index 9d74bfe..0000000 --- a/no_screenshot/test/no_screenshot_test.dart +++ /dev/null @@ -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 screenshotOff() async { - // Mock implementation or return a fixed value - return Future.value(true); - } - - @override - Future screenshotOn() async { - // Mock implementation or return a fixed value - return Future.value(true); - } - - @override - Future toggleScreenshotWithImage() async { - return Future.value(true); - } - - @override - Future toggleScreenshotWithBlur({double blurRadius = 30.0}) async { - return Future.value(true); - } - - @override - Future toggleScreenshotWithColor({int color = 0xFF000000}) async { - return Future.value(true); - } - - @override - Future toggleScreenshot() async { - // Mock implementation or return a fixed value - return Future.value(true); - } - - @override - Stream get screenshotStream => const Stream.empty(); - - @override - Future screenshotWithImage() async { - return Future.value(true); - } - - @override - Future screenshotWithBlur({double blurRadius = 30.0}) async { - return Future.value(true); - } - - @override - Future screenshotWithColor({int color = 0xFF000000}) async { - return Future.value(true); - } - - @override - Future startScreenshotListening() { - return Future.value(); - } - - @override - Future stopScreenshotListening() { - return Future.value(); - } - - @override - Future startScreenRecordingListening() { - return Future.value(); - } - - @override - Future 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()); - }); - - 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>()); - }); - 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()); - }); -} diff --git a/no_screenshot/test/no_screenshot_web_test.dart b/no_screenshot/test/no_screenshot_web_test.dart deleted file mode 100644 index e4551e5..0000000 --- a/no_screenshot/test/no_screenshot_web_test.dart +++ /dev/null @@ -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 = []; - 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 = []; - 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 = []; - 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 = []; - 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); - }); - }); -} diff --git a/no_screenshot/test/secure_navigator_observer_test.dart b/no_screenshot/test/secure_navigator_observer_test.dart deleted file mode 100644 index 85c93cb..0000000 --- a/no_screenshot/test/secure_navigator_observer_test.dart +++ /dev/null @@ -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 calls = []; - - @override - Future screenshotOff() async { - calls.add('screenshotOff'); - return true; - } - - @override - Future screenshotOn() async { - calls.add('screenshotOn'); - return true; - } - - @override - Future screenshotWithImage() async { - calls.add('screenshotWithImage'); - return true; - } - - @override - Future screenshotWithBlur({double blurRadius = 30.0}) async { - calls.add('screenshotWithBlur($blurRadius)'); - return true; - } - - @override - Future screenshotWithColor({int color = 0xFF000000}) async { - calls.add('screenshotWithColor($color)'); - return true; - } - - @override - Future toggleScreenshot() async => true; - - @override - Future toggleScreenshotWithImage() async => true; - - @override - Future toggleScreenshotWithBlur({double blurRadius = 30.0}) async => - true; - - @override - Future toggleScreenshotWithColor({int color = 0xFF000000}) async => - true; - - @override - Stream get screenshotStream => const Stream.empty(); - - @override - Future startScreenshotListening() async {} - - @override - Future stopScreenshotListening() async {} - - @override - Future startScreenRecordingListening() async {} - - @override - Future stopScreenRecordingListening() async {} -} - -// Helper to create a fake route with a given name -Route _fakeRoute(String? name) { - return PageRouteBuilder( - 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.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.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.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.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.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.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.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.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.delayed(Duration.zero); - expect( - fakePlatform.calls, contains('screenshotWithColor(${0xFF2196F3})')); - }); - }); -} diff --git a/no_screenshot/test/secure_widget_test.dart b/no_screenshot/test/secure_widget_test.dart deleted file mode 100644 index 76ede81..0000000 --- a/no_screenshot/test/secure_widget_test.dart +++ /dev/null @@ -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 calls = []; - - @override - Future screenshotOff() async { - calls.add('screenshotOff'); - return true; - } - - @override - Future screenshotOn() async { - calls.add('screenshotOn'); - return true; - } - - @override - Future screenshotWithImage() async { - calls.add('screenshotWithImage'); - return true; - } - - @override - Future screenshotWithBlur({double blurRadius = 30.0}) async { - calls.add('screenshotWithBlur($blurRadius)'); - return true; - } - - @override - Future screenshotWithColor({int color = 0xFF000000}) async { - calls.add('screenshotWithColor($color)'); - return true; - } - - @override - Future toggleScreenshot() async => true; - - @override - Future toggleScreenshotWithImage() async => true; - - @override - Future toggleScreenshotWithBlur({double blurRadius = 30.0}) async => - true; - - @override - Future toggleScreenshotWithColor({int color = 0xFF000000}) async => - true; - - @override - Stream get screenshotStream => const Stream.empty(); - - @override - Future startScreenshotListening() async {} - - @override - Future stopScreenshotListening() async {} - - @override - Future startScreenRecordingListening() async {} - - @override - Future 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); - }); -} diff --git a/pubspec.yaml b/pubspec.yaml index 50ec375..e93025a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -23,8 +23,6 @@ dependency_overrides: path: ./dependencies/lottie mutex: path: ./dependencies/mutex - no_screenshot: - path: ./dependencies/no_screenshot optional: path: ./dependencies/optional photo_view: @@ -37,5 +35,7 @@ dependency_overrides: path: ./dependencies/qr_flutter restart_app: path: ./dependencies/restart_app + screen_protector: + path: ./dependencies/screen_protector x25519: path: ./dependencies/x25519 diff --git a/screen_protector/LICENSE b/screen_protector/LICENSE new file mode 100644 index 0000000..86af9c3 --- /dev/null +++ b/screen_protector/LICENSE @@ -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. diff --git a/no_screenshot/android/.gitignore b/screen_protector/android/.gitignore similarity index 95% rename from no_screenshot/android/.gitignore rename to screen_protector/android/.gitignore index 161bdcd..c6cbe56 100644 --- a/no_screenshot/android/.gitignore +++ b/screen_protector/android/.gitignore @@ -6,4 +6,3 @@ .DS_Store /build /captures -.cxx diff --git a/screen_protector/android/build.gradle b/screen_protector/android/build.gradle new file mode 100644 index 0000000..a94baa1 --- /dev/null +++ b/screen_protector/android/build.gradle @@ -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' + } +} \ No newline at end of file diff --git a/screen_protector/android/gradle/wrapper/gradle-wrapper.jar b/screen_protector/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..980502d Binary files /dev/null and b/screen_protector/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/no_screenshot/android/gradle/wrapper/gradle-wrapper.properties b/screen_protector/android/gradle/wrapper/gradle-wrapper.properties similarity index 74% rename from no_screenshot/android/gradle/wrapper/gradle-wrapper.properties rename to screen_protector/android/gradle/wrapper/gradle-wrapper.properties index 41dfb87..b82aa23 100644 --- a/no_screenshot/android/gradle/wrapper/gradle-wrapper.properties +++ b/screen_protector/android/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/no_screenshot/android/gradlew b/screen_protector/android/gradlew similarity index 84% rename from no_screenshot/android/gradlew rename to screen_protector/android/gradlew index 1b6c787..faf9300 100755 --- a/no_screenshot/android/gradlew +++ b/screen_protector/android/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -80,13 +82,11 @@ do esac done -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" +# This is normally unused +# shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -133,22 +133,29 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -193,11 +200,15 @@ if "$cygwin" || "$msys" ; then done fi -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ @@ -205,6 +216,12 @@ set -- \ org.gradle.wrapper.GradleWrapperMain \ "$@" +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. diff --git a/no_screenshot/android/gradlew.bat b/screen_protector/android/gradlew.bat similarity index 81% rename from no_screenshot/android/gradlew.bat rename to screen_protector/android/gradlew.bat index ac1b06f..9d21a21 100644 --- a/no_screenshot/android/gradlew.bat +++ b/screen_protector/android/gradlew.bat @@ -1,89 +1,94 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/screen_protector/android/settings.gradle b/screen_protector/android/settings.gradle new file mode 100644 index 0000000..c7c48ca --- /dev/null +++ b/screen_protector/android/settings.gradle @@ -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' diff --git a/no_screenshot/android/src/main/AndroidManifest.xml b/screen_protector/android/src/main/AndroidManifest.xml similarity index 60% rename from no_screenshot/android/src/main/AndroidManifest.xml rename to screen_protector/android/src/main/AndroidManifest.xml index a2f47b6..017403f 100644 --- a/no_screenshot/android/src/main/AndroidManifest.xml +++ b/screen_protector/android/src/main/AndroidManifest.xml @@ -1,2 +1,3 @@ - + diff --git a/screen_protector/android/src/main/kotlin/com/prongbang/screen_protector/ScreenProtectorPlugin.kt b/screen_protector/android/src/main/kotlin/com/prongbang/screen_protector/ScreenProtectorPlugin.kt new file mode 100644 index 0000000..b854d53 --- /dev/null +++ b/screen_protector/android/src/main/kotlin/com/prongbang/screen_protector/ScreenProtectorPlugin.kt @@ -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() {} + +} \ No newline at end of file diff --git a/no_screenshot/ios/.gitignore b/screen_protector/ios/.gitignore similarity index 100% rename from no_screenshot/ios/.gitignore rename to screen_protector/ios/.gitignore diff --git a/no_screenshot/ios/Assets/.gitkeep b/screen_protector/ios/Assets/.gitkeep similarity index 100% rename from no_screenshot/ios/Assets/.gitkeep rename to screen_protector/ios/Assets/.gitkeep diff --git a/screen_protector/ios/Classes/FlutterRootViewResolver.swift b/screen_protector/ios/Classes/FlutterRootViewResolver.swift new file mode 100644 index 0000000..a3233fb --- /dev/null +++ b/screen_protector/ios/Classes/FlutterRootViewResolver.swift @@ -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)") + } +} diff --git a/screen_protector/ios/Classes/ScreenProtectorPlugin.h b/screen_protector/ios/Classes/ScreenProtectorPlugin.h new file mode 100644 index 0000000..df91770 --- /dev/null +++ b/screen_protector/ios/Classes/ScreenProtectorPlugin.h @@ -0,0 +1,4 @@ +#import + +@interface ScreenProtectorPlugin : NSObject +@end diff --git a/no_screenshot/ios/Classes/NoScreenshotPlugin.m b/screen_protector/ios/Classes/ScreenProtectorPlugin.m similarity index 53% rename from no_screenshot/ios/Classes/NoScreenshotPlugin.m rename to screen_protector/ios/Classes/ScreenProtectorPlugin.m index f639229..26cff62 100644 --- a/no_screenshot/ios/Classes/NoScreenshotPlugin.m +++ b/screen_protector/ios/Classes/ScreenProtectorPlugin.m @@ -1,15 +1,15 @@ -#import "NoScreenshotPlugin.h" -#if __has_include() -#import +#import "ScreenProtectorPlugin.h" +#if __has_include() +#import #else // Support project import fallback if the generated compatibility header // is not copied when this plugin is created as a library. // https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816 -#import "no_screenshot-Swift.h" +#import "screen_protector-Swift.h" #endif -@implementation NoScreenshotPlugin +@implementation ScreenProtectorPlugin + (void)registerWithRegistrar:(NSObject*)registrar { - [IOSNoScreenshotPlugin registerWithRegistrar:registrar]; + [SwiftScreenProtectorPlugin registerWithRegistrar:registrar]; } @end diff --git a/screen_protector/ios/Classes/SwiftScreenProtectorPlugin.swift b/screen_protector/ios/Classes/SwiftScreenProtectorPlugin.swift new file mode 100644 index 0000000..6f98412 --- /dev/null +++ b/screen_protector/ios/Classes/SwiftScreenProtectorPlugin.swift @@ -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 + + 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 + } +} diff --git a/screen_protector/ios/screen_protector.podspec b/screen_protector/ios/screen_protector.podspec new file mode 100644 index 0000000..45afdbe --- /dev/null +++ b/screen_protector/ios/screen_protector.podspec @@ -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 diff --git a/screen_protector/lib/extension/color_extension.dart b/screen_protector/lib/extension/color_extension.dart new file mode 100644 index 0000000..ad5e226 --- /dev/null +++ b/screen_protector/lib/extension/color_extension.dart @@ -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')}'; +} diff --git a/screen_protector/lib/lifecycle/legacy_lifecycle_state.dart b/screen_protector/lib/lifecycle/legacy_lifecycle_state.dart new file mode 100644 index 0000000..049ff6c --- /dev/null +++ b/screen_protector/lib/lifecycle/legacy_lifecycle_state.dart @@ -0,0 +1,68 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +abstract class LegacyLifecycleState extends State + 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"); + } + } +} diff --git a/screen_protector/lib/lifecycle/lifecycle_state.dart b/screen_protector/lib/lifecycle/lifecycle_state.dart new file mode 100644 index 0000000..22edef2 --- /dev/null +++ b/screen_protector/lib/lifecycle/lifecycle_state.dart @@ -0,0 +1,135 @@ +import 'dart:ui'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +abstract class LifecycleState extends State + 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 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"); + } + } +} diff --git a/screen_protector/lib/screen_protector.dart b/screen_protector/lib/screen_protector.dart new file mode 100644 index 0000000..ee2e1cd --- /dev/null +++ b/screen_protector/lib/screen_protector.dart @@ -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 _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 protectDataLeakageOn() async { + return await _channel.invokeMethod('protectDataLeakageOn'); + } + + /// Supported for Android and iOS. + static Future protectDataLeakageOff() async { + return await _channel.invokeMethod('protectDataLeakageOff'); + } + + /// Supported for iOS only, do nothing when run on Android. + static Future protectDataLeakageWithBlur() async { + return await _channel.invokeMethod('protectDataLeakageWithBlur'); + } + + /// Supported for iOS only, do nothing when run on Android. + static Future protectDataLeakageWithBlurOff() async { + return await _channel.invokeMethod('protectDataLeakageWithBlurOff'); + } + + /// Supported for iOS only, do nothing when run on Android. + static Future protectDataLeakageWithImage(String imageName) async { + return await _channel.invokeMethod('protectDataLeakageWithImage', { + 'name': imageName, + }); + } + + /// Supported for iOS only, do nothing when run on Android. + static Future protectDataLeakageWithImageOff() async { + return await _channel.invokeMethod('protectDataLeakageWithImageOff'); + } + + /// Supported for iOS only, do nothing when run on Android. + static Future protectDataLeakageWithColor(Color color) async { + return await _channel.invokeMethod('protectDataLeakageWithColor', { + 'hexColor': color.toHex(), + }); + } + + /// Supported for iOS only, do nothing when run on Android. + static Future protectDataLeakageWithColorOff() async { + return await _channel.invokeMethod('protectDataLeakageWithColorOff'); + } + + /// Supported for Android and iOS. + static Future preventScreenshotOn() async { + return await _channel.invokeMethod('preventScreenshotOn'); + } + + /// Supported for Android and iOS. + static Future preventScreenshotOff() async { + return await _channel.invokeMethod('preventScreenshotOff'); + } + + /// Supported for iOS only, do nothing when run on Android. + static Future isRecording() async { + return await _channel.invokeMethod('isRecording'); + } +} diff --git a/screen_protector/pubspec.yaml b/screen_protector/pubspec.yaml new file mode 100644 index 0000000..2653fe0 --- /dev/null +++ b/screen_protector/pubspec.yaml @@ -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 diff --git a/screen_protector/test/screen_protector_test.dart b/screen_protector/test/screen_protector_test.dart new file mode 100644 index 0000000..a1b9692 --- /dev/null +++ b/screen_protector/test/screen_protector_test.dart @@ -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'); + }); +}