后台状态下如何响应Sinch的调用?

How to respond to Sinch call in the background state?

我使用 Sinch 进行音频通话,我也使用 CallKit。我在应用程序处于后台状态时遇到问题,我通过 CXProviderDelegate 收到对 CallKit 的呼叫,但 sinch 客户端没有活动的来电。请告诉我如何解决这个问题?

final class ProviderDelegate: NSObject, CXProviderDelegate {

    private let provider: CXProvider

    static let shared = ProviderDelegate()

    private override init() {
        provider = CXProvider(configuration: type(of: self).providerConfiguration)

        super.init()

        provider.setDelegate(self, queue: nil)
    }


    /// The app's provider configuration, representing its CallKit capabilities
    static var providerConfiguration: CXProviderConfiguration {
        let localizedName = NSLocalizedString("App name", comment: "Name of application")
        let providerConfiguration = CXProviderConfiguration(localizedName: localizedName)

        providerConfiguration.supportsVideo = true

        providerConfiguration.maximumCallsPerCallGroup = 1

        providerConfiguration.supportedHandleTypes = [.phoneNumber]

        if let iconMaskImage = UIImage(named: "IconMask") {
            providerConfiguration.iconTemplateImageData = UIImagePNGRepresentation(iconMaskImage)
        }

        providerConfiguration.ringtoneSound = "Ringtone.aif"

        return providerConfiguration
    }

    // MARK: Incoming Calls

    /// Use CXProvider to report the incoming call to the system
    open func reportIncomingCall(uuid: UUID, handle: String, contactID: String, hasVideo: Bool = false, completion: ((Error?) -> Void)? = nil) {
        // Construct a CXCallUpdate describing the incoming call, including the caller.
        let update = CXCallUpdate()
        update.remoteHandle = CXHandle(type: .phoneNumber, value: handle)
        update.hasVideo = hasVideo

        // Report the incoming call to the system
        provider.reportNewIncomingCall(with: uuid, update: update) { error in
            if error == nil {
                let call = SwiftlyChatCall(uuid: uuid, contactID: contactID)
                call.handle = handle

                SwiftlyChatCallManager.shared.addCall(call)
            }

            completion?(error)
        }
    }

    // MARK: CXProviderDelegate

    func providerDidReset(_ provider: CXProvider) {
        print("Provider did reset")

        AudioManager.shared.stopAudio()

        /*
         End any ongoing calls if the provider resets, and remove them from the app's list of calls,
         since they are no longer valid.
         */
        for call in SwiftlyChatCallManager.shared.calls {
            call.endSpeakerboxCall()
        }

        // Remove all calls from the app's list of calls.
        SwiftlyChatCallManager.shared.removeAllCalls()
    }

    func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
        debugPrint("Provider, CXAnswerCallAction")
        guard SwiftlyChatCallManager.shared.callWithUUID(uuid: action.callUUID) != nil else {
            debugPrint("CXAnswerCallAction fail")
            action.fail()
            return
        }

        SinchManager.default.answerCall()

        // Signal to the system that the action has been successfully performed.
        action.fulfill()
    }

    func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
        guard let call = SwiftlyChatCallManager.shared.callWithUUID(uuid: action.callUUID) else {
            action.fail()
            return
        }

        debugPrint("CXEndCallAction", #function)
        SinchManager.default.cancelCall()

        // Signal to the system that the action has been successfully performed.
        action.fulfill()

        // Remove the ended call from the app's list of calls.
        SwiftlyChatCallManager.shared.removeCall(call)
    }

    func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) {
        debugPrint("provider CXSetHeldCallAction")
        guard let call = SwiftlyChatCallManager.shared.callWithUUID(uuid: action.callUUID) else {
            action.fail()
            return
        }

        // Update the SpeakerboxCall's underlying hold state.
        call.isOnHold = action.isOnHold

        // Stop or start audio in response to holding or unholding the call.
        if call.isOnHold {
            AudioManager.shared.stopAudio()
        } else {
            AudioManager.shared.startAudio()
        }

        // Signal to the system that the action has been successfully performed.
        action.fulfill()

        // Remove the ended call from the app's list of calls.

        SwiftlyChatCallManager.shared.removeCall(call)
    }

    func provider(_ provider: CXProvider, timedOutPerforming action: CXAction) {
        print("Timed out \(#function)")

        // React to the action timeout if necessary, such as showing an error UI.
    }

    func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
        print("Received \(#function)")
        // Start call audio media, now that the audio session has been activated after having its priority boosted.
        SinchManager.default.callKitDidActive(provider, audioSession: audioSession)
    }

    func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
        print("Received \(#function)")

        /*
         Restart any non-call related audio now that the app's audio session has been
         de-activated after having its priority restored to normal.
         */
    }

}


final class VOIPManager: NSObject {

    private override init() {
        super.init()
    }

    static let `default` = VOIPManager()

    private let incomingCallIdentificator = "SIN_INCOMING_CALL"
    private let canceledIncomingCallIndentificator = "SIN_CANCEL_CALL"

    open func registration() {
        let mainQueue = DispatchQueue.main
        // Create a push registry object
        let voipRegistry = PKPushRegistry(queue: mainQueue)
        // Set the registry's delegate to self
        voipRegistry.delegate = self
        // Set the push type to VoIP
        voipRegistry.desiredPushTypes = [.voIP]
    }

}

extension VOIPManager: PKPushRegistryDelegate {

    func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) {
        // Register VoIP push token (a property of PKPushCredentials) with server
        guard type == .voIP else { return }
        SinchManager.default.registerDeviceToken(pushCredentials.token)
    }

    func pushRegistry(_ registry: PKPushRegistry, didInvalidatePushTokenFor type: PKPushType) {
        guard type == .voIP else { return }
    }

    func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
        guard type == .voIP else { return }
        debugPrint("didReceiveIncomingPushWith")
        DispatchQueue.global(qos: .default).async {
            guard var dict = payload.dictionaryPayload as? [String : Any] else { return }
            debugPrint("dict", dict)
            guard let sinString = dict["sin"] as? String else { return }
            guard let sinDict = sinString.toDictionary() else { return }
            dict["sin"] = sinDict
            guard let sinchIncomingCall = Mapper<SinchIncomingCallModel>().map(JSON: dict) else { return }
            let lockKey = sinchIncomingCall.aps.alert.locKey

            if lockKey == self.incomingCallIdentificator {
                self.incomingCallAction(sinchIncomingCall)
            } else if lockKey == self.canceledIncomingCallIndentificator {
                self.canceledIncomingCallAction(sinchIncomingCall)
            }
        }
    }

}

// MARK: - Actions

extension VOIPManager {

    private func incomingCallAction(_ sinchIncomingCall: SinchIncomingCallModel) {
        self.getDataForIncomingCall(sinchIncomingCall) { (contactID, phone) in
            DispatchQueue.global(qos: .default).async {
                self.displayIncomingCall(uuid: UUID(), handle: phone, contactID: contactID)
            }
        }
    }

    private func canceledIncomingCallAction(_ sinchIncomingCall: SinchIncomingCallModel) {
        self.getDataForIncomingCall(sinchIncomingCall) { (contactID, _) in
            SwiftlyChatCallManager.shared.end(contactID)
        }
    }

    private func displayIncomingCall(uuid: UUID, handle: String, contactID: String, hasVideo: Bool = false, completion: ((Error?) -> Void)? = nil) {
        ProviderDelegate.shared.reportIncomingCall(uuid: uuid, handle: handle, contactID: contactID, hasVideo: hasVideo, completion: completion)
    }

    private func getDataForIncomingCall(_ sinchIncomingCall: SinchIncomingCallModel, completion: ((_ contactID: String, _ phone: String) -> Void)?) {
        DispatchQueue.global(qos: .default).async {
            let contactsRealmManager = ContactsRealmManager()
            guard let contact = contactsRealmManager.getContact(sinchIncomingCall.sin.userID) else { return }
            let phoneNumber = contact.firstPhoneNumber() ?? ""
            completion?(contact.id, phoneNumber)
        }
    }


}

我也有SinchManager而且有这个方法

extension SinchManager {

    open func activeAudioSession(_ provider: CXProvider, audioSession: AVAudioSession) {
        sinch?.call().provider(provider, didActivate: audioSession)
    }

}

我解决了这个问题。我将此方法添加到 SinchManager

open func relayRemotePushNotification(_ userInfo: [AnyHashable : Any]) {
        DispatchQueue.main.async {
            guard let result = self.sinch?.relayRemotePushNotification(userInfo) else { return }
            guard result.isCall() else { return }
            debugPrint("result.isCall()", result.isCall())
        }
    }

并在此处调用此方法

   func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
        guard type == .voIP else { return }
        debugPrint("VOIPmanager didReceiveIncomingPushWith")
        DispatchQueue.global(qos: .default).async {
            SinchManager.default.relayRemotePushNotification(payload.dictionaryPayload)
        }
    }

Sinch SDK download 的 Sinch iOS 下载包中,有一个参考实现(Sinch CallKit 示例应用程序)显示了通过 CallKit 框架处理来电的大多数用例。请看那里(尽管示例应用程序仍在 Objc 中)。如果您在 Sinch 门户上为您的应用程序上传了有效的 VOIP 推送证书,那么安装该应用程序并在您的 iPhone.

上使用它应该相当容易

下面是 CallKit 示例应用程序的演示视频,用于在前台、后台和锁屏模式下处理呼叫。

Sinch CallKit App Demo Video