adds no_screenshot and restart_app

This commit is contained in:
otsmr 2025-12-27 15:07:37 +01:00
parent b0001d31d9
commit 7930d97270
45 changed files with 1956 additions and 1 deletions

View file

@ -9,9 +9,11 @@ introduction_screen: 4a90e557630b28834479ed9c64a9d2d0185d8e48
libsignal_protocol_dart: 618f0c0b49534245a640a31d204265440cbac9ee
lottie: 4f1a5a52bdf1e1c1e12fa97c96174dcb05419e19
mutex: 84ca903a3ac863735e3228c75a212133621f680f
no_screenshot: 8e19a8d0e30bd1d5000425cabac7ef3e3da4d5ea
optional: 71c638891ce4f2aff35c7387727989f31f9d877d
photo_view: a13ca2fc387a3fb1276126959e092c44d0029987
pointycastle: bbd8569f68a7fccbdf0b92d0b44a9219c126c8dd
qr: ff808bb3f354e6a7029ec953cbe0144a42021db6
qr_flutter: d5e7206396105d643113618290bbcc755d05f492
restart_app: 12339f63bf8e9631e619c4f9f6b4e013fa324715
x25519: ecb1d357714537bba6e276ef45f093846d4beaee

View file

@ -50,3 +50,9 @@ hand_signature:
flutter_sharing_intent:
git: https://github.com/bhagat-techind/flutter_sharing_intent.git
restart_app:
git: https://github.com/gabrimatic/restart_app
no_screenshot:
git: https://github.com/FlutterPlaza/no_screenshot.git

26
no_screenshot/LICENSE Normal file
View file

@ -0,0 +1,26 @@
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.

9
no_screenshot/android/.gitignore vendored Normal file
View file

@ -0,0 +1,9 @@
*.iml
.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures
.cxx

View file

@ -0,0 +1,47 @@
group 'com.flutterplaza.no_screenshot'
version '1.0-SNAPSHOT'
buildscript {
ext.kotlin_version = '1.6.10'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.1.3'
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"
compileSdkVersion 31
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
defaultConfig {
minSdkVersion 16
}
}

Binary file not shown.

View file

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

234
no_screenshot/android/gradlew vendored Executable file
View file

@ -0,0 +1,234 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# 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
#
# https://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.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# 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
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
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"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
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.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
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.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

89
no_screenshot/android/gradlew.bat vendored Normal file
View file

@ -0,0 +1,89 @@
@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

View file

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

View file

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

View file

@ -0,0 +1,199 @@
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 android.view.WindowManager.LayoutParams
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 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"
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 lateinit var preferences: SharedPreferences
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
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext
preferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
methodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, SCREENSHOT_METHOD_CHANNEL)
methodChannel.setMethodCallHandler(this)
eventChannel = EventChannel(flutterPluginBinding.binaryMessenger, SCREENSHOT_EVENT_CHANNEL)
eventChannel.setStreamHandler(this)
initScreenshotObserver()
}
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
methodChannel.setMethodCallHandler(null)
screenshotObserver?.let { context.contentResolver.unregisterContentObserver(it) }
}
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activity = binding.activity
restoreScreenshotState()
}
override fun onDetachedFromActivityForConfigChanges() {}
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
activity = binding.activity
restoreScreenshotState()
}
override fun onDetachedFromActivity() {}
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("") })
}
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 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")
updateSharedPreferencesState(it.path ?: "")
}
}
}
}
}
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) {
preferences.edit().putBoolean(PREF_KEY_SCREENSHOT, isSecure).apply()
}
private fun restoreScreenshotState() {
val isSecure = preferences.getBoolean(PREF_KEY_SCREENSHOT, false)
if (isSecure) {
screenshotOff()
} else {
screenshotOn()
}
}
private fun updateSharedPreferencesState(screenshotData: String) {
val jsonString = convertMapToJsonString(mapOf(
PREF_KEY_SCREENSHOT to preferences.getBoolean(PREF_KEY_SCREENSHOT, false),
SCREENSHOT_PATH to screenshotData,
SCREENSHOT_TAKEN to screenshotData.isNotEmpty()
))
if (lastSharedPreferencesState != jsonString) {
hasSharedPreferencesChanged = true
lastSharedPreferencesState = jsonString
}
}
private fun convertMapToJsonString(map: Map<String, Any>): String {
return JSONObject(map).toString()
}
private val screenshotStream = object : Runnable {
override fun run() {
if (hasSharedPreferencesChanged) {
eventSink?.success(lastSharedPreferencesState)
hasSharedPreferencesChanged = false
}
handler.postDelayed(this, 1000)
}
}
}

View file

@ -0,0 +1,27 @@
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)
}
}

38
no_screenshot/ios/.gitignore vendored Normal file
View file

@ -0,0 +1,38 @@
.idea/
.vagrant/
.sconsign.dblite
.svn/
.DS_Store
*.swp
profile
DerivedData/
build/
GeneratedPluginRegistrant.h
GeneratedPluginRegistrant.m
.generated/
*.pbxuser
*.mode1v3
*.mode2v3
*.perspectivev3
!default.pbxuser
!default.mode1v3
!default.mode2v3
!default.perspectivev3
xcuserdata
*.moved-aside
*.pyc
*sync/
Icon?
.tags*
/Flutter/Generated.xcconfig
/Flutter/ephemeral/
/Flutter/flutter_export_environment.sh

View file

View file

@ -0,0 +1,182 @@
import Flutter
import UIKit
import ScreenProtectorKit
public class IOSNoScreenshotPlugin: NSObject, FlutterPlugin, FlutterStreamHandler {
private var screenProtectorKit: ScreenProtectorKit? = 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 static let ENABLESCREENSHOT = false
private static let DISABLESCREENSHOT = true
private static let preventScreenShotKey = "preventScreenShot"
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"
init(screenProtectorKit: ScreenProtectorKit) {
self.screenProtectorKit = screenProtectorKit
super.init()
// Restore the saved state from UserDefaults
var fetchVal = UserDefaults.standard.bool(forKey: IOSNoScreenshotPlugin.preventScreenShotKey)
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 window = UIApplication.shared.delegate?.window
let screenProtectorKit = ScreenProtectorKit(window: window as? UIWindow)
screenProtectorKit.configurePreventionScreenshot()
let instance = IOSNoScreenshotPlugin(screenProtectorKit: screenProtectorKit)
registrar.addMethodCallDelegate(instance, channel: methodChannel!)
eventChannel?.setStreamHandler(instance)
registrar.addApplicationDelegate(instance)
}
public func applicationWillResignActive(_ application: UIApplication) {
persistState()
}
public func applicationDidBecomeActive(_ application: UIApplication) {
fetchPersistedState()
}
public func applicationWillEnterForeground(_ application: UIApplication) {
fetchPersistedState()
}
public func applicationDidEnterBackground(_ application: UIApplication) {
persistState()
}
public func applicationWillTerminate(_ application: UIApplication) {
persistState()
}
func persistState() {
// Persist the state when changed
UserDefaults.standard.set(IOSNoScreenshotPlugin.preventScreenShot, forKey: IOSNoScreenshotPlugin.preventScreenShotKey)
print("Persisted state: \(IOSNoScreenshotPlugin.preventScreenShot)")
updateSharedPreferencesState("")
}
func fetchPersistedState() {
// Restore the saved state from UserDefaults
var fetchVal = UserDefaults.standard.bool(forKey: IOSNoScreenshotPlugin.preventScreenShotKey) ? IOSNoScreenshotPlugin.DISABLESCREENSHOT :IOSNoScreenshotPlugin.ENABLESCREENSHOT
updateScreenshotState(isScreenshotBlocked: fetchVal)
print("Fetched state: \(IOSNoScreenshotPlugin.preventScreenShot)")
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "screenshotOff":
shotOff()
result(true)
case "screenshotOn":
shotOn()
result(true)
case "toggleScreenshot":
IOSNoScreenshotPlugin.preventScreenShot ? shotOn(): shotOff()
result(true)
case "startScreenshotListening":
startListening()
result("Listening started")
case "stopScreenshotListening":
stopListening()
result("Listening stopped")
default:
result(FlutterMethodNotImplemented)
}
}
private func shotOff() {
IOSNoScreenshotPlugin.preventScreenShot = IOSNoScreenshotPlugin.DISABLESCREENSHOT
screenProtectorKit?.enabledPreventScreenshot()
persistState()
}
private func shotOn() {
IOSNoScreenshotPlugin.preventScreenShot = IOSNoScreenshotPlugin.ENABLESCREENSHOT
screenProtectorKit?.disablePreventScreenshot()
persistState()
}
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()
}
@objc private func screenshotDetected() {
print("Screenshot detected")
updateSharedPreferencesState(IOSNoScreenshotPlugin.screenshotPathPlaceholder)
}
private func updateScreenshotState(isScreenshotBlocked: Bool) {
if isScreenshotBlocked {
screenProtectorKit?.enabledPreventScreenshot()
} else {
screenProtectorKit?.disablePreventScreenshot()
}
}
private func updateSharedPreferencesState(_ screenshotData: String) {
let map: [String: Any] = [
"is_screenshot_on": IOSNoScreenshotPlugin.preventScreenShot,
"screenshot_path": screenshotData,
"was_screenshot_taken": !screenshotData.isEmpty
]
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()
}
}
deinit {
screenProtectorKit?.removeAllObserver()
}
}

View file

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

View file

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

View file

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

View file

@ -0,0 +1,26 @@
#
# 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.3.2-beta.3'
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 = 'Classes/**/*'
s.dependency 'Flutter'
# Updated the dependency version to remove the wildcard and use a specific version range
s.dependency 'ScreenProtectorKit', '~> 1.3.1'
s.platform = :ios, '10.0'
# Flutter.framework does not contain a i386 slice.
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
# Updated swift_version to a single version as an array is not supported for this attribute
s.swift_version = "5.0"
end

View file

@ -0,0 +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";

View file

@ -0,0 +1,74 @@
import 'package:no_screenshot/screenshot_snapshot.dart';
import 'no_screenshot_platform_interface.dart';
/// A class that provides a platform-agnostic way to disable screenshots.
///
class NoScreenshot implements NoScreenshotPlatform {
final _instancePlatform = NoScreenshotPlatform.instance;
NoScreenshot._();
@Deprecated(
"Using this may cause issue\nUse instance directly\ne.g: 'NoScreenshot.instance.screenshotOff()'")
NoScreenshot();
static NoScreenshot get instance => NoScreenshot._();
/// Return `true` if screenshot capabilities has been
/// successfully disabled or is currently disabled and `false` otherwise.
/// throw `UnmimplementedError` if not implement
///
@override
Future<bool> screenshotOff() {
return _instancePlatform.screenshotOff();
}
/// Return `true` if screenshot capabilities has been
/// successfully enabled or is currently enabled and `false` otherwise.
/// throw `UnmimplementedError` if not implement
///
@override
Future<bool> screenshotOn() {
return _instancePlatform.screenshotOn();
}
/// Return `true` if screenshot capabilities has been
/// successfully toggle from it previous state and `false` if the attempt
/// to toggle failed.
/// throw `UnmimplementedError` if not implement
///
@override
Future<bool> toggleScreenshot() {
return _instancePlatform.toggleScreenshot();
}
/// Stream to screenshot activities [ScreenshotSnapshot]
///
@override
Stream<ScreenshotSnapshot> get screenshotStream {
return _instancePlatform.screenshotStream;
}
/// Start listening to screenshot activities
@override
Future<void> startScreenshotListening() {
return _instancePlatform.startScreenshotListening();
}
/// Stop listening to screenshot activities
@override
Future<void> stopScreenshotListening() {
return _instancePlatform.stopScreenshotListening();
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
other is NoScreenshot &&
runtimeType == other.runtimeType &&
_instancePlatform == other._instancePlatform;
}
@override
int get hashCode => _instancePlatform.hashCode;
}

View file

@ -0,0 +1,52 @@
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);
@override
Stream<ScreenshotSnapshot> get screenshotStream {
return eventChannel.receiveBroadcastStream().map((event) =>
ScreenshotSnapshot.fromMap(jsonDecode(event) as Map<String, dynamic>));
}
@override
Future<bool> toggleScreenshot() async {
final result =
await methodChannel.invokeMethod<bool>(toggleScreenShotConst);
return result ?? false;
}
@override
Future<bool> screenshotOff() async {
final result = await methodChannel.invokeMethod<bool>(screenShotOffConst);
return result ?? false;
}
@override
Future<bool> screenshotOn() async {
final result = await methodChannel.invokeMethod<bool>(screenShotOnConst);
return result ?? false;
}
@override
Future<void> startScreenshotListening() {
return methodChannel.invokeMethod<void>(startScreenshotListeningConst);
}
@override
Future<void> stopScreenshotListening() {
return methodChannel.invokeMethod<void>(stopScreenshotListeningConst);
}
}

View file

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

View file

@ -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<String, dynamic> map) {
return ScreenshotSnapshot(
screenshotPath: map['screenshot_path'] as String? ?? '',
isScreenshotProtectionOn: map['is_screenshot_on'] as bool? ?? false,
wasScreenshotTaken: map['was_screenshot_taken'] as bool? ?? false,
);
}
Map<String, dynamic> 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;
}
}

View file

@ -0,0 +1,32 @@
name: no_screenshot
description: Flutter plugin to enable, disable, toggle or stream screenshot activities in your application.
version: 0.3.2-beta.3
homepage: https://flutterplaza.com
repository: https://github.com/FlutterPlaza/no_screenshot/releases/tag/v0.3.2-beta.3
environment:
sdk: '>=3.0.0 <4.0.0'
flutter: ">=1.17.0"
dependencies:
flutter:
sdk: flutter
plugin_platform_interface: ^2.1.8
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^4.0.0
flutter_driver:
sdk: flutter
flutter:
plugin:
platforms:
android:
package: com.flutterplaza.no_screenshot
pluginClass: NoScreenshotPlugin
ios:
pluginClass: NoScreenshotPlugin
macos:
pluginClass: MacOSNoScreenshotPlugin

View file

@ -0,0 +1,166 @@
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:no_screenshot/constants.dart';
import 'package:no_screenshot/no_screenshot_method_channel.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
});
});
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);
});
test('toMap', () {
final snapshot = ScreenshotSnapshot(
screenshotPath: '/example/path',
isScreenshotProtectionOn: true,
wasScreenshotTaken: true,
);
final map = snapshot.toMap();
expect(map['screenshot_path'], '/example/path');
expect(map['is_screenshot_on'], true);
expect(map['was_screenshot_taken'], true);
});
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,
);
expect(snapshot1 == snapshot2, true);
expect(snapshot1 == snapshot3, 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('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\n)');
});
});
}

View file

@ -0,0 +1,75 @@
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';
class MockNoScreenshotPlatform extends NoScreenshotPlatform {
@override
Future<bool> screenshotOff() async {
return true;
}
@override
Future<bool> screenshotOn() async {
return true;
}
@override
Future<bool> toggleScreenshot() async {
return true;
}
@override
Stream<ScreenshotSnapshot> get screenshotStream {
return const Stream.empty();
}
@override
Future<void> startScreenshotListening() async {
return;
}
@override
Future<void> stopScreenshotListening() async {
return;
}
}
void main() {
final platform = MockNoScreenshotPlatform();
group('NoScreenshotPlatform', () {
test('default instance should be MethodChannelNoScreenshot', () {
expect(NoScreenshotPlatform.instance,
isInstanceOf<MethodChannelNoScreenshot>());
});
test('screenshotOff should return true when called', () async {
expect(await platform.screenshotOff(), isTrue);
});
test('screenshotOn should return true when called', () async {
expect(await platform.screenshotOn(), isTrue);
});
test('toggleScreenshot should return true when called', () async {
expect(await platform.toggleScreenshot(), isTrue);
});
test('screenshotStream should not throw UnimplementedError when accessed',
() {
expect(() => platform.screenshotStream, isNot(throwsUnimplementedError));
});
test(
'startScreenshotListening should not throw UnimplementedError when called',
() async {
expect(platform.startScreenshotListening(), completes);
});
test(
'stopScreenshotListening should not throw UnimplementedError when called',
() async {
expect(platform.stopScreenshotListening(), completes);
});
});
}

View file

@ -0,0 +1,95 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:no_screenshot/no_screenshot_platform_interface.dart';
import 'package:no_screenshot/no_screenshot_method_channel.dart';
import 'package:no_screenshot/screenshot_snapshot.dart';
import 'package:no_screenshot/no_screenshot.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
class MockNoScreenshotPlatform
with MockPlatformInterfaceMixin
implements NoScreenshotPlatform {
@override
Future<bool> screenshotOff() async {
// Mock implementation or return a fixed value
return Future.value(true);
}
@override
Future<bool> screenshotOn() async {
// Mock implementation or return a fixed value
return Future.value(true);
}
@override
Future<bool> toggleScreenshot() async {
// Mock implementation or return a fixed value
return Future.value(true);
}
@override
Stream<ScreenshotSnapshot> get screenshotStream => const Stream.empty();
@override
Future<void> startScreenshotListening() {
return Future.value();
}
@override
Future<void> stopScreenshotListening() {
return Future.value();
}
}
void main() {
final NoScreenshotPlatform initialPlatform = NoScreenshotPlatform.instance;
MockNoScreenshotPlatform fakePlatform = MockNoScreenshotPlatform();
setUp(() {
NoScreenshotPlatform.instance = fakePlatform;
});
tearDown(() {
NoScreenshotPlatform.instance = initialPlatform;
});
test('$MethodChannelNoScreenshot is the default instance', () {
expect(initialPlatform, isInstanceOf<MethodChannelNoScreenshot>());
});
test('NoScreenshot instance is a singleton', () {
final instance1 = NoScreenshot.instance;
final instance2 = NoScreenshot.instance;
expect(instance1, equals(instance2));
});
test('screenshotOn', () async {
expect(await NoScreenshot.instance.screenshotOn(), true);
});
test('screenshotOff', () async {
expect(await NoScreenshot.instance.screenshotOff(), true);
});
test('toggleScreenshot', () async {
expect(await NoScreenshot.instance.toggleScreenshot(), true);
});
test('screenshotStream', () async {
expect(NoScreenshot.instance.screenshotStream,
isInstanceOf<Stream<ScreenshotSnapshot>>());
});
test('startScreenshotListening', () async {
expect(NoScreenshot.instance.startScreenshotListening(), completes);
});
test('stopScreenshotListening', () async {
expect(NoScreenshot.instance.stopScreenshotListening(), completes);
});
test('NoScreenshot equality operator', () {
final instance1 = NoScreenshot.instance;
final instance2 = NoScreenshot.instance;
expect(instance1 == instance2, true, reason: 'Instances should be equal');
});
}

View file

@ -21,6 +21,8 @@ dependency_overrides:
path: ./dependencies/lottie
mutex:
path: ./dependencies/mutex
no_screenshot:
path: ./dependencies/no_screenshot
optional:
path: ./dependencies/optional
photo_view:
@ -31,5 +33,7 @@ dependency_overrides:
path: ./dependencies/qr
qr_flutter:
path: ./dependencies/qr_flutter
restart_app:
path: ./dependencies/restart_app
x25519:
path: ./dependencies/x25519

21
restart_app/LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Hossein Yousefpour
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

8
restart_app/android/.gitignore vendored Normal file
View file

@ -0,0 +1,8 @@
*.iml
.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures

View file

@ -0,0 +1,54 @@
group 'gabrimatic.info.restart'
version '1.0-SNAPSHOT'
buildscript {
ext.kotlin_version = '1.7.10'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.3.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
rootProject.allprojects {
repositories {
google()
mavenCentral()
}
}
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
android {
if (project.android.hasProperty("namespace")) {
namespace 'gabrimatic.info.restart'
}
compileSdk 34
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
defaultConfig {
minSdk 16
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
}

View file

@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true

View file

@ -0,0 +1,6 @@
#Mon Nov 21 13:39:27 CET 2022
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME

View file

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

View file

@ -0,0 +1,3 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="gabrimatic.info.restart">
</manifest>

View file

@ -0,0 +1,92 @@
package gabrimatic.info.restart
import android.app.Activity
import android.content.Context
import android.content.Intent
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.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
/**
* `RestartPlugin` class provides a method to restart a Flutter application in Android.
*
* It uses the Flutter platform channels to communicate with the Flutter code.
* Specifically, it uses a `MethodChannel` named 'restart' for this communication.
*
* The main functionality is provided by the `onMethodCall` method.
*/
class RestartPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
private lateinit var context: Context
private lateinit var channel: MethodChannel
private var activity: Activity? = null
/**
* Called when the plugin is attached to the Flutter engine.
*
* It initializes the `context` with the application context and
* sets this plugin instance as the handler for method calls from Flutter.
*/
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "restart")
channel.setMethodCallHandler(this)
}
/**
* Handles method calls from the Flutter code.
*
* If the method call is 'restartApp', it restarts the app and sends a successful result.
* For any other method call, it sends a 'not implemented' result.
*/
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
if (call.method == "restartApp") {
restartApp()
result.success("ok")
} else {
result.notImplemented()
}
}
/**
* Called when the plugin is detached from the Flutter engine.
*
* It removes the handler for method calls from Flutter.
*/
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
}
/**
* Restarts the application.
*/
private fun restartApp() {
activity?.let { currentActivity ->
val intent =
currentActivity.packageManager.getLaunchIntentForPackage(currentActivity.packageName)
intent?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
currentActivity.startActivity(intent)
currentActivity.finishAffinity()
}
}
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activity = binding.activity
}
override fun onDetachedFromActivityForConfigChanges() {
activity = null
}
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
activity = binding.activity
}
override fun onDetachedFromActivity() {
activity = null
}
}

38
restart_app/ios/.gitignore vendored Normal file
View file

@ -0,0 +1,38 @@
.idea/
.vagrant/
.sconsign.dblite
.svn/
.DS_Store
*.swp
profile
DerivedData/
build/
GeneratedPluginRegistrant.h
GeneratedPluginRegistrant.m
.generated/
*.pbxuser
*.mode1v3
*.mode2v3
*.perspectivev3
!default.pbxuser
!default.mode1v3
!default.mode2v3
!default.perspectivev3
xcuserdata
*.moved-aside
*.pyc
*sync/
Icon?
.tags*
/Flutter/Generated.xcconfig
/Flutter/ephemeral/
/Flutter/flutter_export_environment.sh

View file

View file

@ -0,0 +1,25 @@
import Flutter
import UIKit
public class RestartAppPlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "restart", binaryMessenger: registrar.messenger())
let instance = RestartAppPlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
if call.method == "restartApp" {
DispatchQueue.main.async {
if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
appDelegate.restartFlutterApp()
result("ok")
} else {
result(FlutterError(code: "APP_DELEGATE_NOT_FOUND", message: "Could not find AppDelegate", details: nil))
}
}
} else {
result(FlutterMethodNotImplemented)
}
}
}

View file

@ -0,0 +1,23 @@
#
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
# Run `pod lib lint restart_app.podspec` to validate before publishing.
#
Pod::Spec.new do |s|
s.name = 'restart_app'
s.version = '0.0.1'
s.summary = 'A new Flutter project.'
s.description = <<-DESC
A new Flutter project.
DESC
s.homepage = 'http://example.com'
s.license = { :file => '../LICENSE' }
s.author = { 'Your Company' => 'email@example.com' }
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.dependency 'Flutter'
s.platform = :ios, '11.0'
# Flutter.framework does not contain a i386 slice.
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
s.swift_version = '5.0'
end

View file

@ -0,0 +1,41 @@
import 'dart:async';
import 'package:flutter/services.dart';
/// `Restart` class provides a method to restart a Flutter application.
///
/// It uses the Flutter platform channels to communicate with the platform-specific code.
/// Specifically, it uses a `MethodChannel` named 'restart' for this communication.
///
/// The main functionality is provided by the `restartApp` method.
class Restart {
/// A private constant `MethodChannel`. This channel is used to communicate with the
/// platform-specific code to perform the restart operation.
static const MethodChannel _channel = const MethodChannel('restart');
/// Restarts the Flutter application.
///
/// The `webOrigin` parameter is optional. If it's null, the method uses the `window.origin`
/// to get the site origin. This parameter should only be filled when your current origin
/// is different than the app's origin. It defaults to null.
///
/// The `customMessage` parameter is optional. It allows customization of the notification
/// message displayed on iOS when restarting the app. If not provided, a default message
/// will be used.
///
/// This method communicates with the platform-specific code to perform the restart operation,
/// and then checks the response. If the response is "ok", it returns true, signifying that
/// the restart operation was successful. Otherwise, it returns false.
static Future<bool> restartApp({
String? webOrigin,
String? notificationTitle,
String? notificationBody,
}) async {
final Map<String, dynamic> args = {
'webOrigin': webOrigin,
'notificationTitle': notificationTitle,
'notificationBody': notificationBody,
};
return (await _channel.invokeMethod('restartApp', args)) == "ok";
}
}

View file

@ -0,0 +1,59 @@
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
// In order to *not* need this ignore, consider extracting the "web" version
// of your plugin as a separate package, instead of inlining it in the same
// package as the core of your plugin.
// ignore: avoid_web_libraries_in_flutter
import 'package:web/web.dart' as web show window;
/// `RestartWeb` provides a web implementation of the `Restart` plugin.
///
/// It registers a `MethodChannel` named 'restart' for communication between the Flutter code
/// and the platform-specific web code.
///
/// The main functionality is provided by the `restart` method.
class RestartWeb {
/// Registers this plugin with the given `registrar`.
///
/// This creates a `MethodChannel` named 'restart', and sets the method call handler to
/// this plugin's `handleMethodCall` method.
static void registerWith(Registrar registrar) {
final MethodChannel channel = MethodChannel(
'restart',
const StandardMethodCodec(),
registrar,
);
final pluginInstance = RestartWeb();
channel.setMethodCallHandler(pluginInstance.handleMethodCall);
}
/// Handles method calls from the Flutter code.
///
/// If the method call is 'restartApp', it calls the `restart` method with the given `webOrigin`.
/// Otherwise, it returns 'false' to signify that the method call was not recognized.
Future<dynamic> handleMethodCall(MethodCall call) async {
switch (call.method) {
case 'restartApp':
return restart(call.arguments as String?);
default:
return 'false';
}
}
/// Restarts the web app.
///
/// The `webOrigin` parameter is optional. If it's null, the method uses the `window.origin`
/// to get the site origin. This parameter should only be filled when your current origin
/// is different than the app's origin. It defaults to null.
///
/// This method replaces the current location with the given `webOrigin` (or `window.origin` if
/// `webOrigin` is null), effectively reloading the web app.
void restart(String? webOrigin) {
web.window.location.replace(
webOrigin ?? web.window.origin.toString(),
);
}
}

33
restart_app/pubspec.yaml Normal file
View file

@ -0,0 +1,33 @@
name: restart_app
description: A Flutter plugin that helps you to restart the whole Flutter app with a single function call by using native APIs.
version: 1.3.2
repository: https://github.com/gabrimatic/restart_app
issue_tracker: https://github.com/gabrimatic/restart_app/issues
environment:
sdk: ^3.5.1
flutter: ">=1.17.0"
dependencies:
flutter:
sdk: flutter
web: ^1.0.0
plugin_platform_interface: ^2.1.8
flutter_web_plugins:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter
flutter:
plugin:
platforms:
android:
package: gabrimatic.info.restart
pluginClass: RestartPlugin
ios:
pluginClass: RestartAppPlugin
web:
pluginClass: RestartWeb
fileName: restart_web.dart