You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
session-ios/Session/Utilities/Permissions.swift

346 lines
16 KiB
Swift

// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import Photos
import PhotosUI
import AVFAudio
import SessionUIKit
import SessionUtilitiesKit
import SessionMessagingKit
import Network
extension Permissions {
@discardableResult public static func requestCameraPermissionIfNeeded(
presentingViewController: UIViewController? = nil,
using dependencies: Dependencies,
onAuthorized: ((Bool) -> Void)? = nil
) -> Bool {
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .authorized:
onAuthorized?(true)
return true
case .denied, .restricted:
guard
let presentingViewController: UIViewController = (presentingViewController ?? dependencies[singleton: .appContext].frontMostViewController)
else { return false }
let confirmationModal: ConfirmationModal = ConfirmationModal(
info: ConfirmationModal.Info(
title: "permissionsRequired".localized(),
body: .text(
"cameraGrantAccessDenied"
.put(key: "app_name", value: Constants.app_name)
.localized()
),
confirmTitle: "sessionSettings".localized(),
dismissOnConfirm: false
) { [weak presentingViewController] _ in
presentingViewController?.dismiss(animated: true, completion: {
UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!)
})
}
)
presentingViewController.present(confirmationModal, animated: true, completion: nil)
return false
case .notDetermined:
AVCaptureDevice.requestAccess(for: .video, completionHandler: { granted in
onAuthorized?(granted)
})
return false
default: return false
}
}
public static func requestMicrophonePermissionIfNeeded(
presentingViewController: UIViewController? = nil,
using dependencies: Dependencies,
onAuthorized: ((Bool) -> Void)? = nil,
onNotGranted: (() -> Void)? = nil
) {
let handlePermissionDenied: () -> Void = {
guard
let presentingViewController: UIViewController = (presentingViewController ?? dependencies[singleton: .appContext].frontMostViewController)
else { return }
onNotGranted?()
let confirmationModal: ConfirmationModal = ConfirmationModal(
info: ConfirmationModal.Info(
title: "permissionsRequired".localized(),
body: .text(
"permissionsMicrophoneAccessRequiredIos"
.put(key: "app_name", value: Constants.app_name)
.localized()
),
confirmTitle: "sessionSettings".localized(),
dismissOnConfirm: false,
onConfirm: { [weak presentingViewController] _ in
presentingViewController?.dismiss(animated: true, completion: {
UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!)
})
},
afterClosed: { onNotGranted?() }
)
)
presentingViewController.present(confirmationModal, animated: true, completion: nil)
}
if #available(iOS 17.0, *) {
switch AVAudioApplication.shared.recordPermission {
case .granted: break
case .denied: handlePermissionDenied()
case .undetermined:
onNotGranted?()
AVAudioApplication.requestRecordPermission { granted in
dependencies[defaults: .appGroup, key: .lastSeenHasMicrophonePermission] = granted
onAuthorized?(granted)
}
default: break
}
} else {
switch AVAudioSession.sharedInstance().recordPermission {
case .granted: break
case .denied: handlePermissionDenied()
case .undetermined:
onNotGranted?()
AVAudioSession.sharedInstance().requestRecordPermission { granted in
dependencies[defaults: .appGroup, key: .lastSeenHasMicrophonePermission] = granted
onAuthorized?(granted)
}
default: break
}
}
}
public static func requestLibraryPermissionIfNeeded(
isSavingMedia: Bool,
presentingViewController: UIViewController? = nil,
using dependencies: Dependencies,
onAuthorized: @escaping () -> Void
) {
let targetPermission: PHAccessLevel = (isSavingMedia ? .addOnly : .readWrite)
let authorizationStatus = PHPhotoLibrary.authorizationStatus(for: .readWrite)
if authorizationStatus == .notDetermined {
// When the user chooses to select photos (which is the .limit status),
// the PHPhotoUI will present the picker view on the top of the front view.
// Since we have the ScreenLockUI showing when we request premissions,
// the picker view will be presented on the top of the ScreenLockUI.
// However, the ScreenLockUI will dismiss with the permission request alert view, so
// the picker view then will dismiss, too. The selection process cannot be finished
// this way. So we add a flag (isRequestingPermission) to prevent the ScreenLockUI
// from showing when we request the photo library permission.
SessionEnvironment.shared?.isRequestingPermission = true
PHPhotoLibrary.requestAuthorization(for: targetPermission) { status in
SessionEnvironment.shared?.isRequestingPermission = false
if [ PHAuthorizationStatus.authorized, PHAuthorizationStatus.limited ].contains(status) {
onAuthorized()
}
}
}
switch authorizationStatus {
case .authorized, .limited: onAuthorized()
case .denied, .restricted:
guard
let presentingViewController: UIViewController = (presentingViewController ?? dependencies[singleton: .appContext].frontMostViewController)
else { return }
let confirmationModal: ConfirmationModal = ConfirmationModal(
info: ConfirmationModal.Info(
title: "permissionsRequired".localized(),
body: .text(
"permissionsLibrary"
.put(key: "app_name", value: Constants.app_name)
.localized()
),
confirmTitle: "sessionSettings".localized(),
dismissOnConfirm: false
) { [weak presentingViewController] _ in
presentingViewController?.dismiss(animated: true, completion: {
UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!)
})
}
)
presentingViewController.present(confirmationModal, animated: true, completion: nil)
default: return
}
}
// MARK: - Local Network Premission
public static func localNetwork(using dependencies: Dependencies) -> Status {
let status: Bool = dependencies[singleton: .storage, key: .lastSeenHasLocalNetworkPermission]
return status ? .granted : .denied
}
public static func requestLocalNetworkPermissionIfNeeded(using dependencies: Dependencies) {
dependencies[defaults: .standard, key: .hasRequestedLocalNetworkPermission] = true
checkLocalNetworkPermission(using: dependencies)
}
public static func checkLocalNetworkPermission(using dependencies: Dependencies) {
Task {
do {
if try await checkLocalNetworkPermissionWithBonjour() {
// Permission is granted, continue to next onboarding step
dependencies[singleton: .storage].writeAsync { db in
db[.lastSeenHasLocalNetworkPermission] = true
}
} else {
// Permission denied, explain why we need it and show button to open Settings
dependencies[singleton: .storage].writeAsync { db in
db[.lastSeenHasLocalNetworkPermission] = false
}
}
} catch {
// Networking failure, handle error
}
}
}
public static func checkLocalNetworkPermissionWithBonjour() async throws -> Bool {
let type = "_session_local_network_access_check._tcp" // stringlint:ignore
let queue = DispatchQueue(label: "localNetworkAuthCheck")
let listener = try NWListener(using: NWParameters(tls: .none, tcp: NWProtocolTCP.Options()))
listener.service = NWListener.Service(name: UUID().uuidString, type: type)
listener.newConnectionHandler = { _ in } // Must be set or else the listener will error with POSIX error 22
let parameters = NWParameters()
parameters.includePeerToPeer = true
let browser = NWBrowser(for: .bonjour(type: type, domain: nil), using: parameters)
return try await withTaskCancellationHandler {
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Bool, Error>) in
class LocalState {
var didResume = false
}
let local = LocalState()
@Sendable func resume(with result: Result<Bool, Error>) {
if local.didResume {
print("Already resumed, ignoring subsequent result.")
return
}
local.didResume = true
// Teardown listener and browser
listener.stateUpdateHandler = { _ in }
browser.stateUpdateHandler = { _ in }
browser.browseResultsChangedHandler = { _, _ in }
listener.cancel()
browser.cancel()
continuation.resume(with: result)
}
// Do not setup listener/browser is we're already cancelled, it does work but logs a lot of very ugly errors
if Task.isCancelled {
resume(with: .failure(CancellationError()))
return
}
listener.stateUpdateHandler = { newState in
switch newState {
case .setup:
print("Listener performing setup.")
case .ready:
print("Listener ready to be discovered.")
case .cancelled:
print("Listener cancelled.")
resume(with: .failure(CancellationError()))
case .failed(let error):
print("Listener failed, stopping. \(error)")
resume(with: .failure(error))
case .waiting(let error):
print("Listener waiting, stopping. \(error)")
resume(with: .failure(error))
@unknown default:
print("Ignoring unknown listener state: \(String(describing: newState))")
}
}
listener.start(queue: queue)
browser.stateUpdateHandler = { newState in
switch newState {
case .setup:
print("Browser performing setup.")
return
case .ready:
print("Browser ready to discover listeners.")
return
case .cancelled:
print("Browser cancelled.")
resume(with: .failure(CancellationError()))
case .failed(let error):
print("Browser failed, stopping. \(error)")
resume(with: .failure(error))
case let .waiting(error):
switch error {
case .dns(DNSServiceErrorType(kDNSServiceErr_PolicyDenied)):
print("Browser permission denied, reporting failure.")
resume(with: .success(false))
default:
print("Browser waiting, stopping. \(error)")
resume(with: .failure(error))
}
@unknown default:
print("Ignoring unknown browser state: \(String(describing: newState))")
return
}
}
browser.browseResultsChangedHandler = { results, changes in
if results.isEmpty {
print("Got empty result set from browser, ignoring.")
return
}
print("Discovered \(results.count) listeners, reporting success.")
resume(with: .success(true))
}
browser.start(queue: queue)
// Task cancelled while setting up listener & browser, tear down immediatly
if Task.isCancelled {
print("Task cancelled during listener & browser start. (Some warnings might be logged by the listener or browser.)")
resume(with: .failure(CancellationError()))
return
}
}
} onCancel: {
listener.cancel()
browser.cancel()
}
}
public static func requestPermissionsForCalls(
presentingViewController: UIViewController? = nil,
using dependencies: Dependencies
) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) {
requestMicrophonePermissionIfNeeded(
presentingViewController: presentingViewController,
using: dependencies,
onAuthorized: { _ in
DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) {
requestCameraPermissionIfNeeded(
presentingViewController: presentingViewController,
using: dependencies,
onAuthorized: { _ in
DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) {
requestLocalNetworkPermissionIfNeeded(using: dependencies)
}
}
)
}
}
)
}
}
}