diff --git a/config.lock.yaml b/config.lock.yaml index 0c30c28..880e828 100644 --- a/config.lock.yaml +++ b/config.lock.yaml @@ -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 diff --git a/config.yaml b/config.yaml index f46e879..65e243f 100644 --- a/config.yaml +++ b/config.yaml @@ -49,4 +49,10 @@ hand_signature: git: https://github.com/RomanBase/hand_signature.git flutter_sharing_intent: - git: https://github.com/bhagat-techind/flutter_sharing_intent.git \ No newline at end of file + 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 \ No newline at end of file diff --git a/no_screenshot/LICENSE b/no_screenshot/LICENSE new file mode 100644 index 0000000..8130832 --- /dev/null +++ b/no_screenshot/LICENSE @@ -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. diff --git a/no_screenshot/android/.gitignore b/no_screenshot/android/.gitignore new file mode 100644 index 0000000..161bdcd --- /dev/null +++ b/no_screenshot/android/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.cxx diff --git a/no_screenshot/android/build.gradle b/no_screenshot/android/build.gradle new file mode 100644 index 0000000..b1c7bc7 --- /dev/null +++ b/no_screenshot/android/build.gradle @@ -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 + } +} diff --git a/no_screenshot/android/gradle/wrapper/gradle-wrapper.jar b/no_screenshot/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..41d9927 Binary files /dev/null and b/no_screenshot/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/no_screenshot/android/gradle/wrapper/gradle-wrapper.properties b/no_screenshot/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..41dfb87 --- /dev/null +++ b/no_screenshot/android/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/no_screenshot/android/gradlew b/no_screenshot/android/gradlew new file mode 100755 index 0000000..1b6c787 --- /dev/null +++ b/no_screenshot/android/gradlew @@ -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" "$@" diff --git a/no_screenshot/android/gradlew.bat b/no_screenshot/android/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/no_screenshot/android/gradlew.bat @@ -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 diff --git a/no_screenshot/android/settings.gradle b/no_screenshot/android/settings.gradle new file mode 100644 index 0000000..8ed0d6a --- /dev/null +++ b/no_screenshot/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'no_screenshot' diff --git a/no_screenshot/android/src/main/AndroidManifest.xml b/no_screenshot/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a2f47b6 --- /dev/null +++ b/no_screenshot/android/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + 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 new file mode 100644 index 0000000..9ccb112 --- /dev/null +++ b/no_screenshot/android/src/main/kotlin/com/flutterplaza/no_screenshot/NoScreenshotPlugin.kt @@ -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 { + return JSONObject(map).toString() + } + + private val screenshotStream = object : Runnable { + override fun run() { + if (hasSharedPreferencesChanged) { + eventSink?.success(lastSharedPreferencesState) + hasSharedPreferencesChanged = false + } + handler.postDelayed(this, 1000) + } + } +} \ No newline at end of file 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 new file mode 100644 index 0000000..fe068a2 --- /dev/null +++ b/no_screenshot/android/src/test/kotlin/com/flutterplaza/no_screenshot/NoScreenshotPluginTest.kt @@ -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) + } +} diff --git a/no_screenshot/ios/.gitignore b/no_screenshot/ios/.gitignore new file mode 100644 index 0000000..0c88507 --- /dev/null +++ b/no_screenshot/ios/.gitignore @@ -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 \ No newline at end of file diff --git a/no_screenshot/ios/Assets/.gitkeep b/no_screenshot/ios/Assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/no_screenshot/ios/Classes/IOSNoScreenshotPlugin.swift b/no_screenshot/ios/Classes/IOSNoScreenshotPlugin.swift new file mode 100644 index 0000000..ae360d6 --- /dev/null +++ b/no_screenshot/ios/Classes/IOSNoScreenshotPlugin.swift @@ -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() + } +} diff --git a/no_screenshot/ios/Classes/NoScreenshotPlugin.h b/no_screenshot/ios/Classes/NoScreenshotPlugin.h new file mode 100644 index 0000000..0198b60 --- /dev/null +++ b/no_screenshot/ios/Classes/NoScreenshotPlugin.h @@ -0,0 +1,4 @@ +#import + +@interface NoScreenshotPlugin : NSObject +@end diff --git a/no_screenshot/ios/Classes/NoScreenshotPlugin.m b/no_screenshot/ios/Classes/NoScreenshotPlugin.m new file mode 100644 index 0000000..f639229 --- /dev/null +++ b/no_screenshot/ios/Classes/NoScreenshotPlugin.m @@ -0,0 +1,15 @@ +#import "NoScreenshotPlugin.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" +#endif + +@implementation NoScreenshotPlugin ++ (void)registerWithRegistrar:(NSObject*)registrar { + [IOSNoScreenshotPlugin registerWithRegistrar:registrar]; +} +@end diff --git a/no_screenshot/ios/Resources/PrivacyInfo.xcprivacy b/no_screenshot/ios/Resources/PrivacyInfo.xcprivacy new file mode 100644 index 0000000..a34b7e2 --- /dev/null +++ b/no_screenshot/ios/Resources/PrivacyInfo.xcprivacy @@ -0,0 +1,14 @@ + + + + + NSPrivacyTrackingDomains + + NSPrivacyAccessedAPITypes + + NSPrivacyCollectedDataTypes + + NSPrivacyTracking + + + diff --git a/no_screenshot/ios/no_screenshot.podspec b/no_screenshot/ios/no_screenshot.podspec new file mode 100644 index 0000000..7d9027e --- /dev/null +++ b/no_screenshot/ios/no_screenshot.podspec @@ -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 diff --git a/no_screenshot/lib/constants.dart b/no_screenshot/lib/constants.dart new file mode 100644 index 0000000..f2e5e59 --- /dev/null +++ b/no_screenshot/lib/constants.dart @@ -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"; diff --git a/no_screenshot/lib/no_screenshot.dart b/no_screenshot/lib/no_screenshot.dart new file mode 100644 index 0000000..f9469ad --- /dev/null +++ b/no_screenshot/lib/no_screenshot.dart @@ -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 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(); + } + + /// 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(); + } + + @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 new file mode 100644 index 0000000..6633075 --- /dev/null +++ b/no_screenshot/lib/no_screenshot_method_channel.dart @@ -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 get screenshotStream { + return eventChannel.receiveBroadcastStream().map((event) => + ScreenshotSnapshot.fromMap(jsonDecode(event) as Map)); + } + + @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 startScreenshotListening() { + return methodChannel.invokeMethod(startScreenshotListeningConst); + } + + @override + Future stopScreenshotListening() { + return methodChannel.invokeMethod(stopScreenshotListeningConst); + } +} diff --git a/no_screenshot/lib/no_screenshot_platform_interface.dart b/no_screenshot/lib/no_screenshot_platform_interface.dart new file mode 100644 index 0000000..eac1732 --- /dev/null +++ b/no_screenshot/lib/no_screenshot_platform_interface.dart @@ -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 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 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.'); + } +} diff --git a/no_screenshot/lib/screenshot_snapshot.dart b/no_screenshot/lib/screenshot_snapshot.dart new file mode 100644 index 0000000..6aa20e8 --- /dev/null +++ b/no_screenshot/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/no_screenshot/pubspec.yaml b/no_screenshot/pubspec.yaml new file mode 100644 index 0000000..23a142f --- /dev/null +++ b/no_screenshot/pubspec.yaml @@ -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 diff --git a/no_screenshot/test/no_screenshot_method_channel_test.dart b/no_screenshot/test/no_screenshot_method_channel_test.dart new file mode 100644 index 0000000..1033ec8 --- /dev/null +++ b/no_screenshot/test/no_screenshot_method_channel_test.dart @@ -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)'); + }); + }); +} diff --git a/no_screenshot/test/no_screenshot_platform_interface_test.dart b/no_screenshot/test/no_screenshot_platform_interface_test.dart new file mode 100644 index 0000000..ab0a9d4 --- /dev/null +++ b/no_screenshot/test/no_screenshot_platform_interface_test.dart @@ -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 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 stopScreenshotListening() 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); + }); + }); +} diff --git a/no_screenshot/test/no_screenshot_test.dart b/no_screenshot/test/no_screenshot_test.dart new file mode 100644 index 0000000..86396bd --- /dev/null +++ b/no_screenshot/test/no_screenshot_test.dart @@ -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 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 toggleScreenshot() async { + // 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(); + } +} + +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('NoScreenshot equality operator', () { + final instance1 = NoScreenshot.instance; + final instance2 = NoScreenshot.instance; + + expect(instance1 == instance2, true, reason: 'Instances should be equal'); + }); +} diff --git a/pubspec.yaml b/pubspec.yaml index 256377a..9106396 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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 diff --git a/restart_app/LICENSE b/restart_app/LICENSE new file mode 100644 index 0000000..58e9821 --- /dev/null +++ b/restart_app/LICENSE @@ -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. \ No newline at end of file diff --git a/restart_app/android/.gitignore b/restart_app/android/.gitignore new file mode 100644 index 0000000..c6cbe56 --- /dev/null +++ b/restart_app/android/.gitignore @@ -0,0 +1,8 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures diff --git a/restart_app/android/build.gradle b/restart_app/android/build.gradle new file mode 100644 index 0000000..5775252 --- /dev/null +++ b/restart_app/android/build.gradle @@ -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" +} diff --git a/restart_app/android/gradle.properties b/restart_app/android/gradle.properties new file mode 100644 index 0000000..94adc3a --- /dev/null +++ b/restart_app/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/restart_app/android/gradle/wrapper/gradle-wrapper.properties b/restart_app/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..00f9c2e --- /dev/null +++ b/restart_app/android/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/restart_app/android/settings.gradle b/restart_app/android/settings.gradle new file mode 100644 index 0000000..ae057a2 --- /dev/null +++ b/restart_app/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'restart' diff --git a/restart_app/android/src/main/AndroidManifest.xml b/restart_app/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..be84c4f --- /dev/null +++ b/restart_app/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/restart_app/android/src/main/kotlin/gabrimatic/info/restart/RestartPlugin.kt b/restart_app/android/src/main/kotlin/gabrimatic/info/restart/RestartPlugin.kt new file mode 100644 index 0000000..ae5ba31 --- /dev/null +++ b/restart_app/android/src/main/kotlin/gabrimatic/info/restart/RestartPlugin.kt @@ -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 + } +} \ No newline at end of file diff --git a/restart_app/ios/.gitignore b/restart_app/ios/.gitignore new file mode 100644 index 0000000..0c88507 --- /dev/null +++ b/restart_app/ios/.gitignore @@ -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 \ No newline at end of file diff --git a/restart_app/ios/Assets/.gitkeep b/restart_app/ios/Assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/restart_app/ios/Classes/RestartAppPlugin.swift b/restart_app/ios/Classes/RestartAppPlugin.swift new file mode 100644 index 0000000..afa431b --- /dev/null +++ b/restart_app/ios/Classes/RestartAppPlugin.swift @@ -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) + } + } +} \ No newline at end of file diff --git a/restart_app/ios/restart_app.podspec b/restart_app/ios/restart_app.podspec new file mode 100644 index 0000000..756f7d9 --- /dev/null +++ b/restart_app/ios/restart_app.podspec @@ -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 diff --git a/restart_app/lib/restart_app.dart b/restart_app/lib/restart_app.dart new file mode 100644 index 0000000..9accaef --- /dev/null +++ b/restart_app/lib/restart_app.dart @@ -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 restartApp({ + String? webOrigin, + String? notificationTitle, + String? notificationBody, + }) async { + final Map args = { + 'webOrigin': webOrigin, + 'notificationTitle': notificationTitle, + 'notificationBody': notificationBody, + }; + return (await _channel.invokeMethod('restartApp', args)) == "ok"; + } +} diff --git a/restart_app/lib/restart_web.dart b/restart_app/lib/restart_web.dart new file mode 100644 index 0000000..a42735a --- /dev/null +++ b/restart_app/lib/restart_web.dart @@ -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 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(), + ); + } +} diff --git a/restart_app/pubspec.yaml b/restart_app/pubspec.yaml new file mode 100644 index 0000000..5e5610d --- /dev/null +++ b/restart_app/pubspec.yaml @@ -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 \ No newline at end of file