From bef20e2f9adbdd854dc9c1673630f183893b5d5d Mon Sep 17 00:00:00 2001 From: ryanzhao Date: Mon, 8 Nov 2021 09:12:18 +1100 Subject: [PATCH] refactor for CallKit --- Session.xcodeproj/project.pbxproj | 10 +++- .../Calls/Call Management/SessionCall.swift | 22 +++++---- .../SessionCallManager+CXCallController.swift | 47 +++++++++++++++++++ .../SessionCallManager+CXProvider.swift | 44 +++++++++++++++++ .../Call Management/SessionCallManager.swift | 41 ++++++++++++---- Session/Calls/CallVC.swift | 4 +- .../Views & Modals/IncomingCallBanner.swift | 5 +- Session/Meta/AppDelegate.swift | 33 +++++++------ .../Calls/WebRTCSession+MessageHandling.swift | 2 +- SessionMessagingKit/Calls/WebRTCSession.swift | 2 - 10 files changed, 170 insertions(+), 40 deletions(-) create mode 100644 Session/Calls/Call Management/SessionCallManager+CXCallController.swift create mode 100644 Session/Calls/Call Management/SessionCallManager+CXProvider.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index cd78b38cf..9164645b4 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -148,6 +148,8 @@ 7B7CB190270FB2150079FF93 /* MiniCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB18F270FB2150079FF93 /* MiniCallView.swift */; }; 7B7CB192271508AD0079FF93 /* Vibration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB191271508AD0079FF93 /* Vibration.swift */; }; 7BA68909272A27BE00EFC32F /* SessionCall.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA68908272A27BE00EFC32F /* SessionCall.swift */; }; + 7BA6890D27325CCC00EFC32F /* SessionCallManager+CXCallController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA6890C27325CCC00EFC32F /* SessionCallManager+CXCallController.swift */; }; + 7BA6890F27325CE300EFC32F /* SessionCallManager+CXProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA6890E27325CE300EFC32F /* SessionCallManager+CXProvider.swift */; }; 7BC01A3E241F40AB00BC7C55 /* NotificationServiceExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BC01A3D241F40AB00BC7C55 /* NotificationServiceExtension.swift */; }; 7BC01A42241F40AB00BC7C55 /* SessionNotificationServiceExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 7BC01A3B241F40AB00BC7C55 /* SessionNotificationServiceExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 7BC707F227290ACB002817AD /* SessionCallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BC707F127290ACB002817AD /* SessionCallManager.swift */; }; @@ -1133,6 +1135,8 @@ 7B7CB18F270FB2150079FF93 /* MiniCallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MiniCallView.swift; sourceTree = ""; }; 7B7CB191271508AD0079FF93 /* Vibration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Vibration.swift; sourceTree = ""; }; 7BA68908272A27BE00EFC32F /* SessionCall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionCall.swift; sourceTree = ""; }; + 7BA6890C27325CCC00EFC32F /* SessionCallManager+CXCallController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionCallManager+CXCallController.swift"; sourceTree = ""; }; + 7BA6890E27325CE300EFC32F /* SessionCallManager+CXProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionCallManager+CXProvider.swift"; sourceTree = ""; }; 7BC01A3B241F40AB00BC7C55 /* SessionNotificationServiceExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SessionNotificationServiceExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 7BC01A3D241F40AB00BC7C55 /* NotificationServiceExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationServiceExtension.swift; sourceTree = ""; }; 7BC01A3F241F40AB00BC7C55 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -2071,8 +2075,10 @@ 7BA68907272A279900EFC32F /* Call Management */ = { isa = PBXGroup; children = ( - 7BC707F127290ACB002817AD /* SessionCallManager.swift */, 7BA68908272A27BE00EFC32F /* SessionCall.swift */, + 7BC707F127290ACB002817AD /* SessionCallManager.swift */, + 7BA6890C27325CCC00EFC32F /* SessionCallManager+CXCallController.swift */, + 7BA6890E27325CE300EFC32F /* SessionCallManager+CXProvider.swift */, ); path = "Call Management"; sourceTree = ""; @@ -4894,6 +4900,7 @@ B82B408C239A068800A248E7 /* RegisterVC.swift in Sources */, 346129991FD1E4DA00532771 /* SignalApp.m in Sources */, 3496957121A301A100DCFE74 /* OWSBackupImportJob.m in Sources */, + 7BA6890F27325CE300EFC32F /* SessionCallManager+CXProvider.swift in Sources */, 34BECE301F7ABCF800D7438D /* GifPickerLayout.swift in Sources */, C331FFFE2558FF3B00070591 /* ConversationCell.swift in Sources */, B8F5F72325F1B4CA003BF8D4 /* DownloadAttachmentModal.swift in Sources */, @@ -4908,6 +4915,7 @@ B8AF4BB426A5204600583500 /* SendSeedModal.swift in Sources */, B821494625D4D6FF009C0F2A /* URLModal.swift in Sources */, B877E24226CA12910007970A /* CallVC.swift in Sources */, + 7BA6890D27325CCC00EFC32F /* SessionCallManager+CXCallController.swift in Sources */, C374EEEB25DA3CA70073A857 /* ConversationTitleView.swift in Sources */, B88FA7F2260C3EB10049422F /* OpenGroupSuggestionGrid.swift in Sources */, 4CA485BB2232339F004B9E7D /* PhotoCaptureViewController.swift in Sources */, diff --git a/Session/Calls/Call Management/SessionCall.swift b/Session/Calls/Call Management/SessionCall.swift index d2320a43b..a1c2fc9a8 100644 --- a/Session/Calls/Call Management/SessionCall.swift +++ b/Session/Calls/Call Management/SessionCall.swift @@ -1,6 +1,7 @@ import Foundation import WebRTC import SessionMessagingKit +import PromiseKit public final class SessionCall: NSObject, WebRTCSessionDelegate { // MARK: Metadata Properties @@ -147,14 +148,18 @@ public final class SessionCall: NSObject, WebRTCSessionDelegate { // MARK: Actions func startSessionCall(completion: (() -> Void)?) { guard case .offer = mode else { return } - AppEnvironment.shared.callManager.reportOutgoingCall(self) - Storage.write { transaction in - self.webRTCSession.sendPreOffer(to: self.sessionID, using: transaction).done { - self.webRTCSession.sendOffer(to: self.sessionID, using: transaction).done { - self.hasStartedConnecting = true - }.retainUntilComplete() - }.retainUntilComplete() - } + var promise: Promise! + Storage.write(with: { transaction in + promise = self.webRTCSession.sendPreOffer(to: self.sessionID, using: transaction) + }, completion: { [weak self] in + let _ = promise.done { + Storage.shared.write { transaction in + self?.webRTCSession.sendOffer(to: self!.sessionID, using: transaction as! YapDatabaseReadWriteTransaction).done { + self?.hasStartedConnecting = true + }.retainUntilComplete() + } + } + }) completion?() } @@ -175,7 +180,6 @@ public final class SessionCall: NSObject, WebRTCSessionDelegate { self.webRTCSession.endCall(with: self.sessionID, using: transaction) } hasEnded = true - AppEnvironment.shared.callManager.reportCurrentCallEnded() } // MARK: Renderer diff --git a/Session/Calls/Call Management/SessionCallManager+CXCallController.swift b/Session/Calls/Call Management/SessionCallManager+CXCallController.swift new file mode 100644 index 000000000..c543f5026 --- /dev/null +++ b/Session/Calls/Call Management/SessionCallManager+CXCallController.swift @@ -0,0 +1,47 @@ +import CallKit +import SessionUtilitiesKit + +extension SessionCallManager { + public func startCall(_ call: SessionCall, completion: (() -> Void)?) { + guard case .offer = call.mode else { return } + let handle = CXHandle(type: .generic, value: call.sessionID) + let startCallAction = CXStartCallAction(call: call.uuid, handle: handle) + + startCallAction.isVideo = false + + let transaction = CXTransaction() + transaction.addAction(startCallAction) + + reportOutgoingCall(call) + requestTransaction(transaction) + completion?() + } + + public func endCall(_ call: SessionCall, completion: (() -> Void)?) { + let endCallAction = CXEndCallAction(call: call.uuid) + let transaction = CXTransaction() + transaction.addAction(endCallAction) + + requestTransaction(transaction) + completion?() + } + + // Not currently in use + public func setOnHoldStatus(for call: SessionCall) { + let setHeldCallAction = CXSetHeldCallAction(call: call.uuid, onHold: true) + let transaction = CXTransaction() + transaction.addAction(setHeldCallAction) + + requestTransaction(transaction) + } + + private func requestTransaction(_ transaction: CXTransaction) { + callController.request(transaction) { error in + if let error = error { + SNLog("Error requesting transaction: \(error)") + } else { + SNLog("Requested transaction successfully") + } + } + } +} diff --git a/Session/Calls/Call Management/SessionCallManager+CXProvider.swift b/Session/Calls/Call Management/SessionCallManager+CXProvider.swift new file mode 100644 index 000000000..3b0cfa9b7 --- /dev/null +++ b/Session/Calls/Call Management/SessionCallManager+CXProvider.swift @@ -0,0 +1,44 @@ +import CallKit + +extension SessionCallManager: CXProviderDelegate { + public func providerDidReset(_ provider: CXProvider) { + AssertIsOnMainThread() + currentCall?.endSessionCall() + } + + public func provider(_ provider: CXProvider, perform action: CXStartCallAction) { + AssertIsOnMainThread() + guard let call = self.currentCall else { return action.fail() } + call.startSessionCall(completion: nil) + action.fulfill() + } + + public func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) { + AssertIsOnMainThread() + guard let _ = self.currentCall else { return action.fail() } + let userDefaults = UserDefaults.standard + if userDefaults[.hasSeenCallIPExposureWarning] { + showCallVC() + } else { + showCallModal() + } + action.fulfill() + } + + public func provider(_ provider: CXProvider, perform action: CXEndCallAction) { + AssertIsOnMainThread() + guard let call = self.currentCall else { return action.fail() } + call.endSessionCall() + reportCurrentCallEnded(reason: nil) + action.fulfill() + } + + public func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) { + // TODO: set on hold + } + + public func provider(_ provider: CXProvider, timedOutPerforming action: CXAction) { + // TODO: handle timeout + } +} + diff --git a/Session/Calls/Call Management/SessionCallManager.swift b/Session/Calls/Call Management/SessionCallManager.swift index b309ce49a..914bf5255 100644 --- a/Session/Calls/Call Management/SessionCallManager.swift +++ b/Session/Calls/Call Management/SessionCallManager.swift @@ -1,8 +1,9 @@ import CallKit import SessionMessagingKit -public final class SessionCallManager: NSObject, CXProviderDelegate { - private let provider: CXProvider +public final class SessionCallManager: NSObject { + let provider: CXProvider + let callController = CXCallController() var currentCall: SessionCall? private static var _sharedProvider: CXProvider? @@ -43,11 +44,6 @@ public final class SessionCallManager: NSObject, CXProviderDelegate { self.provider.setDelegate(self, queue: nil) } - public func providerDidReset(_ provider: CXProvider) { - AssertIsOnMainThread() - currentCall?.endSessionCall() - } - public func reportOutgoingCall(_ call: SessionCall) { AssertIsOnMainThread() self.currentCall = call @@ -82,8 +78,14 @@ public final class SessionCallManager: NSObject, CXProviderDelegate { } } - public func reportCurrentCallEnded() { + public func reportCurrentCallEnded(reason: CXCallEndedReason?) { + guard let call = currentCall else { return } + if let reason = reason { + self.provider.reportCall(with: call.uuid, endedAt: nil, reason: reason) + } + self.currentCall?.webRTCSession.dropConnection() self.currentCall = nil + WebRTCSession.current = nil } // MARK: Util @@ -99,4 +101,27 @@ public final class SessionCallManager: NSObject, CXProviderDelegate { // Is there any reason to support this? callUpdate.supportsDTMF = false } + + internal func showCallModal() { + let callModal = CallModal() { [weak self] in + self?.showCallVC() + } + callModal.modalPresentationStyle = .overFullScreen + callModal.modalTransitionStyle = .crossDissolve + guard let presentingVC = CurrentAppContext().frontmostViewController() else { preconditionFailure() } // TODO: Handle more gracefully + presentingVC.present(callModal, animated: true, completion: nil) + } + + internal func showCallVC() { + guard let presentingVC = CurrentAppContext().frontmostViewController() else { preconditionFailure() } // TODO: Handle more gracefully + let callVC = CallVC(for: self.currentCall!) + callVC.shouldAnswer = true + if let conversationVC = presentingVC as? ConversationVC { + callVC.conversationVC = conversationVC + conversationVC.inputAccessoryView?.isHidden = true + conversationVC.inputAccessoryView?.alpha = 0 + } + presentingVC.present(callVC, animated: true, completion: nil) + } } + diff --git a/Session/Calls/CallVC.swift b/Session/Calls/CallVC.swift index 2d9a17afc..e724cd338 100644 --- a/Session/Calls/CallVC.swift +++ b/Session/Calls/CallVC.swift @@ -194,7 +194,7 @@ final class CallVC : UIViewController, VideoPreviewDelegate { if shouldRestartCamera { cameraManager.prepare() } touch(call.videoCapturer) titleLabel.text = self.call.contactName - self.call.startSessionCall{ + AppEnvironment.shared.callManager.startCall(call) { self.callInfoLabel.text = "Ringing..." self.answerButton.isHidden = true } @@ -319,7 +319,7 @@ final class CallVC : UIViewController, VideoPreviewDelegate { } @objc private func endCall() { - self.call.endSessionCall() + AppEnvironment.shared.callManager.endCall(call, completion: nil) } @objc private func minimize() { diff --git a/Session/Calls/Views & Modals/IncomingCallBanner.swift b/Session/Calls/Views & Modals/IncomingCallBanner.swift index 29f1a0221..9ebdee16d 100644 --- a/Session/Calls/Views & Modals/IncomingCallBanner.swift +++ b/Session/Calls/Views & Modals/IncomingCallBanner.swift @@ -155,8 +155,9 @@ final class IncomingCallBanner: UIView, UIGestureRecognizerDelegate { } @objc private func endCall() { - self.call.endSessionCall() - self.dismiss() + AppEnvironment.shared.callManager.endCall(call) { + self.dismiss() + } } public func showCallVC(answer: Bool) { diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index 2ca189647..be98926c7 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -8,20 +8,24 @@ extension AppDelegate { // MARK: Call handling func createNewIncomingCall(caller: String, uuid: String) { let call = SessionCall(for: caller, uuid: uuid, mode: .answer) - guard let presentingVC = CurrentAppContext().frontmostViewController() else { preconditionFailure() } // TODO: Handle more gracefully - if let conversationVC = presentingVC as? ConversationVC, let contactThread = conversationVC.thread as? TSContactThread, contactThread.contactSessionID() == caller { - let callVC = CallVC(for: call) - callVC.conversationVC = conversationVC - conversationVC.inputAccessoryView?.isHidden = true - conversationVC.inputAccessoryView?.alpha = 0 - presentingVC.present(callVC, animated: true, completion: nil) - } else { - call.reportIncomingCallIfNeeded{ error in - if let error = error { - SNLog("[Calls] Failed to report incoming call to CallKit due to error: \(error)") - let incomingCallBanner = IncomingCallBanner(for: call) - incomingCallBanner.show() + if CurrentAppContext().isMainAppAndActive { + guard let presentingVC = CurrentAppContext().frontmostViewController() else { preconditionFailure() } // TODO: Handle more gracefully + if let conversationVC = presentingVC as? ConversationVC, let contactThread = conversationVC.thread as? TSContactThread, contactThread.contactSessionID() == caller { + DispatchQueue.main.async { + let callVC = CallVC(for: call) + callVC.conversationVC = conversationVC + conversationVC.inputAccessoryView?.isHidden = true + conversationVC.inputAccessoryView?.alpha = 0 + presentingVC.present(callVC, animated: true, completion: nil) } + return + } + } + call.reportIncomingCallIfNeeded{ error in + if let error = error { + SNLog("[Calls] Failed to report incoming call to CallKit due to error: \(error)") + let incomingCallBanner = IncomingCallBanner(for: call) + incomingCallBanner.show() } } } @@ -53,8 +57,7 @@ extension AppDelegate { if let currentBanner = IncomingCallBanner.current { currentBanner.dismiss() } if let callVC = CurrentAppContext().frontmostViewController() as? CallVC { callVC.handleEndCallMessage(message) } if let miniCallView = MiniCallView.current { miniCallView.dismiss() } - WebRTCSession.current?.dropConnection() - WebRTCSession.current = nil + AppEnvironment.shared.callManager.reportCurrentCallEnded(reason: .remoteEnded) } } } diff --git a/SessionMessagingKit/Calls/WebRTCSession+MessageHandling.swift b/SessionMessagingKit/Calls/WebRTCSession+MessageHandling.swift index ec860bb65..7ad8413d4 100644 --- a/SessionMessagingKit/Calls/WebRTCSession+MessageHandling.swift +++ b/SessionMessagingKit/Calls/WebRTCSession+MessageHandling.swift @@ -11,7 +11,7 @@ extension WebRTCSession { print("[Calls] Received remote SDP: \(sdp.sdp).") peerConnection.setRemoteDescription(sdp, completionHandler: { [weak self] error in if let error = error { - SNLog("Couldn't set SDP due to error: \(error).") + SNLog("[Calls] Couldn't set SDP due to error: \(error).") } else { guard let self = self, sdp.type == .offer, self.peerConnection.localDescription == nil else { return } diff --git a/SessionMessagingKit/Calls/WebRTCSession.swift b/SessionMessagingKit/Calls/WebRTCSession.swift index f2d5cde4c..95d617a67 100644 --- a/SessionMessagingKit/Calls/WebRTCSession.swift +++ b/SessionMessagingKit/Calls/WebRTCSession.swift @@ -227,8 +227,6 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate { message.kind = .endCall print("[Calls] Sending end call message.") MessageSender.sendNonDurably(message, in: thread, using: transaction).retainUntilComplete() - dropConnection() - WebRTCSession.current = nil } public func dropConnection() {