diff --git a/README.md b/README.md index b9ee402..d9d335f 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,22 @@ **previously nammed secure_window** +Forked from [neckaros](https://github.com/neckaros/secure_application) & [stevenspiel](https://github.com/stevenspiel/secure_application) + +### Extended features: +- Able to use LaunchImage in background in IOS +- LaunchImage and BlurrEffect can work together + # secure_application This plugin allow you to protect your application content from view on demand - + Pluggin in iOS is in swift Pluggin in Android is in Kotlin / AndroidX libraries -Pluggin is also working for web +Pluggin is also working for web Plugin work for windows (will lock when you minimize the window and lock screen) @@ -19,15 +25,31 @@ Plugin work for windows (will lock when you minimize the window and lock screen) ### Installation Add `secure_application` as a dependency in your pubspec.yaml file ([what?](https://pub.dev/packages/secure_application#-installing-tab-)). +To use this branch, in `pubspec.yaml`: +``` +... +dependencies: + ... + secure_application: + git: + url: https://github.com/eddyuan/secure_application +``` + +### Use useLaunchImageIOS +Open your_project/ios in xcode, add LaunchImage if not already exist + + ### Import Import secure_application: + ```dart import 'package:secure_application/secure_application.dart'; ``` Add a top level SecureApplication + ```dart SecureApplication( onNeedUnlock: (secure) => print( @@ -37,9 +59,13 @@ SecureApplication( ``` Put the content you want to protect in a SecureGate (could be the whole app) + ```dart SecureGate( + useLaunchImageIOS: true, blurr: 5, + opacity: 0.5, + backgroundColor: Colors.white, lockedBuilder: (context, secureNotifier) => Center( child: RaisedButton( child: Text('test'), @@ -50,8 +76,11 @@ SecureGate( ``` ## Tips + ### Placement + Best place to add the secure application is directly **inside** the MaterialApp by using its builder: + ```dart class MyApp extends StatelessWidget { final navigatorKey = GlobalKey(); @@ -131,12 +160,14 @@ class MyApp extends StatelessWidget { } ``` -Notice 3 importants part here: -* Secure application is in MaterialApp/Builder so just above your app naviagator -* I wrap the route i want to protect in onGenerateRoute with a **SecureGate** but you could put it anywhare you want if you only want to protect part of your page -* Ask validation will display a dialog above everything to ask is you want to unlock app or not +Notice 3 importants part here: + +- Secure application is in MaterialApp/Builder so just above your app naviagator +- I wrap the route i want to protect in onGenerateRoute with a **SecureGate** but you could put it anywhare you want if you only want to protect part of your page +- Ask validation will display a dialog above everything to ask is you want to unlock app or not ### react to failed auth + ```dart class SecureReacting extends StatefulWidget { @override @@ -175,13 +206,13 @@ class _SecureReactingState extends State { } ``` -Notice 2 importants part here: -* You are subscribing to a stream so don't forget to unsubscribe in onDispose -* Since your animation from pop can take some time you might want also to have a locked variable here to display different content during transition - +Notice 2 importants part here: +- You are subscribing to a stream so don't forget to unsubscribe in onDispose +- Since your animation from pop can take some time you might want also to have a locked variable here to display different content during transition ## API Docs + [API Reference](https://pub.dev/documentation/secure_application/latest/) ## Basic understanding @@ -189,30 +220,35 @@ Notice 2 importants part here: The library is mainly controller via the [SecureApplicationController](https://pub.dev/documentation/secure_application/latest/secure_application_controller/SecureApplicationController-class.html) which can be ### secured + if the user switch app or leave app the content will not be visible in the app switcher and when it goes back to the app it will **lock** it ### locked + the child of the **SecureGate**s will be hidden bellow the blurry barrier ### paused + even if secured **SecureGate**s will not activate when user comes back to the app ### authenticated + last authentication status. To help you manage visibility of some elements of your UI depending on auth status -* **secureApplicationController.authFailed()** will set it to **false** -* **secureApplicationController.authLogout()** will set it to **false** -* **secureApplicationController.authSuccess()** will set it to **true** +- **secureApplicationController.authFailed()** will set it to **false** +- **secureApplicationController.authLogout()** will set it to **false** +- **secureApplicationController.authSuccess()** will set it to **true** You could use the authenticationEvents for the same purpose as it is a BehaviorSubject stream ### Streams + There is two different BehaviorStream (emit last value when you subscribe): -* **authenticationEvents**: When there is a successful or unsucessful authentification (you can use it for example to clean your app if authentification is not successful) -* **lockEvents**: Will be called when the application lock or unlock. Usefull for example to pause media when the app lock +- **authenticationEvents**: When there is a successful or unsucessful authentification (you can use it for example to clean your app if authentification is not successful) +- **lockEvents**: Will be called when the application lock or unlock. Usefull for example to pause media when the app lock ## Example @@ -225,21 +261,25 @@ This tool does not impose a way to authenticate the user to give them back acces You are free to use your own Widget/Workflow to allow user to see their content once locked (cf **SecureApplication** widget argument [onNeedUnlock](https://pub.dev/documentation/secure_application/latest/secure_application/SecureApplication/onNeedUnlock.html)) Therefore you can use any method you like: -* Your own Widget for code Authentication -* biometrics with the [local_auth](https://pub.dev/packages/local_auth) package -* ... + +- Your own Widget for code Authentication +- biometrics with the [local_auth](https://pub.dev/packages/local_auth) package +- ... ## Android + On Android as soon as you **secure** the application the user will not be able to capture the screen in the app (even if unlocked) We might give an option to allow screenshot as an option if need arise ## iOS + Contrary to Android we create a native frosted view over your app content so that content is not visible in the app switcher. When the user gets back to the app we wait for ~500ms to remove this view to allow time for the app to woke and flutter gate to draw # Widgets ## SecureApplication + [Api Doc](https://pub.dev/documentation/secure_application/latest/secure_application/SecureApplication-class.html) this widget is **required** and need to be a parent of any Gate it provides to its descendant a SecureApplicationProvider that allow you to secure or open the application @@ -247,6 +287,7 @@ it provides to its descendant a SecureApplicationProvider that allow you to secu You can pass you own initialized SecureApplicationController if you want to set default values ## SecureGate + [Api Doc](https://pub.dev/documentation/secure_application/latest/secure_gate/SecureGate-class.html) The **child** of this widget will be below a blurry barrier (control the amount of **blurr** and **opacity** with its arguments) if the provided SecureApplicationController is **locked** @@ -254,20 +295,26 @@ if the provided SecureApplicationController is **locked** # Native workings ## Android + When **locked** we set the secure flag to true + ```kotlin activity?.window?.addFlags(LayoutParams.FLAG_SECURE) ``` + When **opened** we remove the secure flag + ```kotlin activity?.window?.clearFlags(LayoutParams.FLAG_SECURE) ``` ## iOS + When app will become inactive we add a top view with a blurr filter We remove this app 500ms after the app become active to avoid your content form being breifly visible # Because we all want to see code in a Readme + ```dart Widget build(BuildContext context) { var width = MediaQuery.of(context).size.width * 0.8; @@ -397,4 +444,4 @@ Widget build(BuildContext context) { ), ); } -``` \ No newline at end of file +``` diff --git a/android/src/main/kotlin/org/jezequel/secure_application/SecureApplicationPlugin.kt b/android/src/main/kotlin/org/jezequel/secure_application/SecureApplicationPlugin.kt index 4f0b3ac..32b0682 100644 --- a/android/src/main/kotlin/org/jezequel/secure_application/SecureApplicationPlugin.kt +++ b/android/src/main/kotlin/org/jezequel/secure_application/SecureApplicationPlugin.kt @@ -84,7 +84,13 @@ public class SecureApplicationPlugin: FlutterPlugin, MethodCallHandler, Activity result.success(true) } else if (call.method == "open") { activity?.window?.clearFlags(LayoutParams.FLAG_SECURE) - result.success(true) + result.success(true) + } else if (call.method == "useLaunchImage") { + // This is currently not possible on Android, as far as I am aware + result.success(true) + } else if (call.method == "backgroundColor") { + // This is currently not possible on Android, as far as I am aware + result.success(true) } else { result.success(true) } diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist index 9367d48..8d4492f 100644 --- a/example/ios/Flutter/AppFrameworkInfo.plist +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 8.0 + 9.0 diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 1a3f02c..5c8a216 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -14,9 +14,9 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/secure_application/ios" SPEC CHECKSUMS: - Flutter: 434fef37c0980e73bb6479ef766c45957d4b510c + Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a secure_application: 27d424e8c2e770f63e38e280b5a51f921aa9b0c8 PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c -COCOAPODS: 1.10.1 +COCOAPODS: 1.11.3 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index d605d68..40faf6d 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 50; objects = { /* Begin PBXBuildFile section */ @@ -156,7 +156,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1020; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index a28140c..3db53b6 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ UIViewControllerBasedStatusBarAppearance + CADisableMinimumFrameDurationOnPhone + diff --git a/example/lib/main.dart b/example/lib/main.dart index 2c8ce77..97e93fd 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -30,7 +30,7 @@ class _MyAppState extends State { @override Widget build(BuildContext context) { - var width = MediaQuery.of(context).size.width * 0.8; + final double width = MediaQuery.of(context).size.width * 0.2; return MaterialApp( home: SecureApplication( nativeRemoveDelay: 1000, @@ -69,6 +69,8 @@ class _MyAppState extends State { .listen((s) => history.add( '${DateTime.now().toIso8601String()} - ${s ? 'locked' : 'unlocked'}')); return SecureGate( + useLaunchImageIOS: true, + backgroundColor: Colors.white, blurr: blurr, opacity: opacity, lockedBuilder: (context, secureNotifier) => Center( @@ -89,46 +91,44 @@ class _MyAppState extends State { appBar: AppBar( title: const Text('Secure Window Example'), ), - body: Center( - child: Builder(builder: (context) { - var valueNotifier = SecureApplicationProvider.of(context); - if (valueNotifier == null) - throw new Exception( - 'Unable to find secure application context'); - return ListView( - children: [ - Text('This is secure content'), - ValueListenableBuilder( - valueListenable: valueNotifier, - builder: (context, state, _) => state.secured - ? Column( - children: [ - ElevatedButton( - onPressed: () => valueNotifier.open(), - child: Text('Open app'), - ), - state.paused - ? ElevatedButton( - onPressed: () => - valueNotifier.unpause(), - child: Text('resume security'), - ) - : ElevatedButton( - onPressed: () => - valueNotifier.pause(), - child: Text('pause security'), - ), - ], - ) - : ElevatedButton( - onPressed: () => valueNotifier.secure(), - child: Text('Secure app'), - ), - ), - if (failedAuth == null) - Text( - 'Lock the app then switch to another app and come back'), - if (failedAuth != null) + body: Padding( + padding: const EdgeInsets.all(16), + child: Center( + child: Builder(builder: (context) { + var valueNotifier = SecureApplicationProvider.of(context); + if (valueNotifier == null) + throw new Exception( + 'Unable to find secure application context'); + return ListView( + children: [ + Text('This is secure content'), + ValueListenableBuilder( + valueListenable: valueNotifier, + builder: (context, state, _) => state.secured + ? Column( + children: [ + ElevatedButton( + onPressed: () => valueNotifier.open(), + child: Text('Open app'), + ), + state.paused + ? ElevatedButton( + onPressed: () => + valueNotifier.unpause(), + child: Text('resume security'), + ) + : ElevatedButton( + onPressed: () => + valueNotifier.pause(), + child: Text('pause security'), + ), + ], + ) + : ElevatedButton( + onPressed: () => valueNotifier.secure(), + child: Text('Secure app'), + ), + ), failedAuth ? Text( 'Auth failed we cleaned sensitive data', @@ -138,55 +138,57 @@ class _MyAppState extends State { 'Auth success', style: TextStyle(color: Colors.green), ), - FlutterLogo( - size: width, - ), - StreamBuilder( - stream: valueNotifier.authenticationEvents, - builder: (context, snapshot) => - Text('Last auth status is: ${snapshot.data}'), - ), - ElevatedButton( - onPressed: () => valueNotifier.lock(), - child: Text('manually lock'), - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - children: [ - Text('Blurr:'), - Expanded( - child: Slider( - value: blurr, - min: 0, - max: 100, - onChanged: (v) => setState(() => blurr = v)), - ), - Text(blurr.floor().toString()), - ], + FlutterLogo( + size: width, ), - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - children: [ - Text('opacity:'), - Expanded( - child: Slider( - value: opacity, - min: 0, - max: 1, - onChanged: (v) => - setState(() => opacity = v)), - ), - Text((opacity * 100).floor().toString() + "%"), - ], + StreamBuilder( + stream: valueNotifier.authenticationEvents, + builder: (context, snapshot) => + Text('Last auth status is: ${snapshot.data}'), ), - ), - ...history.map((h) => Text(h)).toList(), - ], - ); - }), + ElevatedButton( + onPressed: () => valueNotifier.lock(), + child: Text('manually lock'), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Text('Blurr:'), + Expanded( + child: Slider( + value: blurr, + min: 0, + max: 100, + onChanged: (v) => + setState(() => blurr = v)), + ), + Text(blurr.floor().toString()), + ], + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Text('opacity:'), + Expanded( + child: Slider( + value: opacity, + min: 0, + max: 1, + onChanged: (v) => + setState(() => opacity = v)), + ), + Text((opacity * 100).floor().toString() + "%"), + ], + ), + ), + ...history.map((h) => Text(h)).toList(), + ], + ); + }), + ), ), ), ); diff --git a/example/pubspec.lock b/example/pubspec.lock index b5e1268..cc06998 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -42,7 +42,7 @@ packages: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.15.0" + version: "1.16.0" cupertino_icons: dependency: "direct main" description: @@ -56,7 +56,7 @@ packages: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.0" flutter: dependency: "direct main" description: flutter @@ -85,7 +85,7 @@ packages: name: js url: "https://pub.dartlang.org" source: hosted - version: "0.6.3" + version: "0.6.4" matcher: dependency: transitive description: @@ -93,6 +93,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.12.11" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.4" meta: dependency: transitive description: @@ -106,14 +113,14 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.8.1" rxdart: dependency: transitive description: name: rxdart url: "https://pub.dartlang.org" source: hosted - version: "0.26.0" + version: "0.27.4" secure_application: dependency: "direct dev" description: @@ -132,7 +139,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.2" stack_trace: dependency: transitive description: @@ -167,21 +174,14 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.3" - typed_data: - dependency: transitive - description: - name: typed_data - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.0" + version: "0.4.9" vector_math: dependency: transitive description: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.1.2" sdks: - dart: ">=2.14.0 <3.0.0" + dart: ">=2.17.0-0 <3.0.0" flutter: ">=1.20.0" diff --git a/example/screenshot/launch_image.png b/example/screenshot/launch_image.png new file mode 100644 index 0000000..1db4e10 Binary files /dev/null and b/example/screenshot/launch_image.png differ diff --git a/example/screenshot/xcode_setting.png b/example/screenshot/xcode_setting.png new file mode 100644 index 0000000..8a9b3f5 Binary files /dev/null and b/example/screenshot/xcode_setting.png differ diff --git a/example/windows/flutter/generated_plugin_registrant.cc b/example/windows/flutter/generated_plugin_registrant.cc index b359ee1..3a931bf 100644 --- a/example/windows/flutter/generated_plugin_registrant.cc +++ b/example/windows/flutter/generated_plugin_registrant.cc @@ -2,6 +2,8 @@ // Generated file. Do not edit. // +// clang-format off + #include "generated_plugin_registrant.h" #include diff --git a/example/windows/flutter/generated_plugin_registrant.h b/example/windows/flutter/generated_plugin_registrant.h index 9846246..dc139d8 100644 --- a/example/windows/flutter/generated_plugin_registrant.h +++ b/example/windows/flutter/generated_plugin_registrant.h @@ -2,6 +2,8 @@ // Generated file. Do not edit. // +// clang-format off + #ifndef GENERATED_PLUGIN_REGISTRANT_ #define GENERATED_PLUGIN_REGISTRANT_ diff --git a/example/windows/flutter/generated_plugins.cmake b/example/windows/flutter/generated_plugins.cmake index 6a5fe7f..b331b9c 100644 --- a/example/windows/flutter/generated_plugins.cmake +++ b/example/windows/flutter/generated_plugins.cmake @@ -6,6 +6,9 @@ list(APPEND FLUTTER_PLUGIN_LIST secure_application ) +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + set(PLUGIN_BUNDLED_LIBRARIES) foreach(plugin ${FLUTTER_PLUGIN_LIST}) @@ -14,3 +17,8 @@ foreach(plugin ${FLUTTER_PLUGIN_LIST}) list(APPEND PLUGIN_BUNDLED_LIBRARIES $) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/ios/Classes/SwiftSecureApplicationPlugin.swift b/ios/Classes/SwiftSecureApplicationPlugin.swift index 2fab50e..61f6338 100644 --- a/ios/Classes/SwiftSecureApplicationPlugin.swift +++ b/ios/Classes/SwiftSecureApplicationPlugin.swift @@ -2,100 +2,264 @@ import Flutter import UIKit public class SwiftSecureApplicationPlugin: NSObject, FlutterPlugin { - var secured = false; - var opacity: CGFloat = 0.2; - - var backgroundTask: UIBackgroundTaskIdentifier! - - internal let registrar: FlutterPluginRegistrar - - init(registrar: FlutterPluginRegistrar) { - self.registrar = registrar - super.init() - registrar.addApplicationDelegate(self) - } - + var secured = false; + var opacity: CGFloat = 0.2; + var useLaunchImage: Bool = false; + var backgroundColor: UIColor = UIColor.white; + + var backgroundTask: UIBackgroundTaskIdentifier! + +// let IMAGE_VIEW_TAG = 99697; +// let BLUR_VIEW_TAG = 99698; +// let COLOR_VIEW_TAG = 99699; + + var lockView: UIView? + var isInFadeIn: Bool = false + + // let logoWidthToScreenWidthRatio: CGFloat = 0.5 + // let logoWidthToHeightRatio: CGFloat = 1.0 + let animationDuration: CFTimeInterval = 0.3 + + internal let registrar: FlutterPluginRegistrar + + init(registrar: FlutterPluginRegistrar) { + self.registrar = registrar + super.init() + registrar.addApplicationDelegate(self) + } + public static func register(with registrar: FlutterPluginRegistrar) { let channel = FlutterMethodChannel(name: "secure_application", binaryMessenger: registrar.messenger()) let instance = SwiftSecureApplicationPlugin(registrar: registrar) registrar.addMethodCallDelegate(instance, channel: channel) } - + + private func createLockView(window: UIWindow, isDisplayingLogo: Bool) { + dismissLockView() + + lockView = UIView(frame: window.bounds); + lockView?.alpha = 0.0 + window.addSubview(lockView!) + + let blurEffect = UIBlurEffect(style: UIBlurEffect.Style.extraLight) + let blurView = UIVisualEffectView(effect: blurEffect) + blurView.frame = window.bounds + lockView!.addSubview(blurView) + + let opacityView = UIView(frame: window.bounds) + opacityView.backgroundColor = backgroundColor + opacityView.alpha = opacity + lockView!.addSubview(opacityView) + + if isDisplayingLogo { + // let logoWidth = window.frame.width * logoWidthToScreenWidthRatio + // let logoHeight = logoWidth / logoWidthToHeightRatio + // let logoRect = CGRect(x: (window.frame.width - logoWidth) * 0.5, + // y: (window.frame.height - logoHeight) * 0.5, + // width: logoWidth, + // height: logoHeight) + let logoView = UIImageView(image: UIImage(named: "LaunchImage")) + logoView.frame = window.bounds + logoView.contentMode = .center + // logoView.frame = logoRect + lockView!.addSubview(logoView) + } + + isInFadeIn = true + lockView?.layer.removeAllAnimations() + UIView.transition(with: lockView!, + duration: animationDuration, + options: .transitionCrossDissolve, + animations: { + self.lockView?.alpha = 1.0 + self.isInFadeIn = false + window.snapshotView(afterScreenUpdates: true) + }) + } + + private func dismissLockView() { + guard lockView != nil else { + return + } + + lockView?.layer.removeAllAnimations() + UIView.transition(with: lockView!, + duration: animationDuration, + options: .transitionCrossDissolve, + animations: { + self.lockView?.alpha = 0.0 + }) { (finished) in + if finished && self.lockView != nil && self.isInFadeIn { + for subview in self.lockView!.subviews { subview.removeFromSuperview() } + self.lockView?.removeFromSuperview() + self.lockView = nil + } + } + } + +// public func applicationDidBecomeActive(_ application: UIApplication) { +// dismissLockView() +// } + public func applicationWillResignActive(_ application: UIApplication) { if ( secured ) { - self.registerBackgroundTask() - UIApplication.shared.ignoreSnapshotOnNextApplicationLaunch() - if let window = UIApplication.shared.windows.filter({ (w) -> Bool in - return w.isHidden == false - }).first { - if let existingView = window.viewWithTag(99699), let existingBlurrView = window.viewWithTag(99698) { - window.bringSubviewToFront(existingView) - window.bringSubviewToFront(existingBlurrView) - return - } else { - let colorView = UIView(frame: window.bounds); - colorView.tag = 99699 - colorView.autoresizingMask = [.flexibleWidth, .flexibleHeight] - colorView.backgroundColor = UIColor(white: 1, alpha: opacity) - window.addSubview(colorView) - window.bringSubviewToFront(colorView) - - let blurEffect = UIBlurEffect(style: UIBlurEffect.Style.extraLight) - let blurEffectView = UIVisualEffectView(effect: blurEffect) - blurEffectView.frame = window.bounds - blurEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight] - - blurEffectView.tag = 99698 - - window.addSubview(blurEffectView) - window.bringSubviewToFront(blurEffectView) - window.snapshotView(afterScreenUpdates: true) - RunLoop.current.run(until: Date(timeIntervalSinceNow:0.5)) - } - } - self.endBackgroundTask() + self.registerBackgroundTask() + UIApplication.shared.ignoreSnapshotOnNextApplicationLaunch() + if let window = UIApplication.shared.windows.filter({ (w) -> Bool in + return w.isHidden == false + }).first { + createLockView(window: window, isDisplayingLogo: useLaunchImage) + // if (!useLaunchImage) { + // if let existingColorView = window.viewWithTag(COLOR_VIEW_TAG) { + // existingColorView.frame = window.bounds + // existingColorView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + // window.bringSubviewToFront(existingColorView) + // } else { + // let colorView = UIView(frame: window.bounds); + // colorView.tag = COLOR_VIEW_TAG + // colorView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + // colorView.backgroundColor = backgroundColor.withAlphaComponent(opacity) + // window.addSubview(colorView) + // window.bringSubviewToFront(colorView) + // } + // } + // + // if let existingBlurrView = window.viewWithTag(BLUR_VIEW_TAG) { + // existingBlurrView.frame = window.bounds + // existingBlurrView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + // window.bringSubviewToFront(existingBlurrView) + // } else { + // let blurEffect = UIBlurEffect(style: UIBlurEffect.Style.extraLight) + // let blurEffectView = UIVisualEffectView(effect: blurEffect) + // blurEffectView.frame = window.bounds + // blurEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + // blurEffectView.tag = BLUR_VIEW_TAG + // window.addSubview(blurEffectView) + // window.bringSubviewToFront(blurEffectView) + // } + // + // if (useLaunchImage) { + // if let existingImageView = window.viewWithTag(IMAGE_VIEW_TAG) { + // existingImageView.frame = window.bounds + // existingImageView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + // existingImageView.backgroundColor = backgroundColor.withAlphaComponent(opacity) + // existingImageView.clipsToBounds = true + // existingImageView.contentMode = .center + // window.bringSubviewToFront(existingImageView) + // } else { + // let imageView = UIImageView.init(frame: window.bounds) + // imageView.tag = IMAGE_VIEW_TAG + // imageView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + // imageView.backgroundColor = backgroundColor.withAlphaComponent(opacity) + // imageView.clipsToBounds = true + // imageView.contentMode = .center + // imageView.image = UIImage(named: "LaunchImage") + // imageView.isMultipleTouchEnabled = true + // imageView.translatesAutoresizingMaskIntoConstraints = false + // window.addSubview(imageView) + // window.bringSubviewToFront(imageView) + // } + // } + // + // window.snapshotView(afterScreenUpdates: true) + // RunLoop.current.run(until: Date(timeIntervalSinceNow:0.5)) + } + self.endBackgroundTask() } } - func registerBackgroundTask() { - self.backgroundTask = UIApplication.shared.beginBackgroundTask { [weak self] in - self?.endBackgroundTask() - } - assert(self.backgroundTask != UIBackgroundTaskIdentifier.invalid) - } - - func endBackgroundTask() { - print("Background task ended.") - UIApplication.shared.endBackgroundTask(backgroundTask) - backgroundTask = UIBackgroundTaskIdentifier.invalid + func registerBackgroundTask() { + self.backgroundTask = UIApplication.shared.beginBackgroundTask { [weak self] in + self?.endBackgroundTask() } - + assert(self.backgroundTask != UIBackgroundTaskIdentifier.invalid) + } + + func endBackgroundTask() { + print("Background task ended.") + UIApplication.shared.endBackgroundTask(backgroundTask) + backgroundTask = UIBackgroundTaskIdentifier.invalid + } + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { if (call.method == "secure") { - secured = true; - if let args = call.arguments as? Dictionary, - let opacity = args["opacity"] as? NSNumber { - self.opacity = opacity as! CGFloat + secured = true; + if let args = call.arguments as? Dictionary { + if let opacity = args["opacity"] as? NSNumber { + self.opacity = opacity as! CGFloat } + + if let useLaunchImage = args["useLaunchImage"] as? Bool { + self.useLaunchImage = useLaunchImage + } + + if let backgroundColor = args["backgroundColor"] as? String { + self.backgroundColor = hexStringToUIColor(hex: backgroundColor) + } + } } else if (call.method == "open") { - secured = false; - } else if (call.method == "opacity") { - if let args = call.arguments as? Dictionary, - let opacity = args["opacity"] as? NSNumber { - self.opacity = opacity as! CGFloat - } + secured = false; + } else if (call.method == "opacity") { + if let args = call.arguments as? Dictionary, + let opacity = args["opacity"] as? NSNumber { + self.opacity = opacity as! CGFloat + } + } else if (call.method == "backgroundColor") { + if let args = call.arguments as? Dictionary, + let backgroundColor = args["backgroundColor"] as? String { + self.backgroundColor = hexStringToUIColor(hex: backgroundColor) + } + } else if (call.method == "useLaunchImage") { + if let args = call.arguments as? Dictionary, + let useLaunchImage = args["useLaunchImage"] as? Bool { + self.useLaunchImage = useLaunchImage + } } else if (call.method == "unlock") { - if let window = UIApplication.shared.windows.filter({ (w) -> Bool in - return w.isHidden == false - }).first, let view = window.viewWithTag(99699), let blurrView = window.viewWithTag(99698) { - UIView.animate(withDuration: 0.5, animations: { - view.alpha = 0.0 - blurrView.alpha = 0.0 - }, completion: { finished in - view.removeFromSuperview() - blurrView.removeFromSuperview() - - }) - } + dismissLockView() + // if let window = UIApplication.shared.windows.filter({ (w) -> Bool in + // return w.isHidden == false + // }).first { + // if let colorView = window.viewWithTag(COLOR_VIEW_TAG) { + // UIView.animate(withDuration: 0.4, animations: { + // colorView.alpha = 0.0 + // }, completion: { finished in + // colorView.removeFromSuperview() + // }) + // } + // + // if let imageView = window.viewWithTag(IMAGE_VIEW_TAG) { + // UIView.animate(withDuration: 0.4, animations: { + // imageView.alpha = 0.0 + // }, completion: { finished in + // imageView.removeFromSuperview() + // }) + // } + // + // if let blurrView = window.viewWithTag(BLUR_VIEW_TAG) { + // blurrView.removeFromSuperview() + // } + // } + } + } + + func hexStringToUIColor (hex:String) -> UIColor { + var cString:String = hex.trimmingCharacters(in: .whitespacesAndNewlines).uppercased() + + if (cString.hasPrefix("#")) { + cString.remove(at: cString.startIndex) + } + + if ((cString.count) != 6) { + return UIColor.gray } + + var rgbValue:UInt64 = 0 + Scanner(string: cString).scanHexInt64(&rgbValue) + + return UIColor( + red: CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0, + green: CGFloat((rgbValue & 0x00FF00) >> 8) / 255.0, + blue: CGFloat(rgbValue & 0x0000FF) / 255.0, + alpha: CGFloat(1.0) + ) } } diff --git a/lib/secure_application.dart b/lib/secure_application.dart index 3ab34a0..57f7362 100644 --- a/lib/secure_application.dart +++ b/lib/secure_application.dart @@ -99,7 +99,7 @@ class _SecureApplicationState extends State } }); super.initState(); - WidgetsBinding.instance!.addObserver(this); + WidgetsBinding.instance.addObserver(this); SecureApplicationNative.registerForEvents( secureApplicationController.lockIfSecured, secureApplicationController.unlock); @@ -109,7 +109,7 @@ class _SecureApplicationState extends State void dispose() { _authStreamSubscription?.cancel(); super.dispose(); - WidgetsBinding.instance!.removeObserver(this); + WidgetsBinding.instance.removeObserver(this); } @override @@ -131,7 +131,7 @@ class _SecureApplicationState extends State if (authStatus != null) secureApplicationController.sendAuthenticationEvent(authStatus); - WidgetsBinding.instance!.addPostFrameCallback((_) { + WidgetsBinding.instance.addPostFrameCallback((_) { secureApplicationController.unpause(); }); } diff --git a/lib/secure_application_native.dart b/lib/secure_application_native.dart index 6f710c0..69934c8 100644 --- a/lib/secure_application_native.dart +++ b/lib/secure_application_native.dart @@ -1,6 +1,6 @@ import 'dart:async'; +import 'dart:ui'; -import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; class SecureApplicationNative { @@ -45,4 +45,16 @@ class SecureApplicationNative { static Future opacity(double opacity) { return _channel.invokeMethod('opacity', {"opacity": opacity}); } + + static Future useLaunchImageIOS(bool useLaunchImage) { + return _channel.invokeMethod('useLaunchImage', { + 'useLaunchImage': useLaunchImage, + }); + } + + static Future backgroundColor(Color color) { + return _channel.invokeMethod('backgroundColor', { + 'backgroundColor': '#${color.value.toRadixString(16).substring(2, 8)}', + }); + } } diff --git a/lib/secure_gate.dart b/lib/secure_gate.dart index 0c51b82..bef6fb7 100644 --- a/lib/secure_gate.dart +++ b/lib/secure_gate.dart @@ -1,9 +1,9 @@ import 'dart:ui'; import 'package:flutter/material.dart'; +import 'package:secure_application/secure_application_controller.dart'; import 'package:secure_application/secure_application_native.dart'; import 'package:secure_application/secure_application_provider.dart'; -import 'package:secure_application/secure_application_controller.dart'; /// it will display a blurr over your content if locked /// @@ -27,13 +27,30 @@ class SecureGate extends StatefulWidget { /// default to 0.6 final double opacity; + /// Whether to use the application's LaunchImage in the app switcher. + /// + /// For this to work, you MUST have a LaunchImage ImageSet in your iOS folder, + /// just like a newly generated flutter application (at ios/Runner/Assets.xcassets/LaunchImage.imageset) + /// More info here: https://docs.flutter.dev/development/ui/advanced/splash-screen#ios-launch-screen + /// + /// If this is true, [opacity] and [blurr] are ignored (iOS only). + /// + /// Only available on iOS. It is not possible on Android, as far as I'm aware + final bool useLaunchImageIOS; + + /// The background color in the app switcher. + final Color? backgroundColor; + const SecureGate({ Key? key, required this.child, this.blurr = 20, this.opacity = 0.6, this.lockedBuilder, + this.useLaunchImageIOS = false, + this.backgroundColor, }) : super(key: key); + @override _SecureGateState createState() => _SecureGateState(); } @@ -50,6 +67,10 @@ class _SecureGateState extends State AnimationController(vsync: this, duration: kThemeAnimationDuration * 2) ..addListener(_handleChange); SecureApplicationNative.opacity(widget.opacity); + SecureApplicationNative.useLaunchImageIOS(widget.useLaunchImageIOS); + if (widget.backgroundColor != null) { + SecureApplicationNative.backgroundColor(widget.backgroundColor!); + } super.initState(); } @@ -70,6 +91,13 @@ class _SecureGateState extends State if (oldWidget.opacity != widget.opacity) { SecureApplicationNative.opacity(widget.opacity); } + if (oldWidget.useLaunchImageIOS != widget.useLaunchImageIOS) { + SecureApplicationNative.useLaunchImageIOS(widget.useLaunchImageIOS); + } + if (oldWidget.backgroundColor != widget.backgroundColor && + widget.backgroundColor != null) { + SecureApplicationNative.backgroundColor(widget.backgroundColor!); + } } void _sercureNotified() { @@ -108,7 +136,7 @@ class _SecureGateState extends State sigmaY: widget.blurr * _gateVisibility.value), child: Container( decoration: BoxDecoration( - color: Colors.grey.shade200 + color: (widget.backgroundColor ?? Colors.grey.shade200) .withOpacity(widget.opacity * _gateVisibility.value)), ), ), diff --git a/pubspec.lock b/pubspec.lock index 0de7638..5aadeaf 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -42,14 +42,14 @@ packages: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.15.0" + version: "1.16.0" fake_async: dependency: transitive description: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.0" flutter: dependency: "direct main" description: flutter @@ -61,7 +61,7 @@ packages: name: flutter_plugin_android_lifecycle url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.0.6" flutter_test: dependency: "direct dev" description: flutter @@ -78,7 +78,7 @@ packages: name: js url: "https://pub.dartlang.org" source: hosted - version: "0.6.3" + version: "0.6.4" matcher: dependency: transitive description: @@ -86,6 +86,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.12.11" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.4" meta: dependency: transitive description: @@ -99,14 +106,14 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.8.1" rxdart: dependency: "direct main" description: name: rxdart url: "https://pub.dartlang.org" source: hosted - version: "0.26.0" + version: "0.27.4" sky_engine: dependency: transitive description: flutter @@ -118,7 +125,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.2" stack_trace: dependency: transitive description: @@ -153,21 +160,14 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.3" - typed_data: - dependency: transitive - description: - name: typed_data - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.0" + version: "0.4.9" vector_math: dependency: transitive description: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.1.2" sdks: - dart: ">=2.14.0 <3.0.0" - flutter: ">=1.20.0" + dart: ">=2.17.0-0 <3.0.0" + flutter: ">=2.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 0ad5727..4daa6a9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,7 +14,7 @@ dependencies: flutter_web_plugins: sdk: flutter flutter_plugin_android_lifecycle: ^2.0.0 - rxdart: ^0.26.0 + rxdart: ^0.27.4 dev_dependencies: flutter_test: