import Foundation import UIKit import AVFoundation import Display import TelegramCore import SwiftSignalKit import Photos import CoreLocation import Contacts import UserNotifications import CoreTelephony import TelegramPresentationData import AccountContext public enum DeviceAccessCameraSubject { case video case videoCall case qrCode case ageVerification } public enum DeviceAccessMicrophoneSubject { case audio case video case voiceCall } public enum DeviceAccessMediaLibrarySubject { case send case save case wallpaper case qrCode } public enum DeviceAccessLocationSubject { case send case live case tracking case weather } public enum DeviceAccessSubject { case camera(DeviceAccessCameraSubject) case microphone(DeviceAccessMicrophoneSubject) case mediaLibrary(DeviceAccessMediaLibrarySubject) case location(DeviceAccessLocationSubject) case contacts case notifications case siri case cellularData } private let cachedMediaLibraryAccessStatus = Atomic(value: nil) public func shouldDisplayNotificationsPermissionWarning(status: AccessType, suppressed: Bool) -> Bool { switch (status, suppressed) { case (.allowed, _), (.unreachable, true), (.notDetermined, true): return false default: return true } } public final class DeviceAccess { private static let contactsPromise = Promise(nil) static var contacts: Signal { return self.contactsPromise.get() |> distinctUntilChanged } private static let notificationsPromise = Promise(nil) static var notifications: Signal { return self.notificationsPromise.get() } private static let siriPromise = Promise(nil) static var siri: Signal { return self.siriPromise.get() } private static let locationPromise = Promise(nil) static var location: Signal { return self.locationPromise.get() } private static let cameraPromise = Promise(nil) static var camera: Signal { return self.cameraPromise.get() } private static let microphonePromise = Promise(nil) static var microphone: Signal { return self.microphonePromise.get() } public static func isMicrophoneAccessAuthorized() -> Bool? { return AVAudioSession.sharedInstance().recordPermission == .granted } public static func isCameraAccessAuthorized() -> Bool { return AVCaptureDevice.authorizationStatus(for: .video) == .authorized } public static func authorizationStatus(applicationInForeground: Signal? = nil, siriAuthorization: (() -> AccessType)? = nil, subject: DeviceAccessSubject) -> Signal { switch subject { case .notifications: let status = (Signal { subscriber in if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { UNUserNotificationCenter.current().getNotificationSettings(completionHandler: { settings in switch settings.authorizationStatus { case .authorized: if settings.alertSetting == .disabled { subscriber.putNext(.unreachable) } else { subscriber.putNext(.allowed) } case .denied: subscriber.putNext(.denied) case .notDetermined: subscriber.putNext(.notDetermined) default: subscriber.putNext(.notDetermined) } subscriber.putCompletion() }) } else { subscriber.putNext(.notDetermined) subscriber.putCompletion() } return EmptyDisposable } |> afterNext { status in switch status { case .allowed, .unreachable: DeviceAccess.notificationsPromise.set(.single(nil)) default: break } } ) |> then(self.notifications |> mapToSignal { authorized -> Signal in if let authorized = authorized { return .single(authorized ? .allowed : .denied) } else { return .complete() } }) if let applicationInForeground = applicationInForeground { return applicationInForeground |> distinctUntilChanged |> mapToSignal { inForeground -> Signal in return status } } else { return status } case .contacts: let status = Signal { subscriber in switch CNContactStore.authorizationStatus(for: .contacts) { case .notDetermined: subscriber.putNext(.notDetermined) case .authorized: subscriber.putNext(.allowed) case .limited: subscriber.putNext(.limited) default: subscriber.putNext(.denied) } subscriber.putCompletion() return EmptyDisposable } return status |> then(self.contacts |> mapToSignal { authorized -> Signal in if let authorized { return .single(authorized ? .allowed : .denied) } else { return .complete() } }) case .cellularData: return Signal { subscriber in if #available(iOSApplicationExtension 9.0, iOS 9.0, *) { func statusForCellularState(_ state: CTCellularDataRestrictedState) -> AccessType? { switch state { case .restricted: return .denied case .notRestricted: return .allowed default: return .allowed } } let cellState = CTCellularData.init() if let status = statusForCellularState(cellState.restrictedState) { subscriber.putNext(status) } cellState.cellularDataRestrictionDidUpdateNotifier = { restrictedState in if let status = statusForCellularState(restrictedState) { subscriber.putNext(status) } } } else { subscriber.putNext(.allowed) subscriber.putCompletion() } return EmptyDisposable } case .siri: if let siriAuthorization = siriAuthorization { return Signal { subscriber in let status = siriAuthorization() subscriber.putNext(status) subscriber.putCompletion() return EmptyDisposable } |> then(self.siri |> mapToSignal { authorized -> Signal in if let authorized = authorized { return .single(authorized ? .allowed : .denied) } else { return .complete() } }) } else { return .single(.denied) } case .location: return Signal { subscriber in let status = CLLocationManager.authorizationStatus() switch status { case .authorizedAlways, .authorizedWhenInUse: subscriber.putNext(.allowed) case .denied, .restricted: subscriber.putNext(.denied) case .notDetermined: subscriber.putNext(.notDetermined) @unknown default: fatalError() } subscriber.putCompletion() return EmptyDisposable } |> then(self.location |> mapToSignal { authorized -> Signal in if let authorized = authorized { return .single(authorized ? .allowed : .denied) } else { return .complete() } } ) case .camera: return Signal { subscriber in let status = AVCaptureDevice.authorizationStatus(for: .video) switch status { case .authorized: subscriber.putNext(.allowed) case .denied, .restricted: subscriber.putNext(.denied) case .notDetermined: subscriber.putNext(.notDetermined) @unknown default: fatalError() } subscriber.putCompletion() return EmptyDisposable } |> then(self.camera |> mapToSignal { authorized -> Signal in if let authorized = authorized { return .single(authorized ? .allowed : .denied) } else { return .complete() } } ) case .microphone: return Signal { subscriber in let status = AVCaptureDevice.authorizationStatus(for: .audio) switch status { case .authorized: subscriber.putNext(.allowed) case .denied, .restricted: subscriber.putNext(.denied) case .notDetermined: subscriber.putNext(.notDetermined) @unknown default: fatalError() } subscriber.putCompletion() return EmptyDisposable } |> then(self.microphone |> mapToSignal { authorized -> Signal in if let authorized = authorized { return .single(authorized ? .allowed : .denied) } else { return .complete() } } ) default: return .single(.notDetermined) } } public static func authorizeAccess( to subject: DeviceAccessSubject, onlyCheck: Bool = false, registerForNotifications: ((@escaping (Bool) -> Void) -> Void)? = nil, requestSiriAuthorization: ((@escaping (Bool) -> Void) -> Void)? = nil, locationManager: LocationManager? = nil, presentationData: PresentationData? = nil, present: @escaping (ViewController, Any?) -> Void = { _, _ in }, openSettings: @escaping () -> Void = { }, displayNotificationFromBackground: @escaping (String) -> Void = { _ in }, _ completion: @escaping (Bool) -> Void = { _ in }) { switch subject { case let .camera(cameraSubject): let status = AVCaptureDevice.authorizationStatus(for: .video) if case .notDetermined = status { if !onlyCheck { AVCaptureDevice.requestAccess(for: AVMediaType.video) { response in Queue.mainQueue().async { completion(response) self.cameraPromise.set(.single(response)) if !response, let presentationData = presentationData { let text: String switch cameraSubject { case .video: text = presentationData.strings.AccessDenied_Camera case .videoCall: text = presentationData.strings.AccessDenied_VideoCallCamera case .qrCode: text = presentationData.strings.AccessDenied_QrCamera case .ageVerification: text = presentationData.strings.AccessDenied_AgeVerificationCamera } present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: { openSettings() })]), nil) } } } } else { completion(true) } } else if [.restricted, .denied].contains(status) { completion(false) if let presentationData = presentationData { let text: String if case .restricted = status { text = presentationData.strings.AccessDenied_CameraRestricted } else { switch cameraSubject { case .video: text = presentationData.strings.AccessDenied_Camera case .videoCall: text = presentationData.strings.AccessDenied_VideoCallCamera case .qrCode: text = presentationData.strings.AccessDenied_QrCamera case .ageVerification: text = presentationData.strings.AccessDenied_AgeVerificationCamera } } present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: { openSettings() })]), nil) } } else if case .authorized = status { completion(true) } else { assertionFailure() completion(true) } case let .microphone(microphoneSubject): if AVAudioSession.sharedInstance().recordPermission == .granted { completion(true) } else { AVAudioSession.sharedInstance().requestRecordPermission({ granted in Queue.mainQueue().async { if granted { completion(true) } else if let presentationData = presentationData { completion(false) let text: String switch microphoneSubject { case .audio: text = presentationData.strings.AccessDenied_VoiceMicrophone case .video: text = presentationData.strings.AccessDenied_VideoMicrophone case .voiceCall: text = presentationData.strings.AccessDenied_CallMicrophone } present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: { openSettings() })]), nil) if case .voiceCall = microphoneSubject { displayNotificationFromBackground(text) } } self.microphonePromise.set(.single(granted)) } }) } case let .mediaLibrary(mediaLibrarySubject): let continueWithValue: (Bool) -> Void = { value in Queue.mainQueue().async { if value { completion(true) } else if let presentationData = presentationData { completion(false) let text: String switch mediaLibrarySubject { case .send: text = presentationData.strings.AccessDenied_PhotosAndVideos case .save: text = presentationData.strings.AccessDenied_SaveMedia case .wallpaper: text = presentationData.strings.AccessDenied_Wallpapers case .qrCode: text = presentationData.strings.AccessDenied_QrCode } present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: { openSettings() })]), nil) } } } if let value = cachedMediaLibraryAccessStatus.with({ $0 }) { continueWithValue(value) } else { PHPhotoLibrary.requestAuthorization({ status in let value: Bool switch status { case .restricted, .denied, .notDetermined: value = false case .authorized, .limited: value = true @unknown default: fatalError() } let _ = cachedMediaLibraryAccessStatus.swap(value) continueWithValue(value) }) } case let .location(locationSubject): let status = CLLocationManager.authorizationStatus() let hasPreciseLocation: Bool if #available(iOS 14.0, *) { if case .fullAccuracy = CLLocationManager().accuracyAuthorization { hasPreciseLocation = true } else { hasPreciseLocation = false } } else { hasPreciseLocation = true } switch status { case .authorizedAlways: if case .live = locationSubject, !hasPreciseLocation { completion(false) if let presentationData = presentationData { let text = presentationData.strings.AccessDenied_LocationPreciseDenied present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: { openSettings() })]), nil) } } else { completion(true) } case .authorizedWhenInUse: switch locationSubject { case .send, .tracking, .weather: completion(true) case .live: completion(false) if let presentationData = presentationData { let text = presentationData.strings.AccessDenied_LocationAlwaysDenied present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: { openSettings() })]), nil) } } case .denied, .restricted: completion(false) if let presentationData = presentationData { let text: String if status == .denied { switch locationSubject { case .send, .live: text = presentationData.strings.AccessDenied_LocationDenied case .tracking: text = presentationData.strings.AccessDenied_LocationTracking case .weather: text = presentationData.strings.AccessDenied_LocationWeather } } else { text = presentationData.strings.AccessDenied_LocationDisabled } present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: { openSettings() })]), nil) } case .notDetermined: switch locationSubject { case .send, .tracking, .weather: locationManager?.requestWhenInUseAuthorization(completion: { status in completion(status == .authorizedWhenInUse || status == .authorizedAlways) }) case .live: locationManager?.requestAlwaysAuthorization(completion: { status in completion(status == .authorizedAlways) }) } @unknown default: fatalError() } case .contacts: let _ = (self.contactsPromise.get() |> take(1) |> deliverOnMainQueue).start(next: { value in if let value = value { completion(value) } else { switch CNContactStore.authorizationStatus(for: .contacts) { case .notDetermined: let store = CNContactStore() store.requestAccess(for: .contacts, completionHandler: { authorized, _ in self.contactsPromise.set(.single(authorized)) completion(authorized) }) case .authorized: self.contactsPromise.set(.single(true)) completion(true) case .limited: self.contactsPromise.set(.single(true)) completion(true) default: self.contactsPromise.set(.single(false)) completion(false) } } }) case .notifications: if let registerForNotifications = registerForNotifications { registerForNotifications { result in self.notificationsPromise.set(.single(result)) completion(result) } } case .siri: if let requestSiriAuthorization = requestSiriAuthorization { requestSiriAuthorization { result in self.siriPromise.set(.single(result)) completion(result) } } case .cellularData: if let presentationData = presentationData { present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.Permissions_CellularDataTitle_v0, text: presentationData.strings.Permissions_CellularDataText_v0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: { openSettings() })]), nil) } } } }