From 0b59115b4736d09d4f0c91d59408009bcecba34f Mon Sep 17 00:00:00 2001 From: Steven Spiel Date: Fri, 7 Jan 2022 13:56:29 -0500 Subject: [PATCH 1/4] Use LaunchImage in app switcher on iOS --- .../SwiftSecureApplicationPlugin.swift | 33 ++++++++----------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/ios/Classes/SwiftSecureApplicationPlugin.swift b/ios/Classes/SwiftSecureApplicationPlugin.swift index 2c59faa..864e377 100644 --- a/ios/Classes/SwiftSecureApplicationPlugin.swift +++ b/ios/Classes/SwiftSecureApplicationPlugin.swift @@ -28,27 +28,23 @@ public class SwiftSecureApplicationPlugin: NSObject, FlutterPlugin { 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) { + if let existingView = window.viewWithTag(99699) { 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 imageView = UIImageView.init(frame: window.bounds) + imageView.tag = 99699 + imageView.backgroundColor = UIColor.black + imageView.tintColor = UIColor.white + imageView.clipsToBounds = true + imageView.contentMode = .center + imageView.image = UIImage(named: "LaunchImage") + imageView.isMultipleTouchEnabled = true + imageView.translatesAutoresizingMaskIntoConstraints = false - 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(imageView) + window.bringSubviewToFront(imageView) - window.addSubview(blurEffectView) - window.bringSubviewToFront(blurEffectView) window.snapshotView(afterScreenUpdates: true) RunLoop.current.run(until: Date(timeIntervalSinceNow:0.5)) } @@ -86,14 +82,11 @@ public class SwiftSecureApplicationPlugin: NSObject, FlutterPlugin { } 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) { + }).first, let view = window.viewWithTag(99699) { UIView.animate(withDuration: 0.5, animations: { view.alpha = 0.0 - blurrView.alpha = 0.0 }, completion: { finished in view.removeFromSuperview() - blurrView.removeFromSuperview() - }) } } From eae8398892e6063fdb0f08dc69d3346ccd0ea15a Mon Sep 17 00:00:00 2001 From: Steven Spiel Date: Fri, 7 Jan 2022 15:19:13 -0500 Subject: [PATCH 2/4] make ios launch image visibility configurable --- .../SecureApplicationPlugin.kt | 3 + .../SwiftSecureApplicationPlugin.swift | 113 +++++++++++++----- lib/secure_application_native.dart | 6 + lib/secure_gate.dart | 16 +++ 4 files changed, 106 insertions(+), 32 deletions(-) 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..e9cbac9 100644 --- a/android/src/main/kotlin/org/jezequel/secure_application/SecureApplicationPlugin.kt +++ b/android/src/main/kotlin/org/jezequel/secure_application/SecureApplicationPlugin.kt @@ -85,6 +85,9 @@ public class SecureApplicationPlugin: FlutterPlugin, MethodCallHandler, Activity } else if (call.method == "open") { activity?.window?.clearFlags(LayoutParams.FLAG_SECURE) 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 { result.success(true) } diff --git a/ios/Classes/SwiftSecureApplicationPlugin.swift b/ios/Classes/SwiftSecureApplicationPlugin.swift index 864e377..541682a 100644 --- a/ios/Classes/SwiftSecureApplicationPlugin.swift +++ b/ios/Classes/SwiftSecureApplicationPlugin.swift @@ -4,7 +4,8 @@ import UIKit public class SwiftSecureApplicationPlugin: NSObject, FlutterPlugin { var secured = false; var opacity: CGFloat = 0.2; - + var useLaunchImage: Bool = false; + var backgroundTask: UIBackgroundTaskIdentifier! internal let registrar: FlutterPluginRegistrar @@ -28,25 +29,52 @@ public class SwiftSecureApplicationPlugin: NSObject, FlutterPlugin { if let window = UIApplication.shared.windows.filter({ (w) -> Bool in return w.isHidden == false }).first { - if let existingView = window.viewWithTag(99699) { - window.bringSubviewToFront(existingView) - return + if (useLaunchImage) { + if let existingView = window.viewWithTag(99697) { + window.bringSubviewToFront(existingView) + return + } else { + let imageView = UIImageView.init(frame: window.bounds) + imageView.tag = 99697 + imageView.backgroundColor = UIColor.black + imageView.tintColor = UIColor.white + 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)) + } } else { - let imageView = UIImageView.init(frame: window.bounds) - imageView.tag = 99699 - imageView.backgroundColor = UIColor.black - imageView.tintColor = UIColor.white - imageView.clipsToBounds = true - imageView.contentMode = .center - imageView.image = UIImage(named: "LaunchImage") - imageView.isMultipleTouchEnabled = true - imageView.translatesAutoresizingMaskIntoConstraints = false - - window.addSubview(imageView) - window.bringSubviewToFront(imageView) + 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) - window.snapshotView(afterScreenUpdates: true) - RunLoop.current.run(until: Date(timeIntervalSinceNow:0.5)) + 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() @@ -68,26 +96,47 @@ public class SwiftSecureApplicationPlugin: NSObject, FlutterPlugin { 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 + 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 + } } } 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 - } + } 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 == "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) { - UIView.animate(withDuration: 0.5, animations: { - view.alpha = 0.0 - }, completion: { finished in - view.removeFromSuperview() - }) + }).first { + if let colorView = window.viewWithTag(99699), let blurrView = window.viewWithTag(99698) { + UIView.animate(withDuration: 0.5, animations: { + colorView.alpha = 0.0 + }, completion: { finished in + colorView.removeFromSuperview() + blurrView.removeFromSuperview() + }) + } + + if let imageView = window.viewWithTag(99697) { + UIView.animate(withDuration: 0.3, animations: { + imageView.alpha = 0.0 + }, completion: { finished in + imageView.removeFromSuperview() + }) + } } } } diff --git a/lib/secure_application_native.dart b/lib/secure_application_native.dart index 6f710c0..f393ded 100644 --- a/lib/secure_application_native.dart +++ b/lib/secure_application_native.dart @@ -45,4 +45,10 @@ class SecureApplicationNative { static Future opacity(double opacity) { return _channel.invokeMethod('opacity', {"opacity": opacity}); } + + static Future useLaunchImageIOS(bool useLaunchImage) { + return _channel.invokeMethod('useLaunchImage', { + 'useLaunchImage': useLaunchImage, + }); + } } diff --git a/lib/secure_gate.dart b/lib/secure_gate.dart index 0c51b82..56b8dd2 100644 --- a/lib/secure_gate.dart +++ b/lib/secure_gate.dart @@ -27,12 +27,24 @@ 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. More info here: + /// https://docs.flutter.dev/development/ui/advanced/splash-screen#ios-launch-screen + /// + /// If this is true, [opacity] and [blurr] are ignored. + /// + /// Only available on iOS. It is not possible on Android, as far as I'm aware + final bool useLaunchImageIOS; + const SecureGate({ Key? key, required this.child, this.blurr = 20, this.opacity = 0.6, this.lockedBuilder, + this.useLaunchImageIOS = false, }) : super(key: key); @override _SecureGateState createState() => _SecureGateState(); @@ -50,6 +62,7 @@ class _SecureGateState extends State AnimationController(vsync: this, duration: kThemeAnimationDuration * 2) ..addListener(_handleChange); SecureApplicationNative.opacity(widget.opacity); + SecureApplicationNative.useLaunchImageIOS(widget.useLaunchImageIOS); super.initState(); } @@ -70,6 +83,9 @@ class _SecureGateState extends State if (oldWidget.opacity != widget.opacity) { SecureApplicationNative.opacity(widget.opacity); } + if (oldWidget.useLaunchImageIOS != widget.useLaunchImageIOS) { + SecureApplicationNative.useLaunchImageIOS(widget.useLaunchImageIOS); + } } void _sercureNotified() { From 2bd10feaa570eca7be61d87bf1b01ad0b3eb9ef5 Mon Sep 17 00:00:00 2001 From: Steven Spiel Date: Fri, 7 Jan 2022 15:20:15 -0500 Subject: [PATCH 3/4] minor formatting/documentation updates --- ios/Classes/SwiftSecureApplicationPlugin.swift | 2 +- lib/secure_gate.dart | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ios/Classes/SwiftSecureApplicationPlugin.swift b/ios/Classes/SwiftSecureApplicationPlugin.swift index 541682a..c79fae8 100644 --- a/ios/Classes/SwiftSecureApplicationPlugin.swift +++ b/ios/Classes/SwiftSecureApplicationPlugin.swift @@ -129,7 +129,7 @@ public class SwiftSecureApplicationPlugin: NSObject, FlutterPlugin { blurrView.removeFromSuperview() }) } - + if let imageView = window.viewWithTag(99697) { UIView.animate(withDuration: 0.3, animations: { imageView.alpha = 0.0 diff --git a/lib/secure_gate.dart b/lib/secure_gate.dart index 56b8dd2..43e9ac9 100644 --- a/lib/secure_gate.dart +++ b/lib/secure_gate.dart @@ -30,10 +30,10 @@ class SecureGate extends StatefulWidget { /// 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. More info here: - /// https://docs.flutter.dev/development/ui/advanced/splash-screen#ios-launch-screen + /// 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. + /// 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; From cb99a59b1f34a9464ab6dd179915fe63329014e7 Mon Sep 17 00:00:00 2001 From: Steven Spiel Date: Fri, 11 Mar 2022 23:12:33 -0500 Subject: [PATCH 4/4] add background color parameter for iOS --- .../SecureApplicationPlugin.kt | 3 ++ .../SwiftSecureApplicationPlugin.swift | 41 ++++++++++++++++--- lib/secure_application_native.dart | 8 +++- lib/secure_gate.dart | 16 +++++++- 4 files changed, 60 insertions(+), 8 deletions(-) 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 e9cbac9..7beae1f 100644 --- a/android/src/main/kotlin/org/jezequel/secure_application/SecureApplicationPlugin.kt +++ b/android/src/main/kotlin/org/jezequel/secure_application/SecureApplicationPlugin.kt @@ -88,6 +88,9 @@ public class SecureApplicationPlugin: FlutterPlugin, MethodCallHandler, Activity } 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/ios/Classes/SwiftSecureApplicationPlugin.swift b/ios/Classes/SwiftSecureApplicationPlugin.swift index c79fae8..36b4356 100644 --- a/ios/Classes/SwiftSecureApplicationPlugin.swift +++ b/ios/Classes/SwiftSecureApplicationPlugin.swift @@ -5,6 +5,7 @@ public class SwiftSecureApplicationPlugin: NSObject, FlutterPlugin { var secured = false; var opacity: CGFloat = 0.2; var useLaunchImage: Bool = false; + var backgroundColor: UIColor = UIColor.white; var backgroundTask: UIBackgroundTaskIdentifier! @@ -36,8 +37,7 @@ public class SwiftSecureApplicationPlugin: NSObject, FlutterPlugin { } else { let imageView = UIImageView.init(frame: window.bounds) imageView.tag = 99697 - imageView.backgroundColor = UIColor.black - imageView.tintColor = UIColor.white + imageView.backgroundColor = backgroundColor imageView.clipsToBounds = true imageView.contentMode = .center imageView.image = UIImage(named: "LaunchImage") @@ -59,7 +59,7 @@ public class SwiftSecureApplicationPlugin: NSObject, FlutterPlugin { let colorView = UIView(frame: window.bounds); colorView.tag = 99699 colorView.autoresizingMask = [.flexibleWidth, .flexibleHeight] - colorView.backgroundColor = UIColor(white: 1, alpha: opacity) + colorView.backgroundColor = backgroundColor.withAlphaComponent(opacity) window.addSubview(colorView) window.bringSubviewToFront(colorView) @@ -102,8 +102,12 @@ public class SwiftSecureApplicationPlugin: NSObject, FlutterPlugin { } if let useLaunchImage = args["useLaunchImage"] as? Bool { - self.useLaunchImage = useLaunchImage - } + self.useLaunchImage = useLaunchImage + } + + if let backgroundColor = args["backgroundColor"] as? String { + self.backgroundColor = hexStringToUIColor(hex: backgroundColor) + } } } else if (call.method == "open") { secured = false; @@ -112,6 +116,11 @@ public class SwiftSecureApplicationPlugin: NSObject, FlutterPlugin { 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 { @@ -140,4 +149,26 @@ public class SwiftSecureApplicationPlugin: NSObject, FlutterPlugin { } } } + + 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_native.dart b/lib/secure_application_native.dart index f393ded..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 { @@ -51,4 +51,10 @@ class SecureApplicationNative { '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 43e9ac9..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 /// @@ -38,6 +38,9 @@ class SecureGate extends StatefulWidget { /// 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, @@ -45,7 +48,9 @@ class SecureGate extends StatefulWidget { this.opacity = 0.6, this.lockedBuilder, this.useLaunchImageIOS = false, + this.backgroundColor, }) : super(key: key); + @override _SecureGateState createState() => _SecureGateState(); } @@ -63,6 +68,9 @@ class _SecureGateState extends State ..addListener(_handleChange); SecureApplicationNative.opacity(widget.opacity); SecureApplicationNative.useLaunchImageIOS(widget.useLaunchImageIOS); + if (widget.backgroundColor != null) { + SecureApplicationNative.backgroundColor(widget.backgroundColor!); + } super.initState(); } @@ -86,6 +94,10 @@ class _SecureGateState extends State if (oldWidget.useLaunchImageIOS != widget.useLaunchImageIOS) { SecureApplicationNative.useLaunchImageIOS(widget.useLaunchImageIOS); } + if (oldWidget.backgroundColor != widget.backgroundColor && + widget.backgroundColor != null) { + SecureApplicationNative.backgroundColor(widget.backgroundColor!); + } } void _sercureNotified() { @@ -124,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)), ), ),