Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/C/Threema/Threema/Core/ThreemaCalls/   (Social Medium ©)  Datei vom 26.9.2026 mit Größe 16 kB image not shown  

Quelle  VoIPCallStateManager.swift   Sprache: JAVA

 

import CallKit
import CocoaLumberjackSwift
import Collections
import Foundation
import ThreemaFramework
import ThreemaMacros
import UIKit
import UserNotifications

final class VoIPCallStateManager: NSObject {

    @objc static let shared = VoIPCallStateManager()

    private let callKitManager: VoIPCallKitManager

    /// Contains `VoIPCallMessage` and `VoIPCallUserAction`
    private var callQueue = Deque<VoIPCallIDProtocol>()
    private let callQueueQueue = DispatchQueue(label: "ch.threema.VoIPCallStateManager.callQueueQueue")
    private let callServiceQueue = DispatchQueue(label: "ch.threema.VoIPCallStateManager.callServiceQueue")

    private let endedDelay = 5.0

    private var callService: VoIPCallService? {
        didSet {
            if let callService {
                DDLogNotice("[Call:1to1:StateManager] [cid=\(callService.callID)]: Created new call service")
            }
            else if let oldValue {
                DDLogNotice("[Call:1to1:StateManager] [cid=\(oldValue.callID)]: Removed call service")
            }
        }
    }

    var onCallFinished: (() -> Void)?
    var onCallAnswered: ((String) -> Void)?
    var onCallConnected: (() -> Void)?
    var onCallViewHidden: (() -> Void)?

    /// Provides the view controller to present the 1-1 call view on (the new coordinator flow's
    /// `CallWindow` root). `nil` (legacy flow) falls back to the key window's root.
    var callViewPresenterProvider: (() -> UIViewController?)?

    // MARK: - Lifecycle

    override required init() {
        self.callKitManager = VoIPCallKitManager()
        super.init()
    }

    // MARK: - Public functions

    /// Get the current state of a call
    /// - Returns: CallState
    @objc func currentCallState() -> CallState {
        callService?.currentState() ?? .idle
    }

    func isCallServiceInitialized() -> Bool {
        callServiceQueue.sync {
            callService != nil
        }
    }

    /// Present the CallViewController
    func presentCallViewController() {
        callService?.presentCallViewController()
    }

    // MARK: - Local user actions

    /// Add a user action to the process queue
    /// - parameter action: VoIPCallUserAction
    func addActionToCallQueue(_ action: VoIPCallUserAction) {
        addElementToCallQueue(action)
    }
    
    // MARK: - Local call start
    
    /// Starts a 1:1 with the given identity. Assumes callee supports 1:1 calls.
    /// - Parameters:
    ///   - callee: Identity of the callee
    ///   - onCompletion: Completion handler
    func startCall(callee: String, onCompletion: (() -> Void)? = nil) {
        let callID = VoIPCallID.generate()
        let action = VoIPCallUserAction(
            action: .call,
            contactIdentity: callee,
            callID: callID,
            completion: onCompletion
        )
        DDLogNotice(
            "[Call:1to1:StateManager] [cid=\(callID)]: Handle new call with \(callee), we are the caller"
        )
        
        DDLogNotice(
            "[Call:1to1:StateManager] [cid=\(callID)]: Enqueuing start call action"
        )
        
        addElementToCallQueue(action)
    }

    // MARK: - Call message handling

    /// Add a incoming call offer to the process queue
    /// - parameter offer: VoIPCallOfferMessage
    /// - parameter identity: Identity from the offer
    /// - parameter completion: Completion block
    func incomingCallOffer(offer: VoIPCallOfferMessage, identity: String, completion: (() -> Void)?) {
        DDLogNotice(
            "[Call:1to1:StateManager] [cid=\(offer.callID)]: Handle new call with \(identity), we are the callee"
        )

        DDLogNotice(
            "[Call:1to1:StateManager] [cid=\(offer.callID)]: Call offer received from \(identity)"
        )

        BackgroundTaskManager.shared.newBackgroundTask(
            key: kAppVoIPIncomCallBackgroundTask,
            timeout: Int(kAppVoIPIncomingCallBackgroundTaskTime)
        ) {
            offer.completion = completion
            offer.contactIdentity = identity
            DDLogNotice("[Call:1to1:StateManager] [cid=\(offer.callID)]: Enqueuing call offer message")
            self.addElementToCallQueue(offer)
        }
    }

    /// Add a incoming call answer to the process queue
    /// - parameter answer: VoIPCallAnswerMessage
    /// - parameter identity: Identity from the answer
    /// - parameter completion: Completion block
    func incomingCallAnswer(
        answer: VoIPCallAnswerMessage,
        identity: String,
        completion: @escaping () -> Void
    ) {
        DDLogNotice(
            "[Call:1to1:StateManager] [cid=\(answer.callID)]: Call answer received from \(identity): \(answer.description())"
        )

        BackgroundTaskManager.shared.newBackgroundTask(
            key: kAppVoIPBackgroundTask,
            timeout: Int(kAppPushBackgroundTaskTime)
        ) {
            answer.completion = completion
            answer.contactIdentity = identity
            DDLogNotice("[Call:1to1:StateManager] [cid=\(answer.callID)]: Enqueuing call answer message")
            self.addElementToCallQueue(answer)
        }
    }

    /// Add a incoming ringing message to the process queue
    /// - parameter ringing: VoIPCallRingingMessage
    func incomingCallRinging(ringing: VoIPCallRingingMessage) {
        DDLogNotice(
            "[Call:1to1:StateManager] [cid=\(ringing.callID)]: Call ringing message received from \(ringing.contactIdentity ?? "?")"
        )

        BackgroundTaskManager.shared.newBackgroundTask(
            key: kAppVoIPBackgroundTask,
            timeout: Int(kAppPushBackgroundTaskTime)
        ) {
            DDLogNotice("[Call:1to1:StateManager] [cid=\(ringing.callID)]: Enqueuing call ringing message")
            self.addElementToCallQueue(ringing)
        }
    }

    /// Add a incoming ice candidates message to the process queue
    /// - parameter candidates: VoIPCallIceCandidatesMessage
    /// - parameter identity: Identity from the ice candidates
    /// - parameter completion: Completion block
    func incomingIceCandidates(
        candidates: VoIPCallIceCandidatesMessage,
        identity: String,
        completion: (() -> Void)?
    ) {
        DDLogNotice(
            "[Call:1to1:StateManager] [cid=\(candidates.callID)]: Call ICE candidate message received from \(identity) (\(candidates.candidates.count) candidates)"
        )

        BackgroundTaskManager.shared.newBackgroundTask(
            key: kAppVoIPBackgroundTask,
            timeout: Int(kAppPushBackgroundTaskTime)
        ) {
            candidates.completion = completion
            candidates.contactIdentity = identity
            DDLogNotice("[Call:1to1:StateManager] [cid=\(candidates.callID)]: Enqueuing ICE candidate message")
            self.addElementToCallQueue(candidates)
        }
    }

    /// Add a incoming hangup message to the process queue
    /// - parameter hangup: VoIPCallHangupMessage
    func incomingCallHangup(hangup: VoIPCallHangupMessage) {
        DDLogNotice(
            "[Call:1to1:StateManager] [cid=\(hangup.callID)]: Call hangup message received from \(hangup.contactIdentity ?? "?")"
        )

        BackgroundTaskManager.shared.newBackgroundTask(
            key: kAppVoIPBackgroundTask,
            timeout: Int(kAppPushBackgroundTaskTime)
        ) {
            DDLogNotice("[Call:1to1:StateManager] [cid=\(hangup.callID)]: Enqueuing hangup message")
            self.addElementToCallQueue(hangup)
        }
    }

    // MARK: - CallKit

    /// Incoming from background
    /// - Parameters:
    ///   - callID: Unique call ID
    ///   - callPartnerIdentity: Caller identity
    ///   - callPartnerName: Caller name
    ///   - ringtoneSound: Filename of ringtone to be used
    ///   - fromPush: Whether we report from the NSE
    ///   - completion: Completion handler returns a task to update call
    func newIncomingCallFromBackground(
        with callID: VoIPCallID,
        callPartnerIdentity: String,
        callPartnerName: String?,
        ringtoneSound: String,
        fromPush: Bool = false,
        completion: @escaping (@escaping () -> Void) -> Void
    ) {
        callKitManager.reportIncomingCall(
            with: callID,
            callPartnerIdentity: callPartnerIdentity,
            callPartnerName: callPartnerName,
            ringtoneSound: ringtoneSound,
            fromPush: fromPush
        ) { _ in
            // Task to update call when business is ready
            let task: (() -> Void) = {
                Task { @MainActor in
                    PersistenceManager(
                        appGroupID: AppGroup.groupID(),
                        userDefaults: AppGroup.userDefaults(),
                        remoteSecretManager: RemoteSecretProvider.remoteSecretManager
                    ).dirtyObjectManager.refreshDirtyObjects(reset: true)

                    if ServerConnector.shared().connectionState == .disconnected {
                        ServerConnector.shared().isAppInBackground = SceneDelegate.isAppInBackground
                        ServerConnector.shared().connectWait(initiator: .threemaCall)
                    }

                    self.callServiceQueue.async {
                        // If another call already running (1-to-1 or group), reject call
                        if let callService = self.callService, callService.callID.callID != callID.callID
                            || NavigationBarPromptHandler.isGroupCallActive {
                            self.callKitManager.endCall(with: callID)
                            
                            // We are in a different call already. Other devices might be able to pickup, so we do not
                            // send the reject.
                            let action = VoIPCallUserAction(
                                action: .reject(sendMessage: false),
                                contactIdentity: callPartnerIdentity,
                                callID: callID,
                                completion: { }
                            )

                            BackgroundTaskManager.shared.newBackgroundTask(
                                key: kAppVoIPBackgroundTask,
                                timeout: Int(kAppPushBackgroundTaskTime)
                            ) {
                                DDLogNotice(
                                    "[Call:1to1:StateManager] [cid=\(callID)]: Another call is active. Enqueuing reject message"
                                )
                                self.addElementToCallQueue(action)
                            }
                        }
                    }
                }
            }

            completion(task)
        }
    }

    func ringingCallHungUpFromBackground(
        with callID: VoIPCallID,
        callPartnerIdentity: String,
        callPartnerName: String?,
        completion: @escaping (@escaping () -> Void) -> Void
    ) {
        // Not on `callServiceQueue`, the PushKit callback must report without blocking. Any state, not only
        // ringing: the call might have been answered since the extension checked, it must end anyway.
        let isCurrentCall = callService?.callID.callID == callID.callID

        // Every VoIP push must report a call. Re-report the current one, cancel it for any other call.
        guard isCurrentCall else {
            DDLogNotice(
                "[Call:1to1:StateManager] [cid=\(callID)]: Hangup for a call that is not the current one, reporting and cancelling"
            )
            callKitManager.startAndCancelCall(from: TargetManager.appName) {
                completion { }
            }
            return
        }

        callKitManager.reportIncomingCall(
            with: callID,
            callPartnerIdentity: callPartnerIdentity,
            callPartnerName: callPartnerName,
            ringtoneSound: "default",
            fromPush: true
        ) { _ in
            completion {
                // Already acknowledged by the extension
                let hangup = VoIPCallHangupMessage(
                    contactIdentity: callPartnerIdentity,
                    callID: callID,
                    duration: nil,
                    isOutgoing: false
                )
                self.incomingCallHangup(hangup: hangup)
            }
        }
    }

    /// Start and cancel call (for deprecated/invalid VoIP pushes) over CallKit, because for any VoIP received,
    /// `CXProvider.reportNewIncomingCall` must be called
    func startAndCancelCall(from localizedName: String, completion: @escaping () -> Void) {
        callKitManager.startAndCancelCall(from: localizedName, completion: completion)
    }

    // MARK: - State machine management

    /// Add a message to the process queue and start process
    /// - parameter element: `VoIPCallUserAction` or `VoIPCallMessageProtocol`
    private func addElementToCallQueue(_ element: VoIPCallIDProtocol) {

        guard element is VoIPCallUserAction || element is VoIPCallMessageProtocol else {
            DDLogError(
                "[Call:StateManager] Tried to add element that is not VoIPCallUserAction or VoIPCallMessageProtocol to callQueue: \(element)"
            )
            assertionFailure()
            return
        }

        let callQueueCount = callQueueQueue.sync {
            callQueue.append(element)
            return callQueue.count
        }
        if callQueueCount == 1 {
            processNextElementInCallQueue()
        }
    }

    private func processNextElementInCallQueue() {
        let element = callQueueQueue.sync {
            callQueue.popFirst()
        }
        
        guard let element,
              let contactIdentity = element.contactIdentity
        else {
            return
        }
        
        callServiceQueue.async {
            if self.callService == nil {
                DDLogNotice("[Call:1to1:StateManager] [cid=\(element.callID)]: Initialize VoIPCallService")
                self.callService = VoIPCallService(
                    callPartnerIdentity: contactIdentity,
                    callID: element.callID,
                    delegate: self,
                    callKitManager: self.callKitManager
                )
            }
            self.callService?.processCallQueueElement(element)
        }
    }

    private func resetCallService() {
        callServiceQueue.async {
            self.callService = nil
        }
    }

    /// Release the connection that was claimed for the call
    ///
    /// Delayed, because after a disconnect no further (VoIP) messages of this call are processed. A new call claims
    /// the connection before its state leaves `idle` (it waits for the connection first), thus the service and not
    /// the state decides whether the claim is still needed.
    private func releaseConnectionAfterDelay() {
        DispatchQueue.main.asyncAfter(deadline: .now() + endedDelay) { [weak self] in
            guard let self, callService == nil else {
                return
            }

            ServerConnector.shared().disconnect(initiator: .threemaCall)
        }
    }
}

// MARK: - VoIPCallServiceDelegate

extension VoIPCallStateManager: VoIPCallServiceDelegate {
    func prependCallQueueElement(_ element: VoIPCallIDProtocol) {
        callQueueQueue.sync {
            callQueue.prepend(element)
        }
    }

    func finishedProcessingCallQueueElement() {
        processNextElementInCallQueue()
    }

    func callFinished() {
        DDLogNotice("[Call:StateManager] Call finished, resetting service")
        // `onCallFinished` drives UI (`CallCoordinator.finish`), must run on main.
        // Must be async: callers may originate from the main thread via
        // LocalNetworkPermissionChecker → WebRTC, so DispatchQueue.main.sync would deadlock.
        DispatchQueue.main.async {
            self.onCallFinished?()
        }
        resetCallService()
        releaseConnectionAfterDelay()
        processNextElementInCallQueue()
    }

    func callReadyForPresentation(callPartnerIdentity: String) {
        // Invoked from the WebRTC signaling thread (SetRemoteDescription completion). `onCallAnswered`
        // builds the call UI (`CallWindow`/`UIWindow`), which must run on main — else NSInternalInconsistency.
        // Must be async: the call chain originates on the main thread (LocalNetworkPermissionChecker
        // dispatches its completion to DispatchQueue.main), so DispatchQueue.main.sync would deadlock.
        DispatchQueue.main.async {
            self.onCallAnswered?(callPartnerIdentity)
        }
    }

    func callDidConnect() {
        // `onCallConnected` drives UI (call activity), must run on main.
        // Must be async: callers may originate from the main thread via
        // LocalNetworkPermissionChecker → WebRTC, so DispatchQueue.main.sync would deadlock.
        DispatchQueue.main.async {
            self.onCallConnected?()
        }
    }

    func callViewPresentingViewController() -> UIViewController? {
        MainActor.assumeIsolated {
            self.callViewPresenterProvider?()
        }
    }
}

Messung
CHZG
869110092

¤ Dauer der Verarbeitung: 0.2 Sekunden  (vorverarbeitet am  2026-11-04) ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

PVS Prover

Isabelle Prover


Cephes Mathematical Library

Vienna Development Method

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.