From d3a2b456f19a417f06f9a16736dfcea62c6794f1 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Thu, 18 Nov 2021 10:39:50 +1100 Subject: [PATCH] handle permission request for voice and video calls --- Session.xcodeproj/project.pbxproj | 4 + Session/Calls/CallVC.swift | 1 + .../ConversationVC+Interaction.swift | 88 +----------------- Session/Utilities/Permissions.swift | 90 +++++++++++++++++++ 4 files changed, 97 insertions(+), 86 deletions(-) create mode 100644 Session/Utilities/Permissions.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index ee9f7731f..70bc73c20 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -156,6 +156,7 @@ 7BCD116C27016062006330F1 /* WebRTCSession+DataChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BCD116B27016062006330F1 /* WebRTCSession+DataChannel.swift */; }; 7BDCFC08242186E700641C39 /* NotificationServiceExtensionContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BDCFC07242186E700641C39 /* NotificationServiceExtensionContext.swift */; }; 7BDCFC0B2421EB7600641C39 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = B6F509951AA53F760068F56A /* Localizable.strings */; }; + 7BFD1A8A2745C4F000FB91B9 /* Permissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BFD1A892745C4F000FB91B9 /* Permissions.swift */; }; 945AA2B82B621254F69FA9E8 /* Pods_SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9117261809D69B3D7C26B8F1 /* Pods_SessionUtilitiesKit.framework */; }; 9EE44C6B4D4A069B86112387 /* Pods_SessionSnodeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9559C3068280BA2383F547F7 /* Pods_SessionSnodeKit.framework */; }; A11CD70D17FA230600A2D1B1 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A11CD70C17FA230600A2D1B1 /* QuartzCore.framework */; }; @@ -1144,6 +1145,7 @@ 7BCD116B27016062006330F1 /* WebRTCSession+DataChannel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WebRTCSession+DataChannel.swift"; sourceTree = ""; }; 7BDCFC0424206E7300641C39 /* SessionNotificationServiceExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SessionNotificationServiceExtension.entitlements; sourceTree = ""; }; 7BDCFC07242186E700641C39 /* NotificationServiceExtensionContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationServiceExtensionContext.swift; sourceTree = ""; }; + 7BFD1A892745C4F000FB91B9 /* Permissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Permissions.swift; sourceTree = ""; }; 7DD180F770F8518B4E8796F2 /* Pods-SessionUtilitiesKit.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionUtilitiesKit.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-SessionUtilitiesKit/Pods-SessionUtilitiesKit.app store release.xcconfig"; sourceTree = ""; }; 8981C8F64D94D3C52EB67A2C /* Pods-SignalTests.test.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalTests.test.xcconfig"; path = "Pods/Target Support Files/Pods-SignalTests/Pods-SignalTests.test.xcconfig"; sourceTree = ""; }; 8EEE74B0753448C085B48721 /* Pods-SignalMessaging.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalMessaging.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-SignalMessaging/Pods-SignalMessaging.app store release.xcconfig"; sourceTree = ""; }; @@ -2058,6 +2060,7 @@ B83F2B87240CB75A000A54AB /* UIImage+Scaling.swift */, C31A6C59247F214E001123EF /* UIView+Glow.swift */, C3548F0724456AB6009433A8 /* UIView+Wrapping.swift */, + 7BFD1A892745C4F000FB91B9 /* Permissions.swift */, ); path = Utilities; sourceTree = ""; @@ -4989,6 +4992,7 @@ 340FC8AC204DAC8D007AEB0F /* PrivacySettingsTableViewController.m in Sources */, 7B7CB18E270D066F0079FF93 /* IncomingCallBanner.swift in Sources */, B8569AE325CBB19A00DBA3DB /* DocumentView.swift in Sources */, + 7BFD1A8A2745C4F000FB91B9 /* Permissions.swift in Sources */, B85357BF23A1AE0800AAF6CD /* SeedReminderView.swift in Sources */, B821494F25D4E163009C0F2A /* BodyTextView.swift in Sources */, C35E8AAE2485E51D00ACB629 /* IP2Country.swift in Sources */, diff --git a/Session/Calls/CallVC.swift b/Session/Calls/CallVC.swift index 7a4ba3df7..bd01d0560 100644 --- a/Session/Calls/CallVC.swift +++ b/Session/Calls/CallVC.swift @@ -367,6 +367,7 @@ final class CallVC : UIViewController, VideoPreviewDelegate { switchCameraButton.isEnabled = false call.isVideoEnabled = false } else { + guard requestCameraPermissionIfNeeded() else { return } let previewVC = VideoPreviewVC() previewVC.delegate = self present(previewVC, animated: true, completion: nil) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 2f3a3cd9c..d9db9c7fa 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -33,6 +33,8 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc userDefaults[.hasSeenCallIPExposureWarning] = true showCallModal() } else if SSKPreferences.areCallsEnabled { + requestMicrophonePermissionIfNeeded { } + guard AVAudioSession.sharedInstance().recordPermission == .granted else { return } guard let contactSessionID = (thread as? TSContactThread)?.contactSessionID() else { return } guard AppEnvironment.shared.callManager.currentCall == nil else { return } let call = SessionCall(for: contactSessionID, uuid: UUID().uuidString.lowercased(), mode: .offer, outgoing: true) @@ -905,92 +907,6 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc } } - // MARK: Requesting Permission - func requestCameraPermissionIfNeeded() -> Bool { - switch AVCaptureDevice.authorizationStatus(for: .video) { - case .authorized: return true - case .denied, .restricted: - let modal = PermissionMissingModal(permission: "camera") { } - modal.modalPresentationStyle = .overFullScreen - modal.modalTransitionStyle = .crossDissolve - present(modal, animated: true, completion: nil) - return false - case .notDetermined: - AVCaptureDevice.requestAccess(for: .video, completionHandler: { _ in }) - return false - default: return false - } - } - - func requestMicrophonePermissionIfNeeded(onNotGranted: @escaping () -> Void) { - switch AVAudioSession.sharedInstance().recordPermission { - case .granted: break - case .denied: - onNotGranted() - let modal = PermissionMissingModal(permission: "microphone") { - onNotGranted() - } - modal.modalPresentationStyle = .overFullScreen - modal.modalTransitionStyle = .crossDissolve - present(modal, animated: true, completion: nil) - case .undetermined: - onNotGranted() - AVAudioSession.sharedInstance().requestRecordPermission { _ in } - default: break - } - } - - func requestLibraryPermissionIfNeeded(onAuthorized: @escaping () -> Void) { - let authorizationStatus: PHAuthorizationStatus - if #available(iOS 14, *) { - 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. - Environment.shared.isRequestingPermission = true - let appMode = AppModeManager.shared.currentAppMode - // FIXME: Rather than setting the app mode to light and then to dark again once we're done, - // it'd be better to just customize the appearance of the image picker. There doesn't currently - // appear to be a good way to do so though... - AppModeManager.shared.setCurrentAppMode(to: .light) - PHPhotoLibrary.requestAuthorization(for: .readWrite) { status in - DispatchQueue.main.async { - AppModeManager.shared.setCurrentAppMode(to: appMode) - } - Environment.shared.isRequestingPermission = false - if [ PHAuthorizationStatus.authorized, PHAuthorizationStatus.limited ].contains(status) { - onAuthorized() - } - } - } - } else { - authorizationStatus = PHPhotoLibrary.authorizationStatus() - if authorizationStatus == .notDetermined { - PHPhotoLibrary.requestAuthorization { status in - if status == .authorized { - onAuthorized() - } - } - } - } - switch authorizationStatus { - case .authorized, .limited: - onAuthorized() - case .denied, .restricted: - let modal = PermissionMissingModal(permission: "library") { } - modal.modalPresentationStyle = .overFullScreen - modal.modalTransitionStyle = .crossDissolve - present(modal, animated: true, completion: nil) - default: return - } - } - // MARK: Convenience func showErrorAlert(for attachment: SignalAttachment) { let title = NSLocalizedString("ATTACHMENT_ERROR_ALERT_TITLE", comment: "") diff --git a/Session/Utilities/Permissions.swift b/Session/Utilities/Permissions.swift new file mode 100644 index 000000000..7146580e3 --- /dev/null +++ b/Session/Utilities/Permissions.swift @@ -0,0 +1,90 @@ +import Photos +import PhotosUI + +public func requestCameraPermissionIfNeeded() -> Bool { + switch AVCaptureDevice.authorizationStatus(for: .video) { + case .authorized: return true + case .denied, .restricted: + let modal = PermissionMissingModal(permission: "camera") { } + modal.modalPresentationStyle = .overFullScreen + modal.modalTransitionStyle = .crossDissolve + guard let presentingVC = CurrentAppContext().frontmostViewController() else { preconditionFailure() } + presentingVC.present(modal, animated: true, completion: nil) + return false + case .notDetermined: + AVCaptureDevice.requestAccess(for: .video, completionHandler: { _ in }) + return false + default: return false + } +} + +public func requestMicrophonePermissionIfNeeded(onNotGranted: @escaping () -> Void) { + switch AVAudioSession.sharedInstance().recordPermission { + case .granted: break + case .denied: + onNotGranted() + let modal = PermissionMissingModal(permission: "microphone") { + onNotGranted() + } + modal.modalPresentationStyle = .overFullScreen + modal.modalTransitionStyle = .crossDissolve + guard let presentingVC = CurrentAppContext().frontmostViewController() else { preconditionFailure() } + presentingVC.present(modal, animated: true, completion: nil) + case .undetermined: + onNotGranted() + AVAudioSession.sharedInstance().requestRecordPermission { _ in } + default: break + } +} + +public func requestLibraryPermissionIfNeeded(onAuthorized: @escaping () -> Void) { + let authorizationStatus: PHAuthorizationStatus + if #available(iOS 14, *) { + 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. + Environment.shared.isRequestingPermission = true + let appMode = AppModeManager.shared.currentAppMode + // FIXME: Rather than setting the app mode to light and then to dark again once we're done, + // it'd be better to just customize the appearance of the image picker. There doesn't currently + // appear to be a good way to do so though... + AppModeManager.shared.setCurrentAppMode(to: .light) + PHPhotoLibrary.requestAuthorization(for: .readWrite) { status in + DispatchQueue.main.async { + AppModeManager.shared.setCurrentAppMode(to: appMode) + } + Environment.shared.isRequestingPermission = false + if [ PHAuthorizationStatus.authorized, PHAuthorizationStatus.limited ].contains(status) { + onAuthorized() + } + } + } + } else { + authorizationStatus = PHPhotoLibrary.authorizationStatus() + if authorizationStatus == .notDetermined { + PHPhotoLibrary.requestAuthorization { status in + if status == .authorized { + onAuthorized() + } + } + } + } + switch authorizationStatus { + case .authorized, .limited: + onAuthorized() + case .denied, .restricted: + let modal = PermissionMissingModal(permission: "library") { } + modal.modalPresentationStyle = .overFullScreen + modal.modalTransitionStyle = .crossDissolve + guard let presentingVC = CurrentAppContext().frontmostViewController() else { preconditionFailure() } + presentingVC.present(modal, animated: true, completion: nil) + default: return + } +}