refactoring for CallKit

pull/560/head
ryanzhao 4 years ago
parent e530b50938
commit 6f78d6dfbe

@ -2,12 +2,14 @@ import Foundation
import WebRTC import WebRTC
import SessionMessagingKit import SessionMessagingKit
public final class SessionCall: NSObject { public final class SessionCall: NSObject, WebRTCSessionDelegate {
// MARK: Metadata Properties // MARK: Metadata Properties
let uuid: UUID let uuid: UUID
let sessionID: String let sessionID: String
let mode: Mode let mode: Mode
let webRTCSession: WebRTCSession let webRTCSession: WebRTCSession
var remoteSDP: RTCSessionDescription? = nil
var isWaitingForRemoteSDP = false
var contactName: String { var contactName: String {
let contact = Storage.shared.getContact(with: self.sessionID) let contact = Storage.shared.getContact(with: self.sessionID)
return contact?.displayName(for: Contact.Context.regular) ?? self.sessionID return contact?.displayName(for: Contact.Context.regular) ?? self.sessionID
@ -20,10 +22,40 @@ public final class SessionCall: NSObject {
} }
} }
// MARK: Control
lazy public var videoCapturer: RTCVideoCapturer = {
return RTCCameraVideoCapturer(delegate: webRTCSession.localVideoSource)
}()
var isRemoteVideoEnabled = false {
didSet {
remoteVideoStateDidChange?(isRemoteVideoEnabled)
}
}
var isMuted = false {
willSet {
if newValue {
webRTCSession.mute()
} else {
webRTCSession.unmute()
}
}
}
var isVideoEnabled = false {
willSet {
if newValue {
webRTCSession.turnOnVideo()
} else {
webRTCSession.turnOffVideo()
}
}
}
// MARK: Mode // MARK: Mode
enum Mode { enum Mode {
case offer case offer
case answer(sdp: RTCSessionDescription) case answer
} }
// MARK: Call State Properties // MARK: Call State Properties
@ -60,6 +92,7 @@ public final class SessionCall: NSObject {
var hasStartedConnectingDidChange: (() -> Void)? var hasStartedConnectingDidChange: (() -> Void)?
var hasConnectedDidChange: (() -> Void)? var hasConnectedDidChange: (() -> Void)?
var hasEndedDidChange: (() -> Void)? var hasEndedDidChange: (() -> Void)?
var remoteVideoStateDidChange: ((Bool) -> Void)?
// MARK: Derived Properties // MARK: Derived Properties
var hasStartedConnecting: Bool { var hasStartedConnecting: Bool {
@ -92,15 +125,22 @@ public final class SessionCall: NSObject {
self.mode = mode self.mode = mode
self.webRTCSession = WebRTCSession.current ?? WebRTCSession(for: sessionID, with: uuid) self.webRTCSession = WebRTCSession.current ?? WebRTCSession(for: sessionID, with: uuid)
super.init() super.init()
reportIncomingCallIfNeeded() self.webRTCSession.delegate = self
} }
func reportIncomingCallIfNeeded() { func reportIncomingCallIfNeeded(completion: @escaping (Error?) -> Void) {
guard case .answer(_) = mode else { return } guard case .answer = mode else { return }
AppEnvironment.shared.callManager.reportIncomingCall(self, callerName: contactName) { error in AppEnvironment.shared.callManager.reportIncomingCall(self, callerName: contactName) { error in
if let error = error { completion(error)
SNLog("[Calls] Failed to report incoming call to CallKit due to error: \(error)") }
} }
func didReceiveRemoteSDP(sdp: RTCSessionDescription) {
guard remoteSDP == nil else { return }
remoteSDP = sdp
if isWaitingForRemoteSDP {
webRTCSession.handleRemoteSDP(sdp, from: sessionID) // This sends an answer message internally
isWaitingForRemoteSDP = false
} }
} }
@ -119,9 +159,13 @@ public final class SessionCall: NSObject {
} }
func answerSessionCall(completion: (() -> Void)?) { func answerSessionCall(completion: (() -> Void)?) {
guard case let .answer(sdp) = mode else { return } guard case .answer = mode else { return }
hasStartedConnecting = true hasStartedConnecting = true
webRTCSession.handleRemoteSDP(sdp, from: sessionID) // This sends an answer message internally if let sdp = remoteSDP {
webRTCSession.handleRemoteSDP(sdp, from: sessionID) // This sends an answer message internally
} else {
isWaitingForRemoteSDP = true
}
completion?() completion?()
} }
@ -133,4 +177,31 @@ public final class SessionCall: NSObject {
hasEnded = true hasEnded = true
AppEnvironment.shared.callManager.reportCurrentCallEnded() AppEnvironment.shared.callManager.reportCurrentCallEnded()
} }
// MARK: Renderer
func attachRemoteVideoRenderer(_ renderer: RTCVideoRenderer) {
webRTCSession.attachRemoteRenderer(renderer)
}
func attachLocalVideoRenderer(_ renderer: RTCVideoRenderer) {
webRTCSession.attachLocalRenderer(renderer)
}
// MARK: Delegate
public func webRTCIsConnected() {
self.hasConnected = true
}
public func isRemoteVideoDidChange(isEnabled: Bool) {
isRemoteVideoEnabled = isEnabled
}
public func dataChannelDidOpen() {
// Send initial video status
if (isVideoEnabled) {
webRTCSession.turnOnVideo()
} else {
webRTCSession.turnOffVideo()
}
}
} }

@ -9,6 +9,6 @@ extension CallVC : CameraManagerDelegate {
let timestampNs = Int64(timestamp * 1000000000) let timestampNs = Int64(timestamp * 1000000000)
let frame = RTCVideoFrame(buffer: rtcPixelBuffer, rotation: RTCVideoRotation._0, timeStampNs: timestampNs) let frame = RTCVideoFrame(buffer: rtcPixelBuffer, rotation: RTCVideoRotation._0, timeStampNs: timestampNs)
frame.timeStamp = Int32(timestamp) frame.timeStamp = Int32(timestamp)
webRTCSession.handleLocalFrameCaptured(frame) call.webRTCSession.handleLocalFrameCaptured(frame)
} }
} }

@ -4,12 +4,9 @@ import SessionMessagingKit
import SessionUtilitiesKit import SessionUtilitiesKit
import UIKit import UIKit
final class CallVC : UIViewController, WebRTCSessionDelegate, VideoPreviewDelegate { final class CallVC : UIViewController, VideoPreviewDelegate {
let call: SessionCall let call: SessionCall
var webRTCSession: WebRTCSession { return call.webRTCSession }
var shouldAnswer = false var shouldAnswer = false
var isMuted = false
var isVideoEnabled = false
var shouldRestartCamera = true var shouldRestartCamera = true
weak var conversationVC: ConversationVC? = nil weak var conversationVC: ConversationVC? = nil
@ -19,14 +16,10 @@ final class CallVC : UIViewController, WebRTCSessionDelegate, VideoPreviewDelega
return result return result
}() }()
lazy var videoCapturer: RTCVideoCapturer = {
return RTCCameraVideoCapturer(delegate: webRTCSession.localVideoSource)
}()
// MARK: UI Components // MARK: UI Components
private lazy var localVideoView: RTCMTLVideoView = { private lazy var localVideoView: RTCMTLVideoView = {
let result = RTCMTLVideoView() let result = RTCMTLVideoView()
result.isHidden = !isVideoEnabled result.isHidden = !call.isVideoEnabled
result.contentMode = .scaleAspectFill result.contentMode = .scaleAspectFill
result.set(.width, to: 80) result.set(.width, to: 80)
result.set(.height, to: 173) result.set(.height, to: 173)
@ -98,7 +91,7 @@ final class CallVC : UIViewController, WebRTCSessionDelegate, VideoPreviewDelega
private lazy var switchCameraButton: UIButton = { private lazy var switchCameraButton: UIButton = {
let result = UIButton(type: .custom) let result = UIButton(type: .custom)
result.isEnabled = isVideoEnabled result.isEnabled = call.isVideoEnabled
let image = UIImage(named: "SwitchCamera")!.withTint(.white) let image = UIImage(named: "SwitchCamera")!.withTint(.white)
result.setImage(image, for: UIControl.State.normal) result.setImage(image, for: UIControl.State.normal)
result.set(.width, to: 60) result.set(.width, to: 60)
@ -161,13 +154,35 @@ final class CallVC : UIViewController, WebRTCSessionDelegate, VideoPreviewDelega
init(for call: SessionCall) { init(for call: SessionCall) {
self.call = call self.call = call
super.init(nibName: nil, bundle: nil) super.init(nibName: nil, bundle: nil)
self.call.webRTCSession.delegate = self setupStateChangeCallbacks()
self.modalPresentationStyle = .overFullScreen
self.modalTransitionStyle = .crossDissolve
}
func setupStateChangeCallbacks() {
self.call.remoteVideoStateDidChange = { isEnabled in
DispatchQueue.main.async {
UIView.animate(withDuration: 0.25) {
self.remoteVideoView.alpha = isEnabled ? 1 : 0
}
}
}
self.call.hasConnectedDidChange = {
DispatchQueue.main.async {
self.callInfoLabel.text = "Connected"
self.minimizeButton.isHidden = false
UIView.animate(withDuration: 0.5, delay: 1, options: [], animations: {
self.callInfoLabel.alpha = 0
}, completion: { _ in
self.callInfoLabel.isHidden = true
self.callInfoLabel.alpha = 1
})
}
}
self.call.hasEndedDidChange = { self.call.hasEndedDidChange = {
self.conversationVC?.showInputAccessoryView() self.conversationVC?.showInputAccessoryView()
self.presentingViewController?.dismiss(animated: true, completion: nil) self.presentingViewController?.dismiss(animated: true, completion: nil)
} }
self.modalPresentationStyle = .overFullScreen
self.modalTransitionStyle = .crossDissolve
} }
required init(coder: NSCoder) { preconditionFailure("Use init(for:) instead.") } required init(coder: NSCoder) { preconditionFailure("Use init(for:) instead.") }
@ -175,10 +190,9 @@ final class CallVC : UIViewController, WebRTCSessionDelegate, VideoPreviewDelega
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
view.backgroundColor = .black view.backgroundColor = .black
WebRTCSession.current = webRTCSession
setUpViewHierarchy() setUpViewHierarchy()
if shouldRestartCamera { cameraManager.prepare() } if shouldRestartCamera { cameraManager.prepare() }
touch(videoCapturer) touch(call.videoCapturer)
titleLabel.text = self.call.contactName titleLabel.text = self.call.contactName
self.call.startSessionCall{ self.call.startSessionCall{
self.callInfoLabel.text = "Ringing..." self.callInfoLabel.text = "Ringing..."
@ -197,12 +211,12 @@ final class CallVC : UIViewController, WebRTCSessionDelegate, VideoPreviewDelega
callInfoLabel.translatesAutoresizingMaskIntoConstraints = false callInfoLabel.translatesAutoresizingMaskIntoConstraints = false
callInfoLabel.center(in: view) callInfoLabel.center(in: view)
// Remote video view // Remote video view
webRTCSession.attachRemoteRenderer(remoteVideoView) call.attachRemoteVideoRenderer(remoteVideoView)
view.addSubview(remoteVideoView) view.addSubview(remoteVideoView)
remoteVideoView.translatesAutoresizingMaskIntoConstraints = false remoteVideoView.translatesAutoresizingMaskIntoConstraints = false
remoteVideoView.pin(to: view) remoteVideoView.pin(to: view)
// Local video view // Local video view
webRTCSession.attachLocalRenderer(localVideoView) call.attachLocalVideoRenderer(localVideoView)
view.addSubview(localVideoView) view.addSubview(localVideoView)
localVideoView.pin(.right, to: .right, of: view, withInset: -Values.smallSpacing) localVideoView.pin(.right, to: .right, of: view, withInset: -Values.smallSpacing)
let topMargin = UIApplication.shared.keyWindow!.safeAreaInsets.top + Values.veryLargeSpacing let topMargin = UIApplication.shared.keyWindow!.safeAreaInsets.top + Values.veryLargeSpacing
@ -252,45 +266,13 @@ final class CallVC : UIViewController, WebRTCSessionDelegate, VideoPreviewDelega
override func viewDidAppear(_ animated: Bool) { override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated) super.viewDidAppear(animated)
if (isVideoEnabled && shouldRestartCamera) { cameraManager.start() } if (call.isVideoEnabled && shouldRestartCamera) { cameraManager.start() }
shouldRestartCamera = true shouldRestartCamera = true
} }
override func viewWillDisappear(_ animated: Bool) { override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated) super.viewWillDisappear(animated)
if (isVideoEnabled && shouldRestartCamera) { cameraManager.stop() } if (call.isVideoEnabled && shouldRestartCamera) { cameraManager.stop() }
}
// MARK: Delegate
func webRTCIsConnected() {
DispatchQueue.main.async {
self.callInfoLabel.text = "Connected"
self.call.hasConnected = true
self.minimizeButton.isHidden = false
UIView.animate(withDuration: 0.5, delay: 1, options: [], animations: {
self.callInfoLabel.alpha = 0
}, completion: { _ in
self.callInfoLabel.isHidden = true
self.callInfoLabel.alpha = 1
})
}
}
func isRemoteVideoDidChange(isEnabled: Bool) {
DispatchQueue.main.async {
UIView.animate(withDuration: 0.25) {
self.remoteVideoView.alpha = isEnabled ? 1 : 0
}
}
}
func dataChannelDidOpen() {
// Send initial video status
if (isVideoEnabled) {
webRTCSession.turnOnVideo()
} else {
webRTCSession.turnOffVideo()
}
} }
// MARK: Interaction // MARK: Interaction
@ -349,13 +331,12 @@ final class CallVC : UIViewController, WebRTCSessionDelegate, VideoPreviewDelega
} }
@objc private func operateCamera() { @objc private func operateCamera() {
if (isVideoEnabled) { if (call.isVideoEnabled) {
webRTCSession.turnOffVideo()
localVideoView.isHidden = true localVideoView.isHidden = true
cameraManager.stop() cameraManager.stop()
videoButton.alpha = 0.5 videoButton.alpha = 0.5
switchCameraButton.isEnabled = false switchCameraButton.isEnabled = false
isVideoEnabled = false call.isVideoEnabled = false
} else { } else {
let previewVC = VideoPreviewVC() let previewVC = VideoPreviewVC()
previewVC.delegate = self previewVC.delegate = self
@ -364,13 +345,12 @@ final class CallVC : UIViewController, WebRTCSessionDelegate, VideoPreviewDelega
} }
func cameraDidConfirmTurningOn() { func cameraDidConfirmTurningOn() {
webRTCSession.turnOnVideo()
localVideoView.isHidden = false localVideoView.isHidden = false
cameraManager.prepare() cameraManager.prepare()
cameraManager.start() cameraManager.start()
videoButton.alpha = 1.0 videoButton.alpha = 1.0
switchCameraButton.isEnabled = true switchCameraButton.isEnabled = true
isVideoEnabled = true call.isVideoEnabled = true
} }
@objc private func switchCamera() { @objc private func switchCamera() {
@ -378,14 +358,12 @@ final class CallVC : UIViewController, WebRTCSessionDelegate, VideoPreviewDelega
} }
@objc private func switchAudio() { @objc private func switchAudio() {
if isMuted { if call.isMuted {
switchAudioButton.backgroundColor = UIColor(hex: 0x1F1F1F) switchAudioButton.backgroundColor = UIColor(hex: 0x1F1F1F)
isMuted = false call.isMuted = false
webRTCSession.unmute()
} else { } else {
switchAudioButton.backgroundColor = Colors.destructive switchAudioButton.backgroundColor = Colors.destructive
isMuted = true call.isMuted = true
webRTCSession.mute()
} }
} }

@ -38,7 +38,7 @@ final class MiniCallView: UIView {
self.addSubview(background) self.addSubview(background)
background.pin(to: self) background.pin(to: self)
// Remote video view // Remote video view
callVC.webRTCSession.attachRemoteRenderer(remoteVideoView) callVC.call.attachRemoteVideoRenderer(remoteVideoView)
self.addSubview(remoteVideoView) self.addSubview(remoteVideoView)
remoteVideoView.translatesAutoresizingMaskIntoConstraints = false remoteVideoView.translatesAutoresizingMaskIntoConstraints = false
remoteVideoView.pin(to: self) remoteVideoView.pin(to: self)

@ -6,23 +6,38 @@ import UIKit
extension AppDelegate { extension AppDelegate {
// MARK: Call handling // 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()
}
}
}
}
@objc func setUpCallHandling() { @objc func setUpCallHandling() {
// Pre offer messages
MessageReceiver.handlePreOfferCallMessage = { message in
guard CurrentAppContext().isMainApp else { return }
self.createNewIncomingCall(caller: message.sender!, uuid: message.uuid!)
}
// Offer messages // Offer messages
MessageReceiver.handleOfferCallMessage = { message in MessageReceiver.handleOfferCallMessage = { message in
DispatchQueue.main.async { DispatchQueue.main.async {
guard let call = AppEnvironment.shared.callManager.currentCall, message.uuid == call.uuid.uuidString else { return }
let sdp = RTCSessionDescription(type: .offer, sdp: message.sdps![0]) let sdp = RTCSessionDescription(type: .offer, sdp: message.sdps![0])
let call = SessionCall(for: message.sender!, uuid: message.uuid!, mode: .answer(sdp: sdp)) call.didReceiveRemoteSDP(sdp: sdp)
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() == message.sender! {
let callVC = CallVC(for: call)
callVC.conversationVC = conversationVC
conversationVC.inputAccessoryView?.isHidden = true
conversationVC.inputAccessoryView?.alpha = 0
presentingVC.present(callVC, animated: true, completion: nil)
} else {
let incomingCallBanner = IncomingCallBanner(for: call)
incomingCallBanner.show()
}
} }
} }
// Answer messages // Answer messages

@ -238,7 +238,14 @@ public enum PushRegistrationError: Error {
public func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType) { public func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType) {
owsAssertDebug(CurrentAppContext().isMainApp) owsAssertDebug(CurrentAppContext().isMainApp)
owsAssertDebug(type == .voIP) owsAssertDebug(type == .voIP)
Vibration.shared.startVibration() let payload = payload.dictionaryPayload
if let uuid = payload["uuid"] as? String, let caller = payload["caller"] as? String {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
appDelegate.createNewIncomingCall(caller: caller, uuid: uuid)
appDelegate.startPollerIfNeeded()
appDelegate.startClosedGroupPoller()
appDelegate.startOpenGroupPollersIfNeeded()
}
} }
} }

@ -251,9 +251,9 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate {
public func peerConnectionShouldNegotiate(_ peerConnection: RTCPeerConnection) { public func peerConnectionShouldNegotiate(_ peerConnection: RTCPeerConnection) {
print("[Calls] Peer connection should negotiate.") print("[Calls] Peer connection should negotiate.")
Storage.write { transaction in // Storage.write { transaction in
self.sendOffer(to: self.contactSessionID, using: transaction).retainUntilComplete() // self.sendOffer(to: self.contactSessionID, using: transaction).retainUntilComplete()
} // }
} }
public func peerConnection(_ peerConnection: RTCPeerConnection, didChange state: RTCIceConnectionState) { public func peerConnection(_ peerConnection: RTCPeerConnection, didChange state: RTCIceConnectionState) {

@ -279,7 +279,9 @@ extension MessageReceiver {
print("[Calls] Received pre-offer message.") print("[Calls] Received pre-offer message.")
if getWebRTCSession().uuid != message.uuid! { if getWebRTCSession().uuid != message.uuid! {
// TODO: Call in progress, put the new call on hold/reject // TODO: Call in progress, put the new call on hold/reject
return
} }
handlePreOfferCallMessage?(message)
case .offer: case .offer:
print("[Calls] Received offer message.") print("[Calls] Received offer message.")
if getWebRTCSession().uuid != message.uuid! { if getWebRTCSession().uuid != message.uuid! {

@ -2,6 +2,7 @@ import SessionUtilitiesKit
public enum MessageReceiver { public enum MessageReceiver {
private static var lastEncryptionKeyPairRequest: [String:Date] = [:] private static var lastEncryptionKeyPairRequest: [String:Date] = [:]
public static var handlePreOfferCallMessage: ((CallMessage) -> Void)?
public static var handleOfferCallMessage: ((CallMessage) -> Void)? public static var handleOfferCallMessage: ((CallMessage) -> Void)?
public static var handleAnswerCallMessage: ((CallMessage) -> Void)? public static var handleAnswerCallMessage: ((CallMessage) -> Void)?
public static var handleEndCallMessage: ((CallMessage) -> Void)? public static var handleEndCallMessage: ((CallMessage) -> Void)?

@ -5,7 +5,7 @@ import PromiseKit
public final class PushNotificationAPI : NSObject { public final class PushNotificationAPI : NSObject {
// MARK: Settings // MARK: Settings
public static let server = "https://live.apns.getsession.org" public static let server = "https://dev.apns.getsession.org"
public static let serverPublicKey = "642a6585919742e5a2d4dc51244964fbcd8bcab2b75612407de58b810740d049" public static let serverPublicKey = "642a6585919742e5a2d4dc51244964fbcd8bcab2b75612407de58b810740d049"
private static let maxRetryCount: UInt = 4 private static let maxRetryCount: UInt = 4
private static let tokenExpirationInterval: TimeInterval = 12 * 60 * 60 private static let tokenExpirationInterval: TimeInterval = 12 * 60 * 60

@ -96,7 +96,7 @@ public final class NotificationServiceExtension : UNNotificationServiceExtension
notificationContent.badge = 1 notificationContent.badge = 1
notificationContent.title = "Session" notificationContent.title = "Session"
notificationContent.body = "\(senderDisplayName) is calling..." notificationContent.body = "\(senderDisplayName) is calling..."
return self.handleSuccessForIncomingCall(for: notificationContent, callID: callMessage.uuid!) return self.handleSuccessForIncomingCall(for: notificationContent, callMessage: callMessage)
default: return self.completeSilenty() default: return self.completeSilenty()
} }
if (senderPublicKey == userPublicKey) { if (senderPublicKey == userPublicKey) {
@ -216,18 +216,21 @@ public final class NotificationServiceExtension : UNNotificationServiceExtension
contentHandler!(.init()) contentHandler!(.init())
} }
private func handleSuccessForIncomingCall(for content: UNMutableNotificationContent, callID: String) { private func handleSuccessForIncomingCall(for content: UNMutableNotificationContent, callMessage: CallMessage) {
// TODO: poll for the real offer, play incoming call ring
if #available(iOSApplicationExtension 14.5, *) { if #available(iOSApplicationExtension 14.5, *) {
CXProvider.reportNewIncomingVoIPPushPayload(["uuid": callID]) { error in if let uuid = callMessage.uuid, let caller = callMessage.sender {
if let error = error { let payload = ["uuid": uuid, "caller": caller]
owsFailDebug("Failed to notify main app of call message: \(error)") return CXProvider.reportNewIncomingVoIPPushPayload(payload) { error in
} else { if let error = error {
Logger.info("Successfully notified main app of call message.") owsFailDebug("Failed to notify main app of call message: \(error)")
} else {
Logger.info("Successfully notified main app of call message.")
}
self.completeSilenty()
} }
self.contentHandler!(content)
} }
} }
self.contentHandler!(content)
} }
private func handleSuccess(for content: UNMutableNotificationContent) { private func handleSuccess(for content: UNMutableNotificationContent) {

Loading…
Cancel
Save