diff --git a/.vscode/launch.json b/.vscode/launch.json index 995076f..a47cb42 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -8,7 +8,8 @@ "name": "no_screenshot", "request": "launch", "type": "dart", - "program": "example/lib/main.dart" + "program": "example/lib/main.dart", + "args": ["--verbose"] } ] } \ No newline at end of file diff --git a/android/src/main/kotlin/com/flutterplaza/no_screenshot/NoScreenshotPlugin.kt b/android/src/main/kotlin/com/flutterplaza/no_screenshot/NoScreenshotPlugin.kt index 57ee82a..bebfd02 100644 --- a/android/src/main/kotlin/com/flutterplaza/no_screenshot/NoScreenshotPlugin.kt +++ b/android/src/main/kotlin/com/flutterplaza/no_screenshot/NoScreenshotPlugin.kt @@ -2,96 +2,243 @@ package com.flutterplaza.no_screenshot import android.app.Activity import android.content.Context +import android.content.SharedPreferences +import android.database.ContentObserver +import android.net.Uri +import android.os.Handler +import android.os.Looper +import android.provider.MediaStore +import android.util.Log import androidx.annotation.NonNull - import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import io.flutter.plugin.common.MethodChannel.MethodCallHandler -import io.flutter.plugin.common.MethodChannel.Result import android.view.WindowManager.LayoutParams 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.MethodChannel +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 SCREENSHOT_METHOD_CHANNEL = "com.flutterplaza.no_screenshot_methods" +const val SCREENSHOT_EVENT_CHANNEL = "com.flutterplaza.no_screenshot_streams" + /** NoScreenshotPlugin */ -class NoScreenshotPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { - /// The MethodChannel that will the communication between Flutter and native Android - /// - /// This local reference serves to register the plugin with the Flutter Engine and unregister it - /// when the Flutter Engine is detached from the Activity - private lateinit var channel: MethodChannel +class NoScreenshotPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware, + EventChannel.StreamHandler { + private lateinit var methodChannel: MethodChannel private lateinit var context: Context - private lateinit var activity: Activity + private var activity: Activity? = null + private lateinit var preferences: SharedPreferences + private var screenshotObserver: ContentObserver? = null + private lateinit var eventChannel: EventChannel + private val handler = Handler(Looper.getMainLooper()) + private var eventSink: EventChannel.EventSink? = null + + private var lastSharedPreferencesState: String = "" + private fun convertMapToJsonString(map: Map): String { + return JSONObject(map).toString() + } + + private fun getCurrentSharedPreferencesState( + screenshotData: String + ): String { + val map = mapOf( + PREF_KEY_SCREENSHOT to preferences.getBoolean(PREF_KEY_SCREENSHOT, false), + SCREENSHOT_PATH to screenshotData, + SCREENSHOT_TAKEN to screenshotData.isNotEmpty() + ) + val jsonString = convertMapToJsonString(map) + if (lastSharedPreferencesState != jsonString) { + hasSharedPreferencesChanged = true + } + return jsonString + } + + private var hasSharedPreferencesChanged: Boolean = false + override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { - channel = - MethodChannel(flutterPluginBinding.binaryMessenger, "com.flutterplaza.no_screenshot") - channel.setMethodCallHandler(this) + methodChannel = + MethodChannel( + flutterPluginBinding.binaryMessenger, + SCREENSHOT_METHOD_CHANNEL + ) + eventChannel = + EventChannel( + flutterPluginBinding.binaryMessenger, + SCREENSHOT_EVENT_CHANNEL + ) + methodChannel.setMethodCallHandler(this) + eventChannel.setStreamHandler(this) context = flutterPluginBinding.applicationContext + preferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + initScreenshotObserver() + } + + private val screenshotStream = object : Runnable { + override fun run() { + if (hasSharedPreferencesChanged) { + // SharedPreferences values have changed, proceed with logic + eventSink?.success(lastSharedPreferencesState) + hasSharedPreferencesChanged = false + } + // Continue posting this runnable to keep checking periodically + handler.postDelayed(this, 1000) + } + } + + + private fun initScreenshotObserver() { + screenshotObserver = object : ContentObserver(Handler()) { + override fun onChange(selfChange: Boolean, uri: Uri?) { + super.onChange(selfChange, uri) + if (uri != null && uri.toString() + .contains(MediaStore.Images.Media.EXTERNAL_CONTENT_URI.toString()) + ) { + Log.d("ScreenshotProtection", "Screenshot detected") + val screenshotPath = uri.path + if (screenshotPath != null) { + lastSharedPreferencesState = + getCurrentSharedPreferencesState(screenshotPath) + } + } + } + } + } + + 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) } } - override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) = + override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: MethodChannel.Result) { when (call.method) { SCREENSHOT_OFF_CONST -> { - val value: Boolean = screenshotOff() + val value = screenshotOff() + lastSharedPreferencesState = + getCurrentSharedPreferencesState("") result.success(value) } + SCREENSHOT_ON_CONST -> { val value = screenshotOn() - + lastSharedPreferencesState = + getCurrentSharedPreferencesState("") result.success(value) } + TOGGLE_SCREENSHOT_CONST -> { - val flags: Int = activity.window.attributes.flags - if ((flags and LayoutParams.FLAG_SECURE) != 0) { + val flags = activity?.window?.attributes?.flags + if ((flags?.and(LayoutParams.FLAG_SECURE)) != 0) { screenshotOn() } else { screenshotOff() } + lastSharedPreferencesState = + getCurrentSharedPreferencesState("") result.success(true) } - else -> { - result.notImplemented() + + START_SCREENSHOT_LISTENING_CONST -> { + startListening() + result.success("Listening started") + } + + STOP_SCREENSHOT_LISTENING_CONST -> { + stopListening() + lastSharedPreferencesState = + getCurrentSharedPreferencesState("") + result.success("Listening stopped") } + + else -> result.notImplemented() } + } private fun screenshotOff(): Boolean { - try { - activity.window.addFlags(LayoutParams.FLAG_SECURE) - return true + return try { + activity?.window?.addFlags(LayoutParams.FLAG_SECURE) + saveScreenshotState(true) + true } catch (e: Exception) { - return false + false } } - private fun screenshotOn() : Boolean{ - try { - activity.window.clearFlags(LayoutParams.FLAG_SECURE) - return true + private fun screenshotOn(): Boolean { + return try { + activity?.window?.clearFlags(LayoutParams.FLAG_SECURE) + saveScreenshotState(false) + true } catch (e: Exception) { - return false + false + } + } + + + private fun saveScreenshotState(isSecure: Boolean) { + preferences.edit().putBoolean(PREF_KEY_SCREENSHOT, isSecure).apply() + } + + + private fun restoreScreenshotState() { + // Restore screenshot state + val isSecure = preferences.getBoolean(PREF_KEY_SCREENSHOT, false) + if (isSecure) { + screenshotOff() + } else { + screenshotOn() } } override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { - channel.setMethodCallHandler(null) + methodChannel.setMethodCallHandler(null) + screenshotObserver?.let { + context.contentResolver.unregisterContentObserver(it) + } } override fun onAttachedToActivity(binding: ActivityPluginBinding) { activity = binding.activity + restoreScreenshotState() } - override fun onDetachedFromActivityForConfigChanges() {} + + override fun onDetachedFromActivityForConfigChanges() {} override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { activity = binding.activity + restoreScreenshotState() + } + override fun onDetachedFromActivity() {} + override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { + eventSink = events + handler.postDelayed(screenshotStream, 1000) } - override fun onDetachedFromActivity() {} + override fun onCancel(arguments: Any?) { + handler.removeCallbacks(screenshotStream) + eventSink = null + } } diff --git a/example/android/app/src/debug/AndroidManifest.xml b/example/android/app/src/debug/AndroidManifest.xml index 391670f..ee4f889 100644 --- a/example/android/app/src/debug/AndroidManifest.xml +++ b/example/android/app/src/debug/AndroidManifest.xml @@ -5,4 +5,7 @@ to allow setting breakpoints, to provide hot reload, etc. --> + + + diff --git a/example/android/app/src/main/kotlin/com/flutterplaza/no_screenshot_example/MainActivity.kt b/example/android/app/src/main/kotlin/com/flutterplaza/no_screenshot_example/MainActivity.kt index 97014af..348b5e9 100644 --- a/example/android/app/src/main/kotlin/com/flutterplaza/no_screenshot_example/MainActivity.kt +++ b/example/android/app/src/main/kotlin/com/flutterplaza/no_screenshot_example/MainActivity.kt @@ -1,6 +1,14 @@ package com.flutterplaza.no_screenshot_example +import com.flutterplaza.no_screenshot.NoScreenshotPlugin import io.flutter.embedding.android.FlutterActivity +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugins.GeneratedPluginRegistrant -class MainActivity: FlutterActivity() { +class MainActivity : FlutterActivity() { + override fun configureFlutterEngine(flutterEngine: FlutterEngine) { + GeneratedPluginRegistrant.registerWith(flutterEngine) + // Ensure your plugin registration is included if it's not auto-registered + flutterEngine.plugins.add(NoScreenshotPlugin()) + } } diff --git a/example/lib/main.dart b/example/lib/main.dart index 941f87f..db4a03a 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:no_screenshot/no_screenshot.dart'; +import 'package:no_screenshot/screenshot_snapshot.dart'; void main() { runApp(const MyApp()); @@ -14,10 +15,21 @@ class MyApp extends StatefulWidget { class _MyAppState extends State { final _noScreenshot = NoScreenshot.instance; + bool _isListeningToScreenshotSnapshot = false; + ScreenshotSnapshot _latestValue = ScreenshotSnapshot( + isScreenshotProtectionOn: false, + wasScreenshotTaken: false, + screenshotPath: '', + ); @override void initState() { super.initState(); + _noScreenshot.screenshotStream.listen((value) { + setState(() { + _latestValue = value; + }); + }); } @override @@ -25,35 +37,60 @@ class _MyAppState extends State { return MaterialApp( home: Scaffold( appBar: AppBar( - title: const Text('NoScreenShot Plugin app'), + title: const Text('No Screenshot Plugin Example'), ), body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - ElevatedButton( - child: const Text('Press to toggle screenshot'), - onPressed: () async { - final result = await _noScreenshot.toggleScreenshot(); - debugPrint(result.toString()); - }, - ), - ElevatedButton( - child: const Text('Press to turn off screenshot'), - onPressed: () async { - final result = await _noScreenshot.screenshotOff(); - debugPrint(result.toString()); - }, - ), - ElevatedButton( - child: const Text('Press to turn on screenshot'), - onPressed: () async { - final result = await _noScreenshot.screenshotOn(); - debugPrint(result.toString()); - }, - ), - ], - )), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const SizedBox(height: 20), + ElevatedButton( + onPressed: () async { + await _noScreenshot.startScreenshotListening(); + setState(() { + _isListeningToScreenshotSnapshot = true; + }); + }, + child: const Text('Start Listening'), + ), + ElevatedButton( + onPressed: () async { + await _noScreenshot.stopScreenshotListening(); + setState(() { + _isListeningToScreenshotSnapshot = false; + }); + }, + child: const Text('Stop Listening'), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Text( + 'ScreenshotSnapshot Streaming is $_isListeningToScreenshotSnapshot\n\n Values: ${_latestValue.toString()}'), + ), + ElevatedButton( + onPressed: () async { + bool result = await _noScreenshot.screenshotOff(); + debugPrint('Screenshot Off: $result'); + }, + child: const Text('Disable Screenshot'), + ), + ElevatedButton( + onPressed: () async { + bool result = await _noScreenshot.screenshotOn(); + debugPrint('Enable Screenshot: $result'); + }, + child: const Text('Enable Screenshot'), + ), + ElevatedButton( + onPressed: () async { + bool result = await _noScreenshot.toggleScreenshot(); + debugPrint('Toggle Screenshot: $result'); + }, + child: const Text('Toggle Screenshot'), + ), + ], + ), + ), ), ); } diff --git a/example/pubspec.lock b/example/pubspec.lock index cf2d64e..7ee1203 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -45,10 +45,10 @@ packages: dependency: "direct main" description: name: cupertino_icons - sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "1.0.8" fake_async: dependency: transitive description: @@ -66,10 +66,10 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c + sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.3" flutter_test: dependency: "direct dev" description: flutter @@ -103,10 +103,10 @@ packages: dependency: transitive description: name: lints - sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.1.1" matcher: dependency: transitive description: @@ -137,7 +137,7 @@ packages: path: ".." relative: true source: path - version: "0.0.1+6" + version: "0.2.0" path: dependency: transitive description: diff --git a/lib/constants.dart b/lib/constants.dart index e9d6791..f2e5e59 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -1,3 +1,7 @@ const screenShotOnConst = "screenshotOn"; const screenShotOffConst = "screenshotOff"; const toggleScreenShotConst = "toggleScreenshot"; +const startScreenshotListeningConst = 'startScreenshotListening'; +const stopScreenshotListeningConst = 'stopScreenshotListening'; +const screenshotMethodChannel = "com.flutterplaza.no_screenshot_methods"; +const screenshotEventChannel = "com.flutterplaza.no_screenshot_streams"; diff --git a/lib/no_screenshot.dart b/lib/no_screenshot.dart index 356b878..7acfd4a 100644 --- a/lib/no_screenshot.dart +++ b/lib/no_screenshot.dart @@ -1,10 +1,13 @@ +import 'package:no_screenshot/screenshot_snapshot.dart'; + import 'no_screenshot_platform_interface.dart'; class NoScreenshot implements NoScreenshotPlatform { final _instancePlatform = NoScreenshotPlatform.instance; NoScreenshot._(); - @Deprecated("Using this may cause issue\nUse instance directly\ne.g: 'NoScreenshot.instance.screenshotOff()'") + @Deprecated( + "Using this may cause issue\nUse instance directly\ne.g: 'NoScreenshot.instance.screenshotOff()'") NoScreenshot(); /// Made `NoScreenshot` class a singleton @@ -30,4 +33,20 @@ class NoScreenshot implements NoScreenshotPlatform { Future toggleScreenshot() { return _instancePlatform.toggleScreenshot(); } + + /// Stream to listen to the increment value + @override + Stream get screenshotStream { + return _instancePlatform.screenshotStream; + } + + @override + Future startScreenshotListening() { + return _instancePlatform.startScreenshotListening(); + } + + @override + Future stopScreenshotListening() { + return _instancePlatform.stopScreenshotListening(); + } } diff --git a/lib/no_screenshot_method_channel.dart b/lib/no_screenshot_method_channel.dart index d98f006..6633075 100644 --- a/lib/no_screenshot_method_channel.dart +++ b/lib/no_screenshot_method_channel.dart @@ -1,6 +1,9 @@ +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'; @@ -8,7 +11,15 @@ import 'no_screenshot_platform_interface.dart'; class MethodChannelNoScreenshot extends NoScreenshotPlatform { /// The method channel used to interact with the native platform. @visibleForTesting - final methodChannel = const MethodChannel('com.flutterplaza.no_screenshot'); + final methodChannel = const MethodChannel(screenshotMethodChannel); + @visibleForTesting + final eventChannel = const EventChannel(screenshotEventChannel); + + @override + Stream get screenshotStream { + return eventChannel.receiveBroadcastStream().map((event) => + ScreenshotSnapshot.fromMap(jsonDecode(event) as Map)); + } @override Future toggleScreenshot() async { @@ -28,4 +39,14 @@ class MethodChannelNoScreenshot extends NoScreenshotPlatform { final result = await methodChannel.invokeMethod(screenShotOnConst); return result ?? false; } + + @override + Future startScreenshotListening() { + return methodChannel.invokeMethod(startScreenshotListeningConst); + } + + @override + Future stopScreenshotListening() { + return methodChannel.invokeMethod(stopScreenshotListeningConst); + } } diff --git a/lib/no_screenshot_platform_interface.dart b/lib/no_screenshot_platform_interface.dart index 09d6ac5..0b21f78 100644 --- a/lib/no_screenshot_platform_interface.dart +++ b/lib/no_screenshot_platform_interface.dart @@ -1,3 +1,4 @@ +import 'package:no_screenshot/screenshot_snapshot.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'no_screenshot_method_channel.dart'; @@ -44,4 +45,17 @@ abstract class NoScreenshotPlatform extends PlatformInterface { Future toggleScreenshot() { throw UnimplementedError('toggleScreenshot() has not been implemented.'); } + /// Stream to listen to the increment value + + Stream get screenshotStream { + throw UnimplementedError('incrementStream has not been implemented.'); + } + + Future startScreenshotListening() { + throw UnimplementedError('startScreenshotListening has not been implemented.'); + } + + Future stopScreenshotListening() { + throw UnimplementedError('stopScreenshotListening has not been implemented.'); + } } diff --git a/lib/screenshot_snapshot.dart b/lib/screenshot_snapshot.dart new file mode 100644 index 0000000..6aa20e8 --- /dev/null +++ b/lib/screenshot_snapshot.dart @@ -0,0 +1,49 @@ +class ScreenshotSnapshot { + final String screenshotPath; + final bool isScreenshotProtectionOn; + final bool wasScreenshotTaken; + + ScreenshotSnapshot({ + required this.screenshotPath, + required this.isScreenshotProtectionOn, + required this.wasScreenshotTaken, + }); + + 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, + ); + } + + Map toMap() { + return { + 'screenshot_path': screenshotPath, + 'is_screenshot_on': isScreenshotProtectionOn, + 'was_screenshot_taken': wasScreenshotTaken, + }; + } + + @override + String toString() { + return 'ScreenshotSnapshot(\nscreenshotPath: $screenshotPath, \nisScreenshotProtectionOn: $isScreenshotProtectionOn, \nwasScreenshotTaken: $wasScreenshotTaken\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; + } + + @override + int get hashCode { + return screenshotPath.hashCode ^ + isScreenshotProtectionOn.hashCode ^ + wasScreenshotTaken.hashCode; + } +} diff --git a/test/no_screenshot_test.dart b/test/no_screenshot_test.dart index 084e66b..3ed6b73 100644 --- a/test/no_screenshot_test.dart +++ b/test/no_screenshot_test.dart @@ -2,6 +2,7 @@ import 'package:flutter/cupertino.dart'; 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:plugin_platform_interface/plugin_platform_interface.dart'; class MockNoScreenshotPlatform @@ -24,6 +25,19 @@ class MockNoScreenshotPlatform // Mock implementation or return a fixed value return Future.value(true); } + + @override + Stream get screenshotStream => const Stream.empty(); + + @override + Future startScreenshotListening() { + return Future.value(); + } + + @override + Future stopScreenshotListening() { + return Future.value(); + } } Future main() async {