iOS/Sources/App/Utilities/Permissions.swift

355 lines
12 KiB
Swift

import CoreLocation
import CoreMotion
import Foundation
import Shared
import UIKit
import UserNotifications
public enum PermissionStatus {
case notDetermined
case denied
case authorized
case authorizedWhenInUse // CLAuthorizationStatus only
case restricted // Used if UNAuthorizationStatus is .provisional
case unknown
var description: String {
switch self {
case .notDetermined:
return "Not determined"
case .restricted:
return "Restricted"
case .denied:
return "Denied"
case .authorized:
return "Authorized"
case .authorizedWhenInUse:
return "Authorized when in use"
case .unknown:
return "Unknown"
}
}
}
extension CLAuthorizationStatus {
var genericStatus: PermissionStatus {
switch self {
case .notDetermined:
return PermissionStatus.notDetermined
case .restricted:
return PermissionStatus.restricted
case .denied:
return PermissionStatus.denied
case .authorizedAlways:
return PermissionStatus.authorized
case .authorizedWhenInUse:
return PermissionStatus.authorizedWhenInUse
@unknown default:
Current.Log.warning("Caught unknown CLAuthorizationStatus \(self), returning PermissionStatus.unknown")
return PermissionStatus.unknown
}
}
}
extension CMAuthorizationStatus {
var genericStatus: PermissionStatus {
switch self {
case .notDetermined:
return PermissionStatus.notDetermined
case .restricted:
return PermissionStatus.restricted
case .denied:
return PermissionStatus.denied
case .authorized:
return PermissionStatus.authorized
@unknown default:
Current.Log.warning("Caught unknown CMAuthorizationStatus \(self), returning PermissionStatus.unknown")
return PermissionStatus.unknown
}
}
}
extension UNAuthorizationStatus {
var genericStatus: PermissionStatus {
switch self {
case .notDetermined:
return PermissionStatus.notDetermined
case .provisional:
return PermissionStatus.restricted
case .denied:
return PermissionStatus.denied
case .ephemeral:
return PermissionStatus.authorized
case .authorized:
return PermissionStatus.authorized
@unknown default:
Current.Log.warning("Caught unknown UNAuthorizationStatus \(self), returning PermissionStatus.unknown")
return PermissionStatus.unknown
}
}
}
extension FocusStatusWrapper.AuthorizationStatus {
var genericStatus: PermissionStatus {
switch self {
case .notDetermined:
return .notDetermined
case .authorized:
return .authorized
case .denied:
return .denied
case .restricted:
return .restricted
}
}
}
public enum PermissionType {
case location
case motion
case notification
case focus
var title: String {
switch self {
case .location:
return L10n.Onboarding.Permissions.Location.title
case .motion:
return L10n.Onboarding.Permissions.Motion.title
case .notification:
return L10n.Onboarding.Permissions.Notification.title
case .focus:
return L10n.Onboarding.Permissions.Focus.title
}
}
var enableIcon: MaterialDesignIcons {
switch self {
case .location: return .mapMarkerOutlineIcon
case .motion: return .runIcon
case .notification: return .bellOutlineIcon
case .focus: return .powerSleepIcon
}
}
var enableDescription: String {
switch self {
case .location:
return L10n.Onboarding.Permissions.Location.grantDescription
case .motion:
return L10n.Onboarding.Permissions.Motion.grantDescription
case .notification:
return L10n.Onboarding.Permissions.Notification.grantDescription
case .focus:
return L10n.Onboarding.Permissions.Focus.grantDescription
}
}
var enableBulletPoints: [(MaterialDesignIcons, String)] {
switch self {
case .location:
return [
(.homeAutomationIcon, L10n.Onboarding.Permissions.Location.Bullet.automations),
(.mapOutlineIcon, L10n.Onboarding.Permissions.Location.Bullet.history),
(.wifiIcon, L10n.Onboarding.Permissions.Location.Bullet.wifi),
]
case .motion:
return [
(.walkIcon, L10n.Onboarding.Permissions.Motion.Bullet.steps),
(.mapMarkerDistanceIcon, L10n.Onboarding.Permissions.Motion.Bullet.distance),
(.bikeIcon, L10n.Onboarding.Permissions.Motion.Bullet.activity),
]
case .notification:
return [
(.alertDecagramIcon, L10n.Onboarding.Permissions.Notification.Bullet.alert),
(.textIcon, L10n.Onboarding.Permissions.Notification.Bullet.commands),
(.bellBadgeOutlineIcon, L10n.Onboarding.Permissions.Notification.Bullet.badge),
]
case .focus:
return [
(.homeAutomationIcon, L10n.Onboarding.Permissions.Focus.Bullet.automations),
(.cancelIcon, L10n.Onboarding.Permissions.Focus.Bullet.instant),
]
}
}
var status: PermissionStatus {
let locationManager = CLLocationManager()
switch self {
case .location:
guard CLLocationManager.locationServicesEnabled() else { return .restricted }
return locationManager.authorizationStatus.genericStatus
case .motion:
return CMMotionActivityManager.authorizationStatus().genericStatus
case .notification:
guard let authorizationStatus = fetchNotificationsAuthorizationStatus() else { return .denied }
return authorizationStatus.genericStatus
case .focus:
return Current.focusStatus.authorizationStatus().genericStatus
}
}
var isAuthorized: Bool {
switch status {
case .authorized, .authorizedWhenInUse:
return true
case .denied, .notDetermined, .restricted, .unknown:
return false
}
}
private func fetchNotificationsAuthorizationStatus() -> UNAuthorizationStatus? {
if self != .notification { return nil }
// Fix for UNNotificationCenter & @IBDesignable - https://stackoverflow.com/a/55803896/486182
if ProcessInfo().processName.hasPrefix("IBDesignablesAgent") { return .notDetermined }
var notificationSettings: UNNotificationSettings?
let semaphore = DispatchSemaphore(value: 0)
DispatchQueue.global().async {
UNUserNotificationCenter.current().getNotificationSettings { setttings in
notificationSettings = setttings
semaphore.signal()
}
}
semaphore.wait()
return notificationSettings?.authorizationStatus
}
func request(_ completionHandler: @escaping (Bool, PermissionStatus) -> Void) {
if status == .denied {
let destination: UIApplication.OpenSettingsDestination
switch self {
case .location: destination = .location
case .motion: destination = .motion
case .notification: destination = .notification
case .focus: destination = .focus
}
UIApplication.shared.openSettings(destination: destination, completionHandler: nil)
completionHandler(false, .denied)
return
}
switch self {
case .location:
if PermissionsLocationDelegate.shared == nil {
PermissionsLocationDelegate.shared = PermissionsLocationDelegate()
}
PermissionsLocationDelegate.shared!.requestPermission { status in
DispatchQueue.main.async {
completionHandler(status == .authorized || status == .authorizedWhenInUse, status)
PermissionsLocationDelegate.shared = nil
}
}
case .motion:
let manager = CMMotionActivityManager()
let now = Date()
manager.queryActivityStarting(from: now, to: now, to: .main, withHandler: { (_, error: Error?) in
if let error = error as NSError?,
error.domain == CMErrorDomain,
error.code == CMErrorMotionActivityNotAuthorized.rawValue {
completionHandler(false, .denied)
return
}
completionHandler(true, .authorized)
})
case .notification:
UNUserNotificationCenter.current().requestAuthorization(options: .defaultOptions) { granted, error in
if let error {
Current.Log.error("Error when requesting notifications permissions: \(error)")
}
DispatchQueue.main.async {
UIApplication.shared.registerForRemoteNotifications()
let status: PermissionStatus = granted ? .authorized : .denied
completionHandler(granted, status)
}
}
if Current.isCatalyst {
// we likely will not get a completion until the user responds to the notification
// but we don't wanna delay onboarding for this
completionHandler(status == .authorized, status)
}
case .focus:
Current.focusStatus.requestAuthorization().done { status in
completionHandler(status == .authorized, status.genericStatus)
}
}
}
}
public extension UNAuthorizationOptions {
static var defaultOptions: UNAuthorizationOptions {
var opts: UNAuthorizationOptions = [.alert, .badge, .sound, .providesAppNotificationSettings]
if !Current.isCatalyst {
// we don't have provisioning for critical alerts in catalyst yet, and asking for permission errors
opts.insert(.criticalAlert)
}
return opts
}
}
private class PermissionsLocationDelegate: NSObject, CLLocationManagerDelegate {
static var shared: PermissionsLocationDelegate?
lazy var locationManager: CLLocationManager = .init()
typealias LocationPermissionCompletionBlock = (PermissionStatus) -> Void
var completionHandler: LocationPermissionCompletionBlock?
override init() {
super.init()
}
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
if manager.authorizationStatus == .notDetermined {
return
}
completionHandler?(manager.authorizationStatus.genericStatus)
}
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
if status == .notDetermined {
return
}
completionHandler?(status.genericStatus)
}
func requestPermission(_ completionHandler: @escaping LocationPermissionCompletionBlock) {
self.completionHandler = completionHandler
let status = locationManager.authorizationStatus
switch status {
case .authorizedWhenInUse, .notDetermined:
locationManager.delegate = self
locationManager.requestAlwaysAuthorization()
default:
completionHandler(status.genericStatus)
}
}
var isAuthorized: Bool {
switch locationManager.authorizationStatus {
case .authorizedAlways, .authorizedWhenInUse:
return true
case .denied, .notDetermined, .restricted:
return false
@unknown default:
return false
}
}
deinit {
locationManager.delegate = nil
}
}