Commit 1bead2d6 by Maurits van Beusekom

Production release 1.0.0

parents 2fbc01a2 2d0802b7
## 0.0.1 ## 1.0.0
* TODO: Describe initial release. * Initial release.
...@@ -2,7 +2,7 @@ group 'com.baseflow.permissionhandler' ...@@ -2,7 +2,7 @@ group 'com.baseflow.permissionhandler'
version '1.0-SNAPSHOT' version '1.0-SNAPSHOT'
buildscript { buildscript {
ext.kotlin_version = '1.1.51' ext.kotlin_version = '1.2.51'
repositories { repositories {
google() google()
jcenter() jcenter()
......
...@@ -99,7 +99,7 @@ class PermissionHandlerPlugin(private val registrar: Registrar, private var requ ...@@ -99,7 +99,7 @@ class PermissionHandlerPlugin(private val registrar: Registrar, private var requ
call.method == "requestPermissions" -> { call.method == "requestPermissions" -> {
if (mResult != null) { if (mResult != null) {
result.error( result.error(
"ERROR_ALREADY_REQUESTED_PERMISSIONS", "ERROR_ALREADY_REQUESTING_PERMISSIONS",
"A request for permissions is already running, please wait for it to finish before doing another request (note that you can request multiple permissions at the same time).", "A request for permissions is already running, please wait for it to finish before doing another request (note that you can request multiple permissions at the same time).",
null) null)
} }
...@@ -220,10 +220,14 @@ class PermissionHandlerPlugin(private val registrar: Registrar, private var requ ...@@ -220,10 +220,14 @@ class PermissionHandlerPlugin(private val registrar: Registrar, private var requ
} }
} }
ActivityCompat.requestPermissions( if (permissionsToRequest.count() > 0) {
registrar.activity(), ActivityCompat.requestPermissions(
permissionsToRequest.toTypedArray(), registrar.activity(),
permissionCode) permissionsToRequest.toTypedArray(),
permissionCode)
} else if (mRequestResults.count() > 0) {
processResult()
}
} }
private fun handlePermissionsRequest(permissions: Array<String>, grantResults: IntArray) { private fun handlePermissionsRequest(permissions: Array<String>, grantResults: IntArray) {
......
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<Objective-C-extensions>
<file>
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Import" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Macro" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Typedef" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Enum" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Constant" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Global" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Struct" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="FunctionPredecl" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Function" />
</file>
<class>
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Property" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Synthesize" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InitMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="StaticMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InstanceMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="DeallocMethod" />
</class>
<extensions>
<pair source="cpp" header="h" fileNamingConvention="NONE" />
<pair source="c" header="h" fileNamingConvention="NONE" />
</extensions>
</Objective-C-extensions>
</code_scheme>
</component>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$/../../android" />
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
<option name="resolveModulePerSourceSet" value="false" />
</GradleProjectSettings>
</option>
</component>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="NullableNotNullManager">
<option name="myDefaultNullable" value="android.support.annotation.Nullable" />
<option name="myDefaultNotNull" value="android.support.annotation.NonNull" />
<option name="myNullables">
<value>
<list size="5">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
<item index="2" class="java.lang.String" itemvalue="javax.annotation.CheckForNull" />
<item index="3" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
<item index="4" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
</list>
</value>
</option>
<option name="myNotNulls">
<value>
<list size="4">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.NonNull" />
</list>
</value>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_7" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/android.iml" filepath="$PROJECT_DIR$/android.iml" />
<module fileurl="file://$PROJECT_DIR$/app/app.iml" filepath="$PROJECT_DIR$/app/app.iml" />
<module fileurl="file://$PROJECT_DIR$/../../android/permission_handler.iml" filepath="$PROJECT_DIR$/../../android/permission_handler.iml" />
</modules>
</component>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
</set>
</option>
</component>
</project>
\ No newline at end of file
...@@ -6,8 +6,28 @@ ...@@ -6,8 +6,28 @@
to allow setting breakpoints, to provide hot reload, etc. to allow setting breakpoints, to provide hot reload, etc.
--> -->
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_CALENDAR"/> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.WRITE_CALENDAR"/> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.RECEIVE_MMS" />
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.RECEIVE_WAP_PUSH" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="com.android.voicemail.permission.ADD_VOICEMAIL" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.READ_CALL_LOG" />
<uses-permission android:name="android.permission.READ_CALENDAR" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_SMS" />
<uses-permission android:name="android.permission.SEND_SMS" />
<uses-permission android:name="android.permission.WRITE_CALENDAR" />
<uses-permission android:name="android.permission.WRITE_CALL_LOG" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.SENSORS" />
<uses-permission android:name="android.permission.BODY_SENSORS" />
<!-- io.flutter.app.FlutterApplication is an android.app.Application that <!-- io.flutter.app.FlutterApplication is an android.app.Application that
calls FlutterMain.startInitialization(this); in its onCreate method. calls FlutterMain.startInitialization(this); in its onCreate method.
......
...@@ -444,7 +444,7 @@ ...@@ -444,7 +444,7 @@
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_SWIFT3_OBJC_INFERENCE = On; SWIFT_SWIFT3_OBJC_INFERENCE = Default;
SWIFT_VERSION = 4.0; SWIFT_VERSION = 4.0;
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";
}; };
...@@ -471,7 +471,7 @@ ...@@ -471,7 +471,7 @@
PRODUCT_BUNDLE_IDENTIFIER = com.baseflow.permissionHandlerExample; PRODUCT_BUNDLE_IDENTIFIER = com.baseflow.permissionHandlerExample;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_SWIFT3_OBJC_INFERENCE = On; SWIFT_SWIFT3_OBJC_INFERENCE = Default;
SWIFT_VERSION = 4.0; SWIFT_VERSION = 4.0;
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";
}; };
......
...@@ -46,7 +46,13 @@ ...@@ -46,7 +46,13 @@
<key>UIViewControllerBasedStatusBarAppearance</key> <key>UIViewControllerBasedStatusBarAppearance</key>
<false/> <false/>
<key>NSLocationWhenInUseUsageDescription</key> <key>NSLocationWhenInUseUsageDescription</key>
<string>Can I haz location?</string> <string>Need location when in use</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Always and when in use!</string>
<key>NSLocationUsageDescription</key>
<string>Older devices need location.</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>Can I haz location always?</string>
<key>NSAppleMusicUsageDescription</key> <key>NSAppleMusicUsageDescription</key>
<string>Music!</string> <string>Music!</string>
<key>NSBluetoothPeripheralUsageDescription</key> <key>NSBluetoothPeripheralUsageDescription</key>
......
import 'dart:async'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:permission_handler/permission_enums.dart'; import 'package:permission_handler/permission_enums.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
void main() => runApp(new MyApp()); void main() => runApp(new MyApp());
class MyApp extends StatefulWidget { class MyApp extends StatelessWidget {
@override @override
_MyAppState createState() => new _MyAppState(); Widget build(BuildContext context) {
return new MaterialApp(
home: new Scaffold(
appBar: new AppBar(
title: const Text('Plugin example app'),
actions: <Widget>[
IconButton(
icon: const Icon(Icons.settings),
onPressed: () {
PermissionHandler.openAppSettings();
},
)
],
),
body: new Center(
child: new ListView(
children: PermissionGroup.values
.where((PermissionGroup permission) {
if (Platform.isIOS) {
return permission != PermissionGroup.unknown &&
permission != PermissionGroup.phone &&
permission != PermissionGroup.sms &&
permission != PermissionGroup.storage;
} else {
return permission != PermissionGroup.unknown &&
permission != PermissionGroup.mediaLibrary &&
permission != PermissionGroup.photos &&
permission != PermissionGroup.reminders;
}
})
.map((PermissionGroup permission) =>
new PermissionWidget(permission))
.toList()),
),
));
}
}
class PermissionWidget extends StatefulWidget {
final PermissionGroup _permissionGroup;
const PermissionWidget(this._permissionGroup);
@override
_PermissionState createState() => _PermissionState(_permissionGroup);
} }
class _MyAppState extends State<MyApp> { class _PermissionState extends State<PermissionWidget> {
String _permissionStatus = 'Unknown'; final PermissionGroup _permissionGroup;
PermissionStatus _permissionStatus = PermissionStatus.unknown;
_PermissionState(this._permissionGroup);
@override @override
void initState() { void initState() {
super.initState(); super.initState();
initPlatformState();
}
// Platform messages are asynchronous, so we initialize in an async method. _listenForPermissionStatus();
Future<void> initPlatformState() async { }
PermissionStatus permissionStatus;
// Platform messages may fail, so we use a try/catch PlatformException.
try {
permissionStatus = await PermissionHandler
.checkPermissionStatus(PermissionGroup.calendar);
/*
if (permissionStatus != PermissionStatus.granted) {
final bool shouldShowRationale = await PermissionHandler
.shouldShowRequestPermissionRationale(PermissionGroup.calendar);
if (shouldShowRationale) {
final Map<PermissionGroup, PermissionStatus> permissions =
await PermissionHandler.requestPermissions(
<PermissionGroup>[PermissionGroup.calendar]);
if (permissions.containsKey(PermissionGroup.calendar)) {
permissionStatus = permissions[PermissionGroup.calendar];
}
}
}
*/
} on PlatformException {
permissionStatus = PermissionStatus.unknown;
}
// If the widget was removed from the tree while the asynchronous platform void _listenForPermissionStatus() async {
// message was in flight, we want to discard the reply rather than calling final PermissionStatus status =
// setState to update our non-existent appearance. await PermissionHandler.checkPermissionStatus(_permissionGroup);
if (!mounted) {
return;
}
setState(() { setState(() {
_permissionStatus = permissionStatus.toString(); _permissionStatus = status;
}); });
} }
Color getPermissionColor() {
switch (_permissionStatus) {
case PermissionStatus.denied:
return Colors.red;
case PermissionStatus.granted:
return Colors.green;
default:
return Colors.grey;
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new MaterialApp( return new ListTile(
home: new Scaffold( title: new Text(_permissionGroup.toString()),
appBar: new AppBar( subtitle: new Text(
title: const Text('Plugin example app'), _permissionStatus.toString(),
), style: new TextStyle(color: getPermissionColor()),
body: new Center(
child: new Column(
children: <Widget>[
new Text('Running on: $_permissionStatus\n'),
new RaisedButton(
child: const Text('Open settings'),
onPressed: () async =>
await PermissionHandler.openAppSettings(),
),
],
),
),
), ),
onTap: () async {
requestPermission(_permissionGroup);
},
); );
} }
void requestPermission(PermissionGroup permission) async {
final List<PermissionGroup> permissions = <PermissionGroup>[permission];
final Map<PermissionGroup, PermissionStatus> permissionRequestResult =
await PermissionHandler.requestPermissions(permissions);
setState(() {
_permissionStatus = permissionRequestResult[permission];
});
}
} }
...@@ -8,8 +8,12 @@ ...@@ -8,8 +8,12 @@
import Flutter import Flutter
import Foundation import Foundation
import UIKit import UIKit
import Swift
typealias PermissionRequestCompletion = (_ permissionRequestResults: [PermissionGroup:PermissionStatus]) -> ()
class PermissionManager: NSObject { class PermissionManager: NSObject {
private var _strategyInstances: [ObjectIdentifier: PermissionStrategy] = [:]
static func checkPermissionStatus(permission: PermissionGroup, result: @escaping FlutterResult) { static func checkPermissionStatus(permission: PermissionGroup, result: @escaping FlutterResult) {
let permissionStrategy = PermissionManager.createPermissionStrategy(permission: permission) let permissionStrategy = PermissionManager.createPermissionStrategy(permission: permission)
...@@ -18,6 +22,28 @@ class PermissionManager: NSObject { ...@@ -18,6 +22,28 @@ class PermissionManager: NSObject {
result(Codec.encodePermissionStatus(permissionStatus: permissionStatus)) result(Codec.encodePermissionStatus(permissionStatus: permissionStatus))
} }
func requestPermissions(permissions: [PermissionGroup], completion: @escaping PermissionRequestCompletion) {
var requestQueue = Set(permissions.map { $0 })
var permissionStatusResult: [PermissionGroup: PermissionStatus] = [:]
for permission in permissions {
let permissionStrategy = PermissionManager.createPermissionStrategy(permission: permission)
let identifier = ObjectIdentifier(permissionStrategy as AnyObject)
_strategyInstances[identifier] = permissionStrategy
permissionStrategy.requestPermission(permission: permission) { (permissionStatus: PermissionStatus) in
permissionStatusResult[permission] = permissionStatus
requestQueue.remove(permission)
self._strategyInstances.removeValue(forKey: ObjectIdentifier(permissionStrategy as AnyObject))
if requestQueue.count == 0 {
completion(permissionStatusResult)
return
}
}
}
}
static func openAppSettings(result: @escaping FlutterResult) { static func openAppSettings(result: @escaping FlutterResult) {
if #available(iOS 8.0, *) { if #available(iOS 8.0, *) {
if #available(iOS 10, *) { if #available(iOS 10, *) {
...@@ -58,6 +84,8 @@ class PermissionManager: NSObject { ...@@ -58,6 +84,8 @@ class PermissionManager: NSObject {
return SensorPermissionStrategy() return SensorPermissionStrategy()
case PermissionGroup.speech: case PermissionGroup.speech:
return SpeechPermissionStrategy() return SpeechPermissionStrategy()
default:
return UnknownPermissionStrategy()
} }
} }
} }
...@@ -9,6 +9,8 @@ import UIKit ...@@ -9,6 +9,8 @@ import UIKit
public class SwiftPermissionHandlerPlugin: NSObject, FlutterPlugin { public class SwiftPermissionHandlerPlugin: NSObject, FlutterPlugin {
private static let METHOD_CHANNEL_NAME = "flutter.baseflow.com/permissions/methods"; private static let METHOD_CHANNEL_NAME = "flutter.baseflow.com/permissions/methods";
private let _permissionManager: PermissionManager = PermissionManager()
private var _methodResult: FlutterResult?
public static func register(with registrar: FlutterPluginRegistrar) { public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: METHOD_CHANNEL_NAME, binaryMessenger: registrar.messenger()) let channel = FlutterMethodChannel(name: METHOD_CHANNEL_NAME, binaryMessenger: registrar.messenger())
...@@ -18,9 +20,33 @@ public class SwiftPermissionHandlerPlugin: NSObject, FlutterPlugin { ...@@ -18,9 +20,33 @@ public class SwiftPermissionHandlerPlugin: NSObject, FlutterPlugin {
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
if call.method == "checkPermissionStatus" { if call.method == "checkPermissionStatus" {
let permission: PermissionGroup = Codec.decodePermissionGroup(from: call.arguments)
PermissionManager.checkPermissionStatus( PermissionManager.checkPermissionStatus(
permission: Codec.decodePermissionGroup(from: call.arguments), permission: permission,
result: result) result: result)
} else if call.method == "requestPermissions" {
if _methodResult != nil {
result(FlutterError(
code: "ERROR_ALREADY_REQUESTING_PERMISSIONS",
message: "A request for permissions is already running, please wait for it to finish before doing another request (note that you can request multiple permissions at the same time).",
details: nil))
}
_methodResult = result
let permissions: [PermissionGroup] = Codec.decodePermissionGroups(from: call.arguments)
_permissionManager.requestPermissions(permissions: permissions) {
(permissionRequestResults: [PermissionGroup:PermissionStatus]) in
if self._methodResult != nil {
self._methodResult!(Codec.encodePermissionRequestResult(permissionStatusResult: permissionRequestResults))
}
self._methodResult = nil
}
} else if call.method == "shouldShowRequestPermissionRationale" {
result(false)
} else if call.method == "openAppSettings" { } else if call.method == "openAppSettings" {
PermissionManager.openAppSettings(result: result) PermissionManager.openAppSettings(result: result)
} else { } else {
......
...@@ -16,8 +16,11 @@ enum PermissionGroup : String, Codable { ...@@ -16,8 +16,11 @@ enum PermissionGroup : String, Codable {
case locationWhenInUse = "locationWhenInUse" case locationWhenInUse = "locationWhenInUse"
case mediaLibrary = "mediaLibrary" case mediaLibrary = "mediaLibrary"
case microphone = "microphone" case microphone = "microphone"
case phone = "phone"
case photos = "photos" case photos = "photos"
case reminders = "reminders" case reminders = "reminders"
case sensors = "sensors" case sensors = "sensors"
case sms = "sms"
case speech = "speech" case speech = "speech"
case storage = "storage"
} }
...@@ -19,11 +19,6 @@ class AudioVideoPermissionStrategy : NSObject, PermissionStrategy { ...@@ -19,11 +19,6 @@ class AudioVideoPermissionStrategy : NSObject, PermissionStrategy {
return PermissionStatus.unknown return PermissionStatus.unknown
} }
func requestPermission(permission: PermissionGroup) -> PermissionStatus {
// TODO: Add implementation
return PermissionStatus.unknown
}
private static func getPermissionStatus(mediaType: AVMediaType) -> PermissionStatus { private static func getPermissionStatus(mediaType: AVMediaType) -> PermissionStatus {
let status: AVAuthorizationStatus = AVCaptureDevice.authorizationStatus(for: mediaType) let status: AVAuthorizationStatus = AVCaptureDevice.authorizationStatus(for: mediaType)
...@@ -38,4 +33,33 @@ class AudioVideoPermissionStrategy : NSObject, PermissionStrategy { ...@@ -38,4 +33,33 @@ class AudioVideoPermissionStrategy : NSObject, PermissionStrategy {
return PermissionStatus.unknown return PermissionStatus.unknown
} }
} }
func requestPermission(permission: PermissionGroup, completionHandler: @escaping PermissionStatusHandler) {
let permissionStatus = checkPermissionStatus(permission: permission)
if permissionStatus != PermissionStatus.unknown {
completionHandler(permissionStatus)
return
}
var mediaType: AVMediaType
if permission == PermissionGroup.camera {
mediaType = AVMediaType.video
} else if permission == PermissionGroup.microphone {
mediaType = AVMediaType.audio
} else {
completionHandler(PermissionStatus.unknown)
return
}
AVCaptureDevice.requestAccess(for: mediaType, completionHandler: {
(granted: Bool) in
if granted {
completionHandler(PermissionStatus.granted)
} else {
completionHandler(PermissionStatus.denied)
}
})
}
} }
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
// //
import AddressBook import AddressBook
import Contacts
import Foundation import Foundation
class ContactPermissionStrategy : NSObject, PermissionStrategy { class ContactPermissionStrategy : NSObject, PermissionStrategy {
...@@ -14,12 +15,22 @@ class ContactPermissionStrategy : NSObject, PermissionStrategy { ...@@ -14,12 +15,22 @@ class ContactPermissionStrategy : NSObject, PermissionStrategy {
return ContactPermissionStrategy.getPermissionStatus() return ContactPermissionStrategy.getPermissionStatus()
} }
func requestPermission(permission: PermissionGroup) -> PermissionStatus {
// TODO: Add implementation
return PermissionStatus.unknown
}
private static func getPermissionStatus() -> PermissionStatus { private static func getPermissionStatus() -> PermissionStatus {
if #available(iOS 9.0, *) {
let status: CNAuthorizationStatus = CNContactStore.authorizationStatus(for: .contacts)
switch status {
case CNAuthorizationStatus.authorized:
return PermissionStatus.granted
case CNAuthorizationStatus.denied:
return PermissionStatus.denied
case CNAuthorizationStatus.restricted:
return PermissionStatus.restricted
default:
return PermissionStatus.unknown
}
}
let status: ABAuthorizationStatus = ABAddressBookGetAuthorizationStatus() let status: ABAuthorizationStatus = ABAddressBookGetAuthorizationStatus()
switch status { switch status {
...@@ -33,4 +44,44 @@ class ContactPermissionStrategy : NSObject, PermissionStrategy { ...@@ -33,4 +44,44 @@ class ContactPermissionStrategy : NSObject, PermissionStrategy {
return PermissionStatus.unknown return PermissionStatus.unknown
} }
} }
func requestPermission(permission: PermissionGroup, completionHandler: @escaping PermissionStatusHandler) {
let permissionStatus = checkPermissionStatus(permission: permission)
if permissionStatus != PermissionStatus.unknown {
completionHandler(permissionStatus)
return
}
if #available(iOS 9.0, *) {
ContactPermissionStrategy.requestPermissionsFromContactStore(completionHandler: completionHandler)
} else {
ContactPermissionStrategy.requestPermissionsFromAddressBook(completionHandler: completionHandler)
}
}
@available(iOS 9.0, *)
private static func requestPermissionsFromContactStore(completionHandler: @escaping PermissionStatusHandler) {
let contactStore = CNContactStore.init()
contactStore.requestAccess(for: .contacts, completionHandler: {
(authorized: Bool, error: Error?) in
if authorized {
completionHandler(PermissionStatus.granted)
} else {
completionHandler(PermissionStatus.denied)
}
})
}
private static func requestPermissionsFromAddressBook(completionHandler: @escaping PermissionStatusHandler) {
ABAddressBookRequestAccessWithCompletion(ABAddressBookCreate() as ABAddressBook, {
(granted: Bool, error: CFError?) in
if granted {
completionHandler(PermissionStatus.granted)
} else {
completionHandler(PermissionStatus.denied)
}
})
}
} }
...@@ -20,11 +20,6 @@ class EventPermissionStrategy : NSObject, PermissionStrategy { ...@@ -20,11 +20,6 @@ class EventPermissionStrategy : NSObject, PermissionStrategy {
return PermissionStatus.unknown return PermissionStatus.unknown
} }
func requestPermission(permission: PermissionGroup) -> PermissionStatus {
// TODO: Add implementation
return PermissionStatus.unknown
}
private static func getPermissionStatus(entityType: EKEntityType) -> PermissionStatus { private static func getPermissionStatus(entityType: EKEntityType) -> PermissionStatus {
let status: EKAuthorizationStatus = EKEventStore.authorizationStatus(for: entityType) let status: EKAuthorizationStatus = EKEventStore.authorizationStatus(for: entityType)
...@@ -40,4 +35,33 @@ class EventPermissionStrategy : NSObject, PermissionStrategy { ...@@ -40,4 +35,33 @@ class EventPermissionStrategy : NSObject, PermissionStrategy {
} }
} }
func requestPermission(permission: PermissionGroup, completionHandler: @escaping PermissionStatusHandler) {
let permissionStatus = checkPermissionStatus(permission: permission)
if permissionStatus != PermissionStatus.unknown {
completionHandler(permissionStatus)
return
}
var entityType: EKEntityType
if permission == PermissionGroup.calendar {
entityType = EKEntityType.event
} else if permission == PermissionGroup.reminders {
entityType = EKEntityType.reminder
} else {
completionHandler(PermissionStatus.unknown)
return
}
let eventStore: EKEventStore = EKEventStore.init()
eventStore.requestAccess(to: entityType) { (granted: Bool, error: Error?) in
if granted {
completionHandler(PermissionStatus.granted)
} else {
completionHandler(PermissionStatus.denied)
}
}
}
} }
...@@ -8,17 +8,15 @@ ...@@ -8,17 +8,15 @@
import CoreLocation import CoreLocation
import Foundation import Foundation
class LocationPermissionStrategy : NSObject, PermissionStrategy { class LocationPermissionStrategy : NSObject, PermissionStrategy, CLLocationManagerDelegate {
private var _locationManager: CLLocationManager? = nil
private var _permissionStatusHandler: PermissionStatusHandler? = nil
private var _requestedPermission: PermissionGroup? = nil
func checkPermissionStatus(permission: PermissionGroup) -> PermissionStatus { func checkPermissionStatus(permission: PermissionGroup) -> PermissionStatus {
return LocationPermissionStrategy.getPermissionStatus(permission: permission) return LocationPermissionStrategy.getPermissionStatus(permission: permission)
} }
func requestPermission(permission: PermissionGroup) -> PermissionStatus {
// TODO: Add implementation
return PermissionStatus.unknown
}
private static func getPermissionStatus(permission: PermissionGroup) -> PermissionStatus { private static func getPermissionStatus(permission: PermissionGroup) -> PermissionStatus {
if !CLLocationManager.locationServicesEnabled() { if !CLLocationManager.locationServicesEnabled() {
return PermissionStatus.disabled return PermissionStatus.disabled
...@@ -26,9 +24,69 @@ class LocationPermissionStrategy : NSObject, PermissionStrategy { ...@@ -26,9 +24,69 @@ class LocationPermissionStrategy : NSObject, PermissionStrategy {
let status: CLAuthorizationStatus = CLLocationManager.authorizationStatus() let status: CLAuthorizationStatus = CLLocationManager.authorizationStatus()
return LocationPermissionStrategy.determinePermissionStatus(
permission: permission,
authorizationStatus: status)
}
func requestPermission(permission: PermissionGroup, completionHandler: @escaping PermissionStatusHandler) {
let permissionStatus = checkPermissionStatus(permission: permission)
if CLLocationManager.authorizationStatus() == CLAuthorizationStatus.authorizedWhenInUse && permission == PermissionGroup.locationAlways {
// don't do anything and continue requesting permissions
} else if permissionStatus != PermissionStatus.unknown {
completionHandler(permissionStatus)
return
}
_permissionStatusHandler = completionHandler
_requestedPermission = permission
if(_locationManager == nil) {
_locationManager = CLLocationManager()
_locationManager!.delegate = self
}
if(permission == PermissionGroup.location) {
if (Bundle.main.object(forInfoDictionaryKey: "NSLocationAlwaysUsageDescription") != nil) {
_locationManager!.requestAlwaysAuthorization()
} else if (Bundle.main.object(forInfoDictionaryKey: "NSLocationWhenInUseUsageDescription") != nil) {
_locationManager!.requestWhenInUseAuthorization();
} else {
NSException(name: NSExceptionName.internalInconsistencyException, reason:"To use location in iOS8 you need to define either NSLocationWhenInUseUsageDescription or NSLocationAlwaysUsageDescription in the app bundle's Info.plist file", userInfo: nil).raise()
}
} else if permission == PermissionGroup.locationAlways {
if (Bundle.main.object(forInfoDictionaryKey: "NSLocationAlwaysUsageDescription") != nil) {
_locationManager!.requestAlwaysAuthorization();
} else {
NSException(name: NSExceptionName.internalInconsistencyException, reason:"To use location in iOS8 you need to define NSLocationAlwaysUsageDescription in the app bundle's Info.plist file", userInfo: nil).raise()
}
} else if permission == PermissionGroup.locationWhenInUse {
if (Bundle.main.object(forInfoDictionaryKey: "NSLocationWhenInUseUsageDescription") != nil) {
_locationManager!.requestWhenInUseAuthorization();
} else {
NSException(name: NSExceptionName.internalInconsistencyException, reason:"To use location in iOS8 you need to define NSLocationWhenInUseUsageDescription in the app bundle's Info.plist file", userInfo: nil).raise()
}
}
}
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
if status == CLAuthorizationStatus.notDetermined {
return
}
guard let completionHandler = _permissionStatusHandler else { return }
completionHandler(
LocationPermissionStrategy.determinePermissionStatus(
permission: _requestedPermission!,
authorizationStatus: status))
}
private static func determinePermissionStatus(permission: PermissionGroup, authorizationStatus: CLAuthorizationStatus) -> PermissionStatus {
if #available(iOS 8.0, *) { if #available(iOS 8.0, *) {
if permission == PermissionGroup.locationAlways { if permission == PermissionGroup.locationAlways {
switch status { switch authorizationStatus {
case CLAuthorizationStatus.authorizedAlways: case CLAuthorizationStatus.authorizedAlways:
return PermissionStatus.granted return PermissionStatus.granted
case CLAuthorizationStatus.authorizedWhenInUse, case CLAuthorizationStatus.authorizedWhenInUse,
...@@ -41,7 +99,7 @@ class LocationPermissionStrategy : NSObject, PermissionStrategy { ...@@ -41,7 +99,7 @@ class LocationPermissionStrategy : NSObject, PermissionStrategy {
} }
} }
switch status { switch authorizationStatus {
case CLAuthorizationStatus.authorizedAlways, case CLAuthorizationStatus.authorizedAlways,
CLAuthorizationStatus.authorizedWhenInUse: CLAuthorizationStatus.authorizedWhenInUse:
return PermissionStatus.granted return PermissionStatus.granted
...@@ -54,7 +112,7 @@ class LocationPermissionStrategy : NSObject, PermissionStrategy { ...@@ -54,7 +112,7 @@ class LocationPermissionStrategy : NSObject, PermissionStrategy {
} }
} }
switch status { switch authorizationStatus {
case CLAuthorizationStatus.authorized: case CLAuthorizationStatus.authorized:
return PermissionStatus.granted return PermissionStatus.granted
case CLAuthorizationStatus.denied: case CLAuthorizationStatus.denied:
......
...@@ -14,27 +14,45 @@ class MediaLibraryPermissionStrategy : NSObject, PermissionStrategy { ...@@ -14,27 +14,45 @@ class MediaLibraryPermissionStrategy : NSObject, PermissionStrategy {
return MediaLibraryPermissionStrategy.getPermissionStatus() return MediaLibraryPermissionStrategy.getPermissionStatus()
} }
func requestPermission(permission: PermissionGroup) -> PermissionStatus {
// TODO: Add implementation
return PermissionStatus.unknown
}
private static func getPermissionStatus() -> PermissionStatus { private static func getPermissionStatus() -> PermissionStatus {
if #available(iOS 9.3, *) { if #available(iOS 9.3, *) {
let status: MPMediaLibraryAuthorizationStatus = MPMediaLibrary.authorizationStatus() let status: MPMediaLibraryAuthorizationStatus = MPMediaLibrary.authorizationStatus()
switch status { return MediaLibraryPermissionStrategy.determinePermissionStatus(authorizationStatus: status)
case MPMediaLibraryAuthorizationStatus.authorized:
return PermissionStatus.granted
case MPMediaLibraryAuthorizationStatus.denied:
return PermissionStatus.denied
case MPMediaLibraryAuthorizationStatus.restricted:
return PermissionStatus.restricted
default:
return PermissionStatus.unknown
}
} }
return PermissionStatus.unknown return PermissionStatus.unknown
} }
func requestPermission(permission: PermissionGroup, completionHandler: @escaping PermissionStatusHandler) -> Void {
let status = checkPermissionStatus(permission: permission)
if status != PermissionStatus.unknown {
completionHandler(status)
return
}
if #available(iOS 9.3, *) {
MPMediaLibrary.requestAuthorization { (status: MPMediaLibraryAuthorizationStatus) in
completionHandler(
MediaLibraryPermissionStrategy.determinePermissionStatus(authorizationStatus: status))
}
} else {
completionHandler(PermissionStatus.unknown)
return
}
}
@available(iOS 9.3, *)
private static func determinePermissionStatus(authorizationStatus: MPMediaLibraryAuthorizationStatus) -> PermissionStatus {
switch authorizationStatus {
case MPMediaLibraryAuthorizationStatus.authorized:
return PermissionStatus.granted
case MPMediaLibraryAuthorizationStatus.denied:
return PermissionStatus.denied
case MPMediaLibraryAuthorizationStatus.restricted:
return PermissionStatus.restricted
default:
return PermissionStatus.unknown
}
}
} }
...@@ -7,7 +7,9 @@ ...@@ -7,7 +7,9 @@
import Foundation import Foundation
typealias PermissionStatusHandler = (_ permissionStatus: PermissionStatus) -> Void
protocol PermissionStrategy { protocol PermissionStrategy {
func checkPermissionStatus(permission: PermissionGroup) -> PermissionStatus func checkPermissionStatus(permission: PermissionGroup) -> PermissionStatus
func requestPermission(permission: PermissionGroup) -> PermissionStatus func requestPermission(permission: PermissionGroup, completionHandler: @escaping PermissionStatusHandler)
} }
...@@ -14,15 +14,28 @@ class PhotoPermissionStrategy : NSObject, PermissionStrategy { ...@@ -14,15 +14,28 @@ class PhotoPermissionStrategy : NSObject, PermissionStrategy {
return PhotoPermissionStrategy.getPermissionStatus() return PhotoPermissionStrategy.getPermissionStatus()
} }
func requestPermission(permission: PermissionGroup) -> PermissionStatus {
// TODO: Add implementation
return PermissionStatus.unknown
}
private static func getPermissionStatus() -> PermissionStatus { private static func getPermissionStatus() -> PermissionStatus {
let status: PHAuthorizationStatus = PHPhotoLibrary.authorizationStatus() let status: PHAuthorizationStatus = PHPhotoLibrary.authorizationStatus()
switch status { return PhotoPermissionStrategy.determinePermissionStatus(authorizationStatus: status)
}
func requestPermission(permission: PermissionGroup, completionHandler: @escaping PermissionStatusHandler) {
let status = checkPermissionStatus(permission: permission)
if status != PermissionStatus.unknown {
completionHandler(status)
return
}
PHPhotoLibrary.requestAuthorization { (authorizationStatus: PHAuthorizationStatus) in
completionHandler(
PhotoPermissionStrategy.determinePermissionStatus(authorizationStatus: authorizationStatus))
}
}
private static func determinePermissionStatus(authorizationStatus: PHAuthorizationStatus) -> PermissionStatus {
switch authorizationStatus {
case PHAuthorizationStatus.authorized: case PHAuthorizationStatus.authorized:
return PermissionStatus.granted return PermissionStatus.granted
case PHAuthorizationStatus.denied: case PHAuthorizationStatus.denied:
......
...@@ -14,11 +14,6 @@ class SensorPermissionStrategy : NSObject, PermissionStrategy { ...@@ -14,11 +14,6 @@ class SensorPermissionStrategy : NSObject, PermissionStrategy {
return SensorPermissionStrategy.getPermissionStatus() return SensorPermissionStrategy.getPermissionStatus()
} }
func requestPermission(permission: PermissionGroup) -> PermissionStatus {
// TODO: Add implementation
return PermissionStatus.unknown
}
private static func getPermissionStatus() -> PermissionStatus { private static func getPermissionStatus() -> PermissionStatus {
if !CMMotionActivityManager.isActivityAvailable() { if !CMMotionActivityManager.isActivityAvailable() {
return PermissionStatus.disabled return PermissionStatus.disabled
...@@ -41,4 +36,25 @@ class SensorPermissionStrategy : NSObject, PermissionStrategy { ...@@ -41,4 +36,25 @@ class SensorPermissionStrategy : NSObject, PermissionStrategy {
return PermissionStatus.unknown return PermissionStatus.unknown
} }
func requestPermission(permission: PermissionGroup, completionHandler: @escaping PermissionStatusHandler) {
let status = checkPermissionStatus(permission: permission)
if status != PermissionStatus.unknown {
completionHandler(status)
return
}
if #available(iOS 11.0, *) {
let motionManager = CMMotionActivityManager.init()
motionManager.startActivityUpdates(to: OperationQueue.main) { (_) in
motionManager.stopActivityUpdates()
completionHandler(.granted)
}
} else {
completionHandler(.unknown)
}
}
} }
...@@ -14,27 +14,46 @@ class SpeechPermissionStrategy : NSObject, PermissionStrategy { ...@@ -14,27 +14,46 @@ class SpeechPermissionStrategy : NSObject, PermissionStrategy {
return SpeechPermissionStrategy.getPermissionStatus() return SpeechPermissionStrategy.getPermissionStatus()
} }
func requestPermission(permission: PermissionGroup) -> PermissionStatus {
// TODO: Add implementation
return PermissionStatus.unknown
}
private static func getPermissionStatus() -> PermissionStatus { private static func getPermissionStatus() -> PermissionStatus {
if #available(iOS 10.0, *) { if #available(iOS 10.0, *) {
let status: SFSpeechRecognizerAuthorizationStatus = SFSpeechRecognizer.authorizationStatus() let status: SFSpeechRecognizerAuthorizationStatus = SFSpeechRecognizer.authorizationStatus()
switch status { return SpeechPermissionStrategy.determinePermissionStatus(authorizationStatus: status)
case SFSpeechRecognizerAuthorizationStatus.authorized:
return PermissionStatus.granted
case SFSpeechRecognizerAuthorizationStatus.denied:
return PermissionStatus.denied
case SFSpeechRecognizerAuthorizationStatus.restricted:
return PermissionStatus.restricted
default:
return PermissionStatus.unknown
}
} }
return PermissionStatus.unknown return PermissionStatus.unknown
} }
func requestPermission(permission: PermissionGroup, completionHandler: @escaping PermissionStatusHandler) {
let status = checkPermissionStatus(permission: permission)
if status != PermissionStatus.unknown {
completionHandler(status)
return
}
if #available(iOS 10.0, *) {
SFSpeechRecognizer.requestAuthorization { (authorizationStatus: SFSpeechRecognizerAuthorizationStatus) in
completionHandler(
SpeechPermissionStrategy.determinePermissionStatus(authorizationStatus: authorizationStatus))
return
}
} else {
completionHandler(PermissionStatus.unknown)
}
}
@available(iOS 10.0, *)
private static func determinePermissionStatus(authorizationStatus: SFSpeechRecognizerAuthorizationStatus) -> PermissionStatus {
switch authorizationStatus {
case SFSpeechRecognizerAuthorizationStatus.authorized:
return PermissionStatus.granted
case SFSpeechRecognizerAuthorizationStatus.denied:
return PermissionStatus.denied
case SFSpeechRecognizerAuthorizationStatus.restricted:
return PermissionStatus.restricted
default:
return PermissionStatus.unknown
}
}
} }
//
// UnknownPermissionStrategy.swift
// permission_handler
//
// Created by Maurits van Beusekom on 07/08/2018.
//
import Foundation
class UnknownPermissionStrategy : NSObject, PermissionStrategy {
func checkPermissionStatus(permission: PermissionGroup) -> PermissionStatus {
return PermissionStatus.unknown
}
func requestPermission(permission: PermissionGroup, completionHandler: @escaping PermissionStatusHandler) {
completionHandler(PermissionStatus.unknown)
}
}
...@@ -19,8 +19,28 @@ struct Codec { ...@@ -19,8 +19,28 @@ struct Codec {
return PermissionGroup(rawValue: permissionString)! return PermissionGroup(rawValue: permissionString)!
} }
static func decodePermissionGroups(from arguments: Any?) -> [PermissionGroup] {
let data = (arguments as! String).data(using: .utf8)
let permissions = try! jsonDecoder.decode([PermissionGroup].self, from: data!)
return permissions
}
static func encodePermissionStatus(permissionStatus: PermissionStatus) -> String? { static func encodePermissionStatus(permissionStatus: PermissionStatus) -> String? {
let status = "\"" + permissionStatus.rawValue + "\"" let status = "\"" + permissionStatus.rawValue + "\""
return status return status
} }
static func encodePermissionRequestResult(permissionStatusResult: [PermissionGroup: PermissionStatus]) -> String? {
let jsonDict = Dictionary(uniqueKeysWithValues:
permissionStatusResult.map {
(key: PermissionGroup, value: PermissionStatus) in (key.rawValue, value.rawValue)
})
let jsonData = try! JSONSerialization.data(withJSONObject: jsonDict, options: [])
let jsonString = String(data: jsonData, encoding: .utf8)!
return jsonString
}
} }
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
# #
Pod::Spec.new do |s| Pod::Spec.new do |s|
s.name = 'permission_handler' s.name = 'permission_handler'
s.version = '0.0.1' s.version = '1.0.0'
s.summary = 'Permission plugin for Flutter.' s.summary = 'Permission plugin for Flutter.'
s.description = <<-DESC s.description = <<-DESC
Permission plugin for Flutter. This plugin provides a cross-platform (iOS, Android) API to request and check permissions. Permission plugin for Flutter. This plugin provides a cross-platform (iOS, Android) API to request and check permissions.
......
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:permission_handler/permission_enums.dart'; import 'package:permission_handler/permission_enums.dart';
...@@ -44,6 +45,11 @@ class PermissionHandler { ...@@ -44,6 +45,11 @@ class PermissionHandler {
/// returns [false]. /// returns [false].
static Future<bool> shouldShowRequestPermissionRationale( static Future<bool> shouldShowRequestPermissionRationale(
PermissionGroup permission) async { PermissionGroup permission) async {
if (!Platform.isAndroid) {
return false;
}
final bool shouldShowRationale = await _channel.invokeMethod( final bool shouldShowRationale = await _channel.invokeMethod(
'shouldShowRequestPermissionRationale', 'shouldShowRequestPermissionRationale',
Codec.encodePermissionGroup(permission)); Codec.encodePermissionGroup(permission));
......
...@@ -12,11 +12,11 @@ class Codec { ...@@ -12,11 +12,11 @@ class Codec {
static Map<PermissionGroup, PermissionStatus> decodePermissionRequestResult( static Map<PermissionGroup, PermissionStatus> decodePermissionRequestResult(
dynamic value) { dynamic value) {
final dynamic jsonObject = json.decode(value.toString()); final Map<String, dynamic> jsonObject = json.decode(value.toString());
final Map<PermissionGroup, PermissionStatus> permissionResults = final Map<PermissionGroup, PermissionStatus> permissionResults =
<PermissionGroup, PermissionStatus>{}; <PermissionGroup, PermissionStatus>{};
jsonObject.forEach((PermissionGroup key, PermissionStatus value) { jsonObject.forEach((String key, dynamic value) {
final PermissionGroup permissionGroup = PermissionGroup.values.firstWhere( final PermissionGroup permissionGroup = PermissionGroup.values.firstWhere(
(PermissionGroup e) => (PermissionGroup e) =>
e.toString().split('.').last == key.toString()); e.toString().split('.').last == key.toString());
......
name: permission_handler name: permission_handler
description: Permission plugin for Flutter. This plugin provides a cross-platform (iOS, Android) API to request and check permissions. description: Permission plugin for Flutter. This plugin provides a cross-platform (iOS, Android) API to request and check permissions.
version: 0.0.1 version: 1.0.0
author: Baseflow <hello@baseflow.com> author: Baseflow <hello@baseflow.com>
homepage: https://github.com/baseflowit/flutter-permission-handler homepage: https://github.com/baseflowit/flutter-permission-handler
...@@ -17,6 +17,9 @@ flutter: ...@@ -17,6 +17,9 @@ flutter:
androidPackage: com.baseflow.permissionhandler androidPackage: com.baseflow.permissionhandler
pluginClass: PermissionHandlerPlugin pluginClass: PermissionHandlerPlugin
environment:
sdk: ">=2.0.0-dev.58.0 <3.0.0"
# To add assets to your plugin package, add an assets section, like this: # To add assets to your plugin package, add an assets section, like this:
# assets: # assets:
# - images/a_dot_burr.jpeg # - images/a_dot_burr.jpeg
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment