Eine aufbereitete Darstellung der Quelle

 
     
 
 
Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 

Benutzer

Quelle  WCSessionManager.swift   Sprache: JAVA

 

import CocoaLumberjackSwift
import FileUtility
import Foundation
import ThreemaEssentials
import ThreemaFramework
import ThreemaMacros

final class WCSessionManager: NSObject {
    
    static let shared = WCSessionManager()
    
    private let businessInjector: BusinessInjectorProtocol

    private var sessions = [Data: WCSession]()
    private(set) var running = [Data]()
    private var runningSessionsQueue = DispatchQueue(label: "ch.threema.runningSessionsQueue", attributes: [])
    private var observerAlreadySet = false
    
    override private init() {
        self.businessInjector = BusinessInjector.ui
        super.init()
        
        // Remove legacy files
        let fileUtility = FileUtility.shared!
        try? fileUtility.delete(at: allSessionsURL())
        try? fileUtility.delete(at: runningSessionsURL())
    }
    
    final class func isWebHostAllowed(scannedHostName: String, whiteList: String) -> Bool {
        if whiteList.isEmpty {
            return false
        }
        let arr = whiteList.components(separatedBy: ",")
        for host in arr {
            let pattern = host.trimmingCharacters(in: .whitespaces)
            if pattern == scannedHostName {
                return true
            }
            let slicedPattern = String(pattern.dropFirst())
            if pattern.hasPrefix("*"), scannedHostName.hasSuffix(slicedPattern) {
                return true
            }
        }
        return false
    }
}

// MARK: Private functions

extension WCSessionManager {
    
    /// Add observers
    /// * batteryLevelDidChange
    /// * batteryStateDidChange
    /// * profileNicknameChanged
    /// * profilePictureChanged
    /// * blackListChanged
    private func addObservers() {
        if observerAlreadySet == false {
            UIDevice.current.isBatteryMonitoringEnabled = true
            NotificationCenter.default.addObserver(
                self,
                selector: #selector(batteryLevelDidChange),
                name: UIDevice.batteryLevelDidChangeNotification,
                object: nil
            )
            NotificationCenter.default.addObserver(
                self,
                selector: #selector(batteryStateDidChange),
                name: UIDevice.batteryStateDidChangeNotification,
                object: nil
            )
            NotificationCenter.default.addObserver(
                self,
                selector: #selector(profileNicknameChanged),
                name: NSNotification.Name(rawValue: kNotificationProfileNicknameChanged),
                object: nil
            )
            NotificationCenter.default.addObserver(
                self,
                selector: #selector(profilePictureChanged),
                name: NSNotification.Name(rawValue: kNotificationProfilePictureChanged),
                object: nil
            )
            NotificationCenter.default.addObserver(
                self,
                selector: #selector(blackListChanged),
                name: NSNotification.Name(rawValue: kNotificationBlockedContact),
                object: nil
            )
            NotificationCenter.default.addObserver(
                self,
                selector: #selector(managedObjectContextDidChange),
                name: .NSManagedObjectContextObjectsDidChange,
                object: nil
            )
            NotificationCenter.default.addObserver(
                self,
                selector: #selector(changedManagedObjects),
                name: DatabaseContext.changedManagedObjects,
                object: nil
            )
            observerAlreadySet = true
            NavigationBarPromptHandler.isWebActive = true
        }
        DispatchQueue.main.async {
            UIApplication.shared.isIdleTimerDisabled = true
        }
    }
    
    @MainActor
    private func removeObservers() {
        UIDevice.current.isBatteryMonitoringEnabled = false
        NotificationCenter.default.removeObserver(self)
        
        observerAlreadySet = false
        NavigationBarPromptHandler.isWebActive = false

        guard
            NavigationBarPromptHandler.isCallActiveInBackground
            || VoIPCallStateManager.shared.currentCallState() == .idle,
            !NavigationBarPromptHandler.isGroupCallActive
        else {
            return
        }

        Task { @MainActor in
            let mainTabBar = SharedAppProvider.tabBarController
            
            guard
                let mainTabBar,
                let viewControllers = mainTabBar.viewControllers
            else {
                UIApplication.shared.isIdleTimerDisabled = false
                return
            }

            guard viewControllers.count > kChatTabBarIndex else {
                UIApplication.shared.isIdleTimerDisabled = false
                return
            }

            guard
                let chatNavVc = viewControllers[Int(kChatTabBarIndex)] as? UINavigationController,
                let curChatVc = chatNavVc.topViewController as? ChatViewController
            else {
                UIApplication.shared.isIdleTimerDisabled = false
                return
            }

            if !curChatVc.isRecording(),
               !curChatVc.isPlayingAudioMessage() {
                UIApplication.shared.isIdleTimerDisabled = false
            }
        }
    }
    
    private func allSessionsURL() -> URL {
        let documentDir = FileUtility.shared.appDataDirectory(appGroupID: AppGroup.groupID())
        return documentDir!.appendingPathComponent("AllWCSessions")
    }
    
    private func runningSessionsURL() -> URL {
        let documentDir = FileUtility.shared.appDataDirectory(appGroupID: AppGroup.groupID())
        return documentDir!.appendingPathComponent("RunningWCSessions")
    }
}

// MARK: Public functions

extension WCSessionManager {
    
    /// Connect a old or new session. Search for correct session or create a new one
    func connect(authToken: Data?, wca: String?, publicKeyHash: String) {
        canConnectToWebClient { isValid in
            guard isValid == true else {
                return
            }

            guard let webClientSession = WebClientSessionStore.shared.webClientSessionForHash(publicKeyHash) else {
                // session not found
                return
            }

            if webClientSession.isConnecting {
                DDLogError("[Threema Web] WebClientSession try already to connect")
                return
            }
            webClientSession.isConnecting = true

            let (publicKey, saltyRTCHost): (Data, String) =
                self.businessInjector.entityManager.performAndWait {
                    (
                        webClientSession.initiatorPermanentPublicKey,
                        webClientSession.saltyRTCHost
                    )
                }

            var session: WCSession? = self.sessions[publicKey]

            if TargetManager.isBusinessApp {
                let mdmSetup = MDMSetup()!
                if let webHosts = mdmSetup.webHosts() {
                    if WCSessionManager.isWebHostAllowed(
                        scannedHostName: saltyRTCHost,
                        whiteList: webHosts
                    ) == false {
                        DDLogNotice("[Threema Web] Scanned qr code host is not white listed")
                        if SharedAppProvider.isAppInBackground {
                            ThreemaUtilityObjC.sendErrorLocalNotification(
                                #localize("webClient_scan_error_mdm_host_title"),
                                body: #localize("webClient_scan_error_mdm_host_message"),
                                userInfo: nil
                            )
                        }
                        else {
                            // This path can run off the main thread (Core Data work). Present on main asynchronously
                            // instead of blocking the main thread to read the root view controller.
                            Task { @MainActor in
                                if let rootVC = SharedAppProvider.keyWindow?.rootViewController {
                                    UIAlertTemplate.showOKAlert(
                                        owner: rootVC,
                                        title: #localize("webClient_scan_error_mdm_host_title"),
                                        message: #localize("webClient_scan_error_mdm_host_message")
                                    )
                                }
                            }
                        }
                        webClientSession.isConnecting = false
                        return
                    }
                }
            }

            if let session,
               let wca,
               let connectionWca = session.connectionWca(),
               wca.elementsEqual(connectionWca) {
                // same wca, ignore this request
                DDLogNotice("[Threema Web] Ignore connect, because it's the same wca")
                webClientSession.isConnecting = false
                return
            }

            if session == nil {
                session = WCSession(webClientSession: webClientSession)
                self.sessions[publicKey] = session
            }
            if wca != nil {
                session!.setWcaForConnection(wca: wca!)
            }
            self.addWCSessionToRunning(webClientSession: webClientSession)
            self.addObservers()
            session!.connect(authToken: authToken)
        }
    }
    
    func connect(authToken: Data?, wca: String?, webClientSession: WebClientSessionEntity) {
        // Pre-read Core Data properties before any async work
        let (publicKey, saltyRTCHost, saltyRTCPort, selfHosted, permanent, version):
            (Data, String, String, String, String, String?) = businessInjector.entityManager.performAndWait {
                (
                    webClientSession.initiatorPermanentPublicKey,
                    webClientSession.saltyRTCHost,
                    webClientSession.saltyRTCPort.stringValue,
                    webClientSession.selfHosted.stringValue,
                    webClientSession.permanent.stringValue,
                    webClientSession.version?.stringValue
                )
            }

        DDLogNotice(
            "[Threema Web] Connect to host \(saltyRTCHost); port: \(saltyRTCPort) selfHosted: \(selfHosted); permanent: \(permanent); version: \(version ?? "?")"
        )

        if webClientSession.isConnecting {
            DDLogError("[Threema Web] WebClientSession try already to connect")
            return
        }
        webClientSession.isConnecting = true

        canConnectToWebClient(completionHandler: { isValid in
            guard isValid == true else {
                webClientSession.isConnecting = false
                return
            }

            var session: WCSession? = self.sessions[publicKey]

            if let session,
               let wca,
               let connectionWca = session.connectionWca(),
               wca.elementsEqual(connectionWca) {
                // same wca, ignore this request
                DDLogNotice("[Threema Web] Ignore connect, because it's the same wca")
                webClientSession.isConnecting = false
                return
            }

            if session == nil {
                session = WCSession(webClientSession: webClientSession)
                self.sessions[publicKey] = session
            }

            if TargetManager.isBusinessApp {
                let mdmSetup = MDMSetup()!
                if let webHosts = mdmSetup.webHosts() {
                    if WCSessionManager.isWebHostAllowed(
                        scannedHostName: saltyRTCHost,
                        whiteList: webHosts
                    ) == false {
                        DDLogError("[Threema Web] Scanned qr code host is not white listed")
                        if SharedAppProvider.isAppInBackground {
                            ThreemaUtilityObjC.sendErrorLocalNotification(
                                #localize("webClient_scan_error_mdm_host_title"),
                                body: #localize("webClient_scan_error_mdm_host_message"),
                                userInfo: nil
                            )
                        }
                        else {
                            // This path can run off the main thread (Core Data work). Present on main asynchronously
                            // instead of blocking the main thread to read the root view controller.
                            Task { @MainActor in
                                if let rootVC = SharedAppProvider.keyWindow?.rootViewController {
                                    UIAlertTemplate.showOKAlert(
                                        owner: rootVC,
                                        title: #localize("webClient_scan_error_mdm_host_title"),
                                        message: #localize("webClient_scan_error_mdm_host_message")
                                    )
                                }
                            }
                        }
                        webClientSession.isConnecting = false
                        return
                    }
                }
            }

            if let wca {
                session!.setWcaForConnection(wca: wca)
            }
            self.addWCSessionToRunning(webClientSession: webClientSession)
            self.addObservers()
            session!.connect(authToken: authToken)
        })
    }
    
    func connectAllRunningSessions() {
        if running.isEmpty {
            DDLogNotice("[Threema Web] There is no active session")
            WebClientSessionStore.shared.setAllWebClientSessionsInactive()

            Task {
                await removeObservers()
            }
            return
        }
        canConnectToWebClient { isValid in
            guard isValid == true else {
                return
            }

            DDLogNotice("[Threema Web] Connect active sessions (\(self.running.count))")
            for publicKey in self.running {
                guard let session = self.sessions[publicKey] else {
                    continue
                }
                
                if session.connectionStatus() == .disconnected {
                    self.addObservers()
                    DDLogNotice("[Threema Web] Connect active session")
                    session.connect(authToken: nil)
                }
                else {
                    if let connectionStatus = session.connectionStatus() {
                        DDLogError(
                            "[Threema Web] Can't connect active session, wrong state \(connectionStatus)"
                        )
                    }
                    else {
                        DDLogError("[Threema Web] Can't connect active session, connectionStatus is nil!")
                        self.removeWCSessionFromRunning(session)
                    }
                }
            }
        }
    }
    
    /// Stop all running sessions.
    /// Clears the list of running sessions
    func stopAllSessions() {
        // disconnect all active sessions and set all sessions on core data to inactive
        DDLogNotice("[Threema Web] Stop all active sessions")
        for publicKey in running {
            guard let session = self.sessions[publicKey] else {
                continue
            }
            
            session.stop(close: true, forget: false, sendDisconnect: true, reason: .stop)
        }
        Task {
            await removeObservers()
        }
    }
    
    /// Stop and delete all running sessions.
    /// Clears the list of running sessions
    func stopAndForgetAllSessions() {
        // disconnect all active sessions and set all sessions on core data to inactive
        DDLogNotice("[Threema Web] Stop and forget all active sessions")
        WebClientSessionStore.shared.setAllWebClientSessionsInactive()
        for publicKey in running {
            guard let session = self.sessions[publicKey] else {
                continue
            }
            
            session.stop(close: true, forget: true, sendDisconnect: true, reason: .stop)
        }
    }
    
    /// Stop specific session.
    func stopSession(_ webClientSession: WebClientSessionEntity) {
        DDLogNotice("[Threema Web] Stop session")
        let publicKey: Data = businessInjector.entityManager.performAndWait {
            webClientSession.initiatorPermanentPublicKey
        }
        
        guard let session: WCSession = sessions[publicKey] else {
            return
        }
        
        session.stop(close: true, forget: false, sendDisconnect: true, reason: .stop)
    }
    
    /// Stop and delete specific session.
    func stopAndDeleteSession(_ webClientSession: WebClientSessionEntity) {
        DDLogNotice("[Threema Web] Stop and delete all active sessions")
        let publicKey: Data = businessInjector.entityManager.performAndWait {
            webClientSession.initiatorPermanentPublicKey
        }
        
        defer {
            WebClientSessionStore.shared.deleteWebClientSession(webClientSession)
        }
        
        guard let session: WCSession = sessions[publicKey] else {
            return
        }
        
        session.stop(close: true, forget: true, sendDisconnect: true, reason: .delete)
        sessions.removeValue(forKey: publicKey)
    }

    /// Remove WebClientSession from running list.
    func removeWCSessionFromRunning(_ session: WCSession) {
        let publicKey: Data? = businessInjector.entityManager.performAndWait {
            session.webClientSession?.initiatorPermanentPublicKey
        }
        if let publicKey {
            runningSessionsQueue.sync {
                if let index = running.firstIndex(of: publicKey) {
                    running.remove(at: index)
                }
                WebClientSessionStore.shared.updateWebClientSession(session: session.webClientSession!, active: false)
            }
        }
        if running.isEmpty {
            Task {
                await removeObservers()
            }
        }
    }
    
    /// Remove all not permanent sessions.
    func removeAllNotPermanentSessions() {
        // Remove all not permanent sessions
        WebClientSessionStore.shared.removeAllNotPermanentSessions()
    }
    
    func isRunningWCSession() -> Bool {
        !running.isEmpty
    }
    
    func pauseAllRunningSessions() {
        DDLogNotice("[Threema Web] Pause all sessions")
        sendConnectionAckToAllActiveSessions()
        for publicKey in running {
            guard let session = sessions[publicKey] else {
                return
            }
            
            session.stop(close: false, forget: false, sendDisconnect: false, reason: .pause)
        }
    }
    
    func updateConversationPushSetting(conversation: ConversationEntity) {
        for publicKey in running {
            guard let session: WCSession = sessions[publicKey] else {
                return
            }
            
            let conversationResponse = WebConversationUpdate(
                conversation: conversation,
                objectMode: .modified,
                session: session
            )
            session.messageQueue.enqueue(
                data: conversationResponse.messagePack(),
                blackListed: false
            )
        }
    }
}

extension WCSessionManager {
    // MARK: Private functions
    
    private func addWCSessionToRunning(webClientSession: WebClientSessionEntity) {
        let publicKey: Data = businessInjector.entityManager.performAndWait {
            webClientSession.initiatorPermanentPublicKey
        }
        runningSessionsQueue.sync {
            guard !running.contains(publicKey) else {
                return
            }
            
            running.append(publicKey)
            WebClientSessionStore.shared.updateWebClientSession(
                session: webClientSession,
                active: true
            )
        }
    }
        
    private func canConnectToWebClient(completionHandler: @escaping ((_ isValid: Bool) -> Void)) {
        if UserSettings.shared().threemaWeb, !MDMSetup().disableWeb() {
            if LicenseStore.shared().isValid() == true {
                completionHandler(true)
            }
            else {
                LicenseStore.shared().performLicenseCheck(completion: { success in
                    if success == true {
                        completionHandler(true)
                    }
                    else {
                        DDLogWarn("[Threema Web] LicenseStore is invalid, stop all sessions")
                        self.stopAllSessions()
                        completionHandler(false)
                    }
                })
            }
        }
        else {
            DDLogWarn("Threema Web is not enabled, stop all sessions")
            stopAllSessions()
            completionHandler(false)
        }
    }
    
    private func sendConnectionAckToAllActiveSessions() {
        for publicKey in running {
            guard
                let session = sessions[publicKey],
                let context = session.connectionContext()
            else {
                continue
            }
            
            context.sendConnectionAck()
        }
    }
    
    private func sendMessagePackToAllActiveSessions(messagePack: Data, blackListed: Bool) {
        for publicKey in running {
            guard let session = sessions[publicKey] else {
                continue
            }
            
            session.sendMessageToWeb(blacklisted: blackListed, msgpack: messagePack)
        }
    }
    
    private func sendMessagePackToAllActiveSessions(
        with requestedConversationID: String,
        messagePack: Data,
        blackListed: Bool
    ) {
        for publicKey in running {
            guard
                let session = sessions[publicKey],
                session.requestedConversations(contains: requestedConversationID) == true
            else {
                continue
            }
            
            session.sendMessageToWeb(blacklisted: blackListed, msgpack: messagePack)
        }
    }

    private func sendMessagePackToRequestedSession(with requestID: String, messagePack: Data, blackListed: Bool) {
        for publicKey in running {
            guard
                let session = sessions[publicKey],
                session.requestMessage(for: requestID) != nil
            else {
                continue
            }
            
            session.sendMessageToWeb(blacklisted: blackListed, msgpack: messagePack)
        }
    }
    
    private func responseUpdateContact(contact: ContactEntity, objectMode: WebReceiverUpdate.ObjectMode) {
        let receiverUpdate = WebReceiverUpdate(updatedContact: contact, objectMode: objectMode)
        DDLogVerbose("[Threema Web] MessagePack -> Send update/receiver")
        sendMessagePackToAllActiveSessions(messagePack: receiverUpdate.messagePack(), blackListed: false)
    }
    
    private func responseUpdateAvatar(contact: ContactEntity?, group: Group?) {
        if contact != nil {
            let avatarUpdate = WebAvatarUpdate(contact: contact!)
            DDLogVerbose("[Threema Web] MessagePack -> Send update/avatar")
            sendMessagePackToAllActiveSessions(messagePack: avatarUpdate.messagePack(), blackListed: false)
        }
        else if group != nil {
            let avatarUpdate = WebAvatarUpdate(group: group!)
            DDLogVerbose("[Threema Web] MessagePack -> Send update/avatar")
            sendMessagePackToAllActiveSessions(messagePack: avatarUpdate.messagePack(), blackListed: false)
        }
    }
    
    private func responseUpdateMessage(
        with requestedConversationID: String,
        message: BaseMessageEntity,
        conversation: ConversationEntity,
        objectMode: WebMessagesUpdate.ObjectMode,
        exclude requestID: String
    ) {
        for publicKey in running {
            guard
                let session = sessions[publicKey],
                session.requestedConversations(contains: requestedConversationID) == true,
                session.requestMessage(for: requestID) == nil
            else {
                continue
            }
            
            sendResponseUpdateMessage(
                message: message,
                conversation: conversation,
                objectMode: objectMode,
                session: session
            )
        }
    }

    private func responseUpdateMessage(
        with requestedConversationID: String,
        message: BaseMessageEntity,
        conversation: ConversationEntity,
        objectMode: WebMessagesUpdate.ObjectMode
    ) {
        for publicKey in running {
            guard
                let session = sessions[publicKey],
                session.requestedConversations(contains: requestedConversationID) == true
            else {
                continue
            }
            
            sendResponseUpdateMessage(
                message: message,
                conversation: conversation,
                objectMode: objectMode,
                session: session
            )
        }
    }
    
    private func sendResponseUpdateMessage(
        message: BaseMessageEntity,
        conversation: ConversationEntity,
        objectMode: WebMessagesUpdate.ObjectMode,
        session: WCSession
    ) {
        let messageUpdate = WebMessagesUpdate(
            baseMessage: message,
            conversation: conversation,
            objectMode: objectMode,
            session: session
        )
        DDLogVerbose("[Threema Web] MessagePack -> Send update/messages")
        session.sendMessageToWeb(blacklisted: false, msgpack: messageUpdate.messagePack())
    }
    
    private func responseUpdateConversation(
        conversation: ConversationEntity,
        objectMode: WebConversationUpdate.ObjectMode
    ) {
        for publicKey in running {
            guard let session = sessions[publicKey] else {
                continue
            }
            
            let conversationResponse = WebConversationUpdate(
                conversation: conversation,
                objectMode: objectMode,
                session: session
            )
            DDLogVerbose("[Threema Web] MessagePack -> Send update/conversation")
            session.sendMessageToWeb(blacklisted: false, msgpack: conversationResponse.messagePack())
        }
    }
    
    private func responseUpdateGroup(group: Group, objectMode: WebReceiverUpdate.ObjectMode) {
        let receiverUpdate = WebReceiverUpdate(updatedGroup: group, objectMode: objectMode)
        DDLogVerbose("[Threema Web] MessagePack -> Send update/receiver")
        sendMessagePackToAllActiveSessions(messagePack: receiverUpdate.messagePack(), blackListed: false)
    }

    private func responseUpdateTyping(identity: ThreemaIdentity, isTyping: Bool) {
        let typingUpdate = WebTypingUpdate(identity: identity, typing: isTyping)
        DDLogVerbose("[Threema Web] MessagePack -> Send update/typing")
        sendMessagePackToAllActiveSessions(messagePack: typingUpdate.messagePack(), blackListed: true)
    }
    
    private func responseUpdateDeletedConversation(
        conversation: ConversationEntity,
        contact: ContactEntity?,
        objectMode: WebConversationUpdate.ObjectMode
    ) {
        let conversationResponse = WebConversationUpdate(
            conversation: conversation,
            contact: contact,
            objectMode: objectMode
        )
        DDLogVerbose("[Threema Web] MessagePack -> Send update/conversation")
        sendMessagePackToAllActiveSessions(messagePack: conversationResponse.messagePack(), blackListed: false)
    }

    private func webRequestMessage(for requestID: String) -> WebAbstractMessage? {
        for publicKey in running {
            guard
                let session = sessions[publicKey],
                let webAbstractMessage = session.requestMessage(for: requestID)
            else {
                continue
            }
            
            return webAbstractMessage
        }
        return nil
    }

    private func removeWebRequestMessage(with requestID: String) {
        for publicKey in running {
            guard
                let session = sessions[publicKey],
                session.requestMessage(for: requestID) != nil
            else {
                continue
            }
            
            session.removeRequestCreateMessage(requestID: requestID)
        }
    }
}

extension WCSessionManager {
    //     MARK: Database Observer
    
    @objc private func managedObjectContextDidChange(notification: NSNotification) {
        if let managedObjectContext = notification.object as? NSManagedObjectContext,
           managedObjectContext.concurrencyType == .mainQueueConcurrencyType {
            
            // MARK: inserted

            handleInsertedObjects(insertedObjects: managedObjectContext.insertedObjects)
            
            // MARK: update

            handleUpdatedObjects(updatedObjects: managedObjectContext.updatedObjects)
            
            // MARK: deleted

            handleDeletedObjects(deletedObjects: managedObjectContext.deletedObjects)
        }
        else {
            if let managedObjectContext = notification.object as? NSManagedObjectContext {
                for managedObject in managedObjectContext.insertedObjects {
                    switch managedObject {
                    case let managedObject as ContactEntity:
                        DDLogNotice("New contact added on wrong context \(managedObject.identity)")
                    case let managedObject as BaseMessageEntity:
                        DDLogNotice("New message added on wrong context \(managedObject.id.hexString)")
                    case let managedObject as ConversationEntity:
                        if managedObject.isGroup {
                            DDLogNotice(
                                "New conversation added on wrong context \(managedObject.groupID?.hexString ?? "?")"
                            )
                        }
                        else {
                            DDLogNotice(
                                "New conversation added on wrong context \(managedObject.contact?.identity ?? "?")"
                            )
                        }
                    default:
                        break
                    }
                }
                for managedObject in managedObjectContext.updatedObjects {
                    switch managedObject {
                    case let managedObject as ContactEntity:
                        DDLogNotice("Updated contact on wrong context \(managedObject.identity)")
                    case let managedObject as BaseMessageEntity:
                        DDLogNotice("Updated message on wrong context \(managedObject.id.hexString)")
                    case let managedObject as ConversationEntity:
                        if managedObject.isGroup {
                            DDLogNotice(
                                "Updated conversation on wrong context \(managedObject.groupID?.hexString ?? "?")"
                            )
                        }
                        else {
                            DDLogNotice(
                                "Updated conversation on wrong context \(managedObject.contact?.identity ?? "?")"
                            )
                        }
                    default:
                        break
                    }
                }
            }
        }
    }
    
    private func handleUpdatedObjects(updatedObjects: Set<NSManagedObject>, _ dirtyObjects: Bool = false) {
        for managedObject in updatedObjects {
            switch managedObject {
            case let managedObject as ContactEntity:
                updateContact(managedObject, dirtyObjects: dirtyObjects)
            case let managedObject as BaseMessageEntity:
                // This is a workaround for an issue that was introduced with IOS-3233 / IOS-3212
                // With the new changes we do not insert new file messages immediately but wait until they are properly
                // decoded (we check if the mimeType has changed/been set) and only then insert them into the web
                // client.
                // These changes caused a crash when using web client because we force unwrap `fileName`, `fileSize` and
                // `mimeType` when creating a `WebFile` struct.
                // As we're sunsetting the web client we just avoid the crash (the message will be updated later anyways
                // so the user impact is low) instead of fixing it properly.
                if let fileMessageEntity = managedObject as? FileMessageEntity,
                   fileMessageEntity.changedValues().keys.contains("mimeType") {
                    insertBaseMessage(managedObject)
                    continue
                }
                updateBaseMessage(managedObject, dirtyObjects: dirtyObjects)
            case let managedObject as ConversationEntity:
                updateConversation(managedObject, dirtyObjects: dirtyObjects)
            default:
                break
            }
        }
    }
    
    private func handleInsertedObjects(insertedObjects: Set<NSManagedObject>) {
        for managedObject in insertedObjects {
            switch managedObject {
            case let managedObject as ContactEntity:
                insertContact(managedObject)
            case let managedObject as BaseMessageEntity:
                if let fileMessageEntity = managedObject as? FileMessageEntity {
                    guard fileMessageEntity.fileName != nil, fileMessageEntity.fileSize != nil,
                          fileMessageEntity.mimeType != nil else {
                        continue
                    }
                }
                
                insertBaseMessage(managedObject)
            case let managedObject as ConversationEntity:
                insertConversation(managedObject)
            default:
                break
            }
        }
    }
    
    private func handleDeletedObjects(deletedObjects: Set<NSManagedObject>) {
        for managedObject in deletedObjects {
            switch managedObject {
            case let managedObject as ContactEntity:
                deleteContact(managedObject)
            case let managedObject as BaseMessageEntity:
                deleteBaseMessage(managedObject)
            case let managedObject as ConversationEntity:
                deleteConversation(managedObject)
            default:
                break
            }
        }
    }
        
    private func updateContact(_ contact: ContactEntity, dirtyObjects: Bool = false) {
        let changedValues = contact.changedValues()
        
        let backgroundKey = kAppAckBackgroundTask + SwiftUtils.pseudoRandomString(length: 10)
        BackgroundTaskManager.shared.newBackgroundTask(
            key: backgroundKey,
            timeout: Int(kAppCoreDataProcessMessageBackgroundTaskTime)
        ) {
            guard let currentContact = self.businessInjector.entityManager.entityFetcher
                .managedObject(with: contact.objectID) as? ContactEntity else {
                BackgroundTaskManager.shared.cancelBackgroundTask(key: backgroundKey)
                return
            }
            DDLogNotice("Updated contact on main context \(currentContact.identity)")
            
            if changedValues.keys.contains("firstName") ||
                changedValues.keys.contains("lastName") ||
                changedValues.keys.contains("displayName") ||
                changedValues.keys.contains("publicNickname") ||
                changedValues.keys.contains("verificationLevel") ||
                changedValues.keys.contains("state") ||
                changedValues.keys.contains("featureMask") ||
                changedValues.keys.contains("workContact") ||
                changedValues.keys.contains("hidden") ||
                dirtyObjects {
                let objectMode: WebReceiverUpdate.ObjectMode = .modified
                self.responseUpdateContact(contact: currentContact, objectMode: objectMode)
            }
            
            if changedValues.keys.contains("contactImage") || changedValues.keys.contains("imageData") {
                self.responseUpdateAvatar(contact: currentContact, group: nil)
            }
            
            BackgroundTaskManager.shared.cancelBackgroundTask(key: backgroundKey)
        }
    }
    
    private func updateBaseMessage(_ baseMessage: BaseMessageEntity, dirtyObjects: Bool = false) {
        let changedValues = baseMessage.changedValuesForCurrentEvent()
        
        let backgroundKey = kAppAckBackgroundTask + SwiftUtils.pseudoRandomString(length: 10)
        BackgroundTaskManager.shared.newBackgroundTask(
            key: backgroundKey,
            timeout: Int(kAppCoreDataProcessMessageBackgroundTaskTime)
        ) {
            guard let currentMessage = self.businessInjector.entityManager.entityFetcher.managedObject(
                with: baseMessage.objectID
            ) as? BaseMessageEntity else {
                BackgroundTaskManager.shared.cancelBackgroundTask(key: backgroundKey)
                return
            }
            
            DDLogNotice("Updated message on main context \(currentMessage.id.hexString)")
            let conversation = currentMessage.conversation
            let identity = conversation.isGroup
                ? conversation.groupID!.hexEncodedString()
                : self.baseMessageIdentity(baseMessage)
            self.processBaseMessageUpdate(baseMessage: currentMessage, changedValues: changedValues, identity: identity)
            
            if let lastMessage = conversation.lastMessage,
               lastMessage.id == currentMessage.id,
               self.shouldSendUpdate(changedValues: changedValues) || dirtyObjects {
                let objectMode: WebConversationUpdate.ObjectMode = .modified
                self.responseUpdateConversation(conversation: conversation, objectMode: objectMode)
                // background task to send ack to server
                let backgroundKey = kAppAckBackgroundTask + currentMessage.id.hexEncodedString()
                BackgroundTaskManager.shared.newBackgroundTask(
                    key: backgroundKey,
                    timeout: Int(kAppAckBackgroundTaskTime),
                    completionHandler: nil
                )
            }
            BackgroundTaskManager.shared.cancelBackgroundTask(key: backgroundKey)
        }
    }
    
    private func processBaseMessageUpdate(
        baseMessage: BaseMessageEntity,
        changedValues: [String: Any],
        identity: String
    ) {
        guard shouldSendUpdate(changedValues: changedValues) else {
            return
        }
        
        let objectMode: WebMessagesUpdate.ObjectMode = .modified
        responseUpdateMessage(
            with: identity,
            message: baseMessage,
            conversation: baseMessage.conversation,
            objectMode: objectMode
        )
        // background task to send ack to server
        let backgroundKey = kAppAckBackgroundTask + baseMessage.id.hexEncodedString()
        BackgroundTaskManager.shared.newBackgroundTask(
            key: backgroundKey,
            timeout: Int(kAppAckBackgroundTaskTime),
            completionHandler: nil
        )
    }
    
    private func shouldSendUpdate(changedValues: [String: Any]) -> Bool {
        if changedValues.count == 1 {
            let changedValueDict = changedValues.first
            if changedValueDict?.key == "progress" {
                return false
            }
        }
        else if changedValues.isEmpty {
            return false
        }
        return true
    }
        
    private func updateConversation(_ conversation: ConversationEntity, dirtyObjects: Bool = false) {
        let changedValues = conversation.changedValues()
        
        let changedValuesForCurrentEvent = conversation.changedValuesForCurrentEvent()
        let backgroundKey = kAppAckBackgroundTask + SwiftUtils.pseudoRandomString(length: 10)
        BackgroundTaskManager.shared.newBackgroundTask(
            key: backgroundKey,
            timeout: Int(kAppCoreDataProcessMessageBackgroundTaskTime)
        ) {
            guard let currentConversation = self.businessInjector.entityManager.entityFetcher.managedObject(
                with: conversation.objectID
            ) as? ConversationEntity else {
                BackgroundTaskManager.shared.cancelBackgroundTask(key: backgroundKey)
                return
            }
            
            DDLogNotice(
                "Updated conversation on main context \(currentConversation.isGroup ? currentConversation.groupID?.hexString ?? "?" : currentConversation.contact?.identity ?? "?")"
            )
                        
            if currentConversation.isGroup {
                
                if changedValues.keys.contains(ConversationEntity.Field.name(
                    for: .groupName,
                    encrypted: RemoteSecretProvider.remoteSecretManager.isRemoteSecretEnabled
                )) || changedValues.keys.contains(ConversationEntity.Field.name(
                    for: .members,
                    encrypted: RemoteSecretProvider.remoteSecretManager.isRemoteSecretEnabled
                )) {
                    if let group = self.businessInjector.groupManager.getGroup(conversation: currentConversation) {
                        self.responseUpdateGroup(group: group, objectMode: .modified)
                    }
                }
                else if changedValues.keys.contains(ConversationEntity.Field.name(
                    for: .groupImage,
                    encrypted: RemoteSecretProvider.remoteSecretManager.isRemoteSecretEnabled
                )),
                    let group = self.businessInjector.groupManager.getGroup(conversation: currentConversation) {
                    self.responseUpdateAvatar(contact: nil, group: group)
                }
            }
            else if let contact = currentConversation.contact,
                    changedValuesForCurrentEvent.keys.contains(ContactEntity.Field.name(
                        for: .typing,
                        encrypted: RemoteSecretProvider.remoteSecretManager.isRemoteSecretEnabled
                    )) {
                self.responseUpdateTyping(
                    identity: contact.threemaIdentity,
                    isTyping: currentConversation.typing.boolValue
                )
            }
            
            if (
                changedValues.keys.contains(ConversationEntity.Field.name(
                    for: .lastMessage,
                    encrypted: RemoteSecretProvider.remoteSecretManager.isRemoteSecretEnabled
                )) ||
                    changedValues.keys.contains(ConversationEntity.Field.name(
                        for: .visibility,
                        encrypted: RemoteSecretProvider.remoteSecretManager.isRemoteSecretEnabled
                    )) ||
                    changedValues.keys.contains(ConversationEntity.Field.name(
                        for: .unreadMessageCount,
                        encrypted: RemoteSecretProvider.remoteSecretManager.isRemoteSecretEnabled
                    )) ||
                    changedValues.keys.contains(ConversationEntity.Field.name(
                        for: .lastUpdate,
                        encrypted: RemoteSecretProvider.remoteSecretManager.isRemoteSecretEnabled
                    )) ||
                    dirtyObjects
            ) && currentConversation.lastMessage != nil {
                let objectMode: WebConversationUpdate.ObjectMode = .modified
                self.responseUpdateConversation(conversation: currentConversation, objectMode: objectMode)
            }
            else if !conversation.isGroup,
                    let contact = conversation.contact,
                    changedValues.keys.contains(ConversationEntity.Field.name(
                        for: .category,
                        encrypted: RemoteSecretProvider.remoteSecretManager.isRemoteSecretEnabled
                    )) || changedValues.keys.contains(ConversationEntity.Field.name(
                        for: .visibility,
                        encrypted: RemoteSecretProvider.remoteSecretManager.isRemoteSecretEnabled
                    )) {
                self.responseUpdateContact(contact: contact, objectMode: .modified)
            }
            BackgroundTaskManager.shared.cancelBackgroundTask(key: backgroundKey)
        }
    }
    
    private func insertContact(_ contact: ContactEntity) {
        let backgroundKey = kAppAckBackgroundTask + SwiftUtils.pseudoRandomString(length: 10)
        BackgroundTaskManager.shared.newBackgroundTask(
            key: backgroundKey,
            timeout: Int(kAppCoreDataProcessMessageBackgroundTaskTime)
        ) {
            guard let currentContact = self.businessInjector.entityManager.entityFetcher.managedObject(
                with: contact.objectID
            ) as? ContactEntity else {
                BackgroundTaskManager.shared.cancelBackgroundTask(key: backgroundKey)
                return
            }
            DDLogNotice("New contact added on main context \(currentContact.identity)")
            
            let objectMode: WebReceiverUpdate.ObjectMode = .new
            self.responseUpdateContact(contact: currentContact, objectMode: objectMode)
            
            BackgroundTaskManager.shared.cancelBackgroundTask(key: backgroundKey)
        }
    }
    
    private func insertConversation(_ conversation: ConversationEntity) {
        
        let backgroundKey = kAppAckBackgroundTask + SwiftUtils.pseudoRandomString(length: 10)
        BackgroundTaskManager.shared.newBackgroundTask(
            key: backgroundKey,
            timeout: Int(kAppCoreDataProcessMessageBackgroundTaskTime)
        ) {
            
            // Show only chats where lastUpdate is not `nil` to avoid showing chats that only contain system messages
            guard let currentConversation = self.businessInjector.entityManager.entityFetcher.managedObject(
                with: conversation.objectID
            ) as? ConversationEntity,
                currentConversation.lastUpdate != nil else {
                BackgroundTaskManager.shared.cancelBackgroundTask(key: backgroundKey)
                return
            }
            
            DDLogNotice(
                "New conversation added on main context \(currentConversation.isGroup ? currentConversation.groupID?.hexString ?? "?" : currentConversation.contact?.identity ?? "?")"
            )
            
            if currentConversation.isGroup,
               let group = self.businessInjector.groupManager.getGroup(conversation: currentConversation) {
                self.responseUpdateGroup(group: group, objectMode: .new)
            }

            self.responseUpdateConversation(conversation: currentConversation, objectMode: .new)
            BackgroundTaskManager.shared.cancelBackgroundTask(key: backgroundKey)
        }
    }
    
    private func insertBaseMessage(_ baseMessage: BaseMessageEntity) {
        let backgroundKey = kAppAckBackgroundTask + SwiftUtils.pseudoRandomString(length: 10)
        BackgroundTaskManager.shared.newBackgroundTask(
            key: backgroundKey,
            timeout: Int(kAppCoreDataProcessMessageBackgroundTaskTime)
        ) {
            guard let currentMessage = self.businessInjector.entityManager.entityFetcher.managedObject(
                with: baseMessage.objectID
            ) as? BaseMessageEntity else {
                BackgroundTaskManager.shared.cancelBackgroundTask(key: backgroundKey)
                return
            }
            
            DDLogNotice("New message added on main context \(currentMessage.id.hexString)")
            let conversation = currentMessage.conversation
            let id: String =
                if conversation.isGroup,
                let groupID = conversation.groupID {
                    groupID.hexEncodedString()
                }
                else {
                    if let sender = currentMessage.sender {
                        sender.identity
                    }
                    else {
                        if let contact = conversation.contact {
                            contact.identity
                        }
                        else {
                            MyIdentityStore.shared().identity
                        }
                    }
                }
            
            switch currentMessage {
            case is TextMessageEntity:
                self.processTextMessageResponse(currentMessage, id)
            case is FileMessageEntity, is ImageMessageEntity, is VideoMessageEntity, is AudioMessageEntity:
                self.processFileMessageResponse(currentMessage, id)
            default:
                self.processBaseMessageResponse(currentMessage, id)
            }
            BackgroundTaskManager.shared.cancelBackgroundTask(key: backgroundKey)
        }
    }
    
    private func deleteContact(_ contact: ContactEntity) {
        let objectMode: WebReceiverUpdate.ObjectMode = .removed
        responseUpdateContact(contact: contact, objectMode: objectMode)
    }
    
    private func deleteBaseMessage(_ baseMessage: BaseMessageEntity) {
        let changedValues = baseMessage.changedValues()
        let changedValuesForCurrentEvent = baseMessage.changedValuesForCurrentEvent()
        let identity: String?
        var conversation: ConversationEntity? = baseMessage.conversation
        
        if changedValues.keys.contains(BaseMessageEntity.Field.name(
            for: .conversation,
            encrypted: RemoteSecretProvider.remoteSecretManager.isRemoteSecretEnabled
        )), changedValuesForCurrentEvent["conversation"] != nil {
            conversation = changedValuesForCurrentEvent["conversation"] as? ConversationEntity
        }
        
        if let conv = conversation {
            if let sender = baseMessage.sender {
                identity = sender.identity
            }
            else {
                if let contact = conv.contact {
                    identity = contact.identity
                }
                else {
                    identity = MyIdentityStore.shared().identity
                }
            }
            
            if conv.isGroup {
                let objectMode: WebMessagesUpdate.ObjectMode = .removed
                responseUpdateMessage(
                    with: conv.groupID!.hexEncodedString(),
                    message: baseMessage,
                    conversation: conv,
                    objectMode: objectMode
                )
            }
            if let identity, !conv.isGroup {
                let objectMode: WebMessagesUpdate.ObjectMode = .removed
                responseUpdateMessage(
                    with: identity,
                    message: baseMessage,
                    conversation: conv,
                    objectMode: objectMode
                )
            }
        }
    }
    
    private func deleteConversation(_ conversation: ConversationEntity) {
        let changedValuesForCurrentEvent = conversation.changedValuesForCurrentEvent()
        let objectMode: WebConversationUpdate.ObjectMode = .removed
        let contact: ContactEntity? = changedValuesForCurrentEvent["contact"] as? ContactEntity
        responseUpdateDeletedConversation(conversation: conversation, contact: contact, objectMode: objectMode)
    }
    
    private func processTextMessageResponse(_ baseMessage: BaseMessageEntity, _ id: String) {
        var createTextMessageResponse: WebCreateTextMessageResponse?
        if let webRequestID = baseMessage.webRequestID, let createTextMessageRequest = webRequestMessage(
            for: webRequestID
        ) as? WebCreateTextMessageRequest {
            createTextMessageResponse = WebCreateTextMessageResponse(
                message: baseMessage,
                request: createTextMessageRequest
            )
        }
        
        let objectMode: WebMessagesUpdate.ObjectMode = .new
        
        if createTextMessageResponse != nil, let webRequestID = baseMessage.webRequestID {
            DDLogVerbose("[Threema Web] MessagePack -> Send create/textMessage")
            sendMessagePackToRequestedSession(
                with: webRequestID,
                messagePack: createTextMessageResponse!.messagePack(),
                blackListed: false
            )
            responseUpdateMessage(
                with: id,
                message: baseMessage,
                conversation: baseMessage.conversation,
                objectMode: objectMode,
                exclude: webRequestID
            )
            removeWebRequestMessage(with: webRequestID)
        }
        else {
            responseUpdateMessage(
                with: id,
                message: baseMessage,
                conversation: baseMessage.conversation,
                objectMode: objectMode
            )
        }
        
        // background task to send ack to server
        let backgroundKey = kAppAckBackgroundTask + baseMessage.id.hexEncodedString()
        BackgroundTaskManager.shared.newBackgroundTask(
            key: backgroundKey,
            timeout: Int(kAppAckBackgroundTaskTime),
            completionHandler: nil
        )
    }
    
    private func processBaseMessageResponse(_ baseMessage: BaseMessageEntity, _ id: String) {
        let objectMode: WebMessagesUpdate.ObjectMode = .new
        responseUpdateMessage(
            with: id,
            message: baseMessage,
            conversation: baseMessage.conversation,
            objectMode: objectMode
        )
        // background task to send ack to server
        let backgroundKey = kAppAckBackgroundTask + baseMessage.id.hexEncodedString()
        BackgroundTaskManager.shared.newBackgroundTask(
            key: backgroundKey,
            timeout: Int(kAppAckBackgroundTaskTime),
            completionHandler: nil
        )
    }
    
    private func processFileMessageResponse(_ baseMessage: BaseMessageEntity, _ id: String) {
        var createFileMessageResponse: WebCreateFileMessageResponse?
        var backgroundIdentifier: String?
        
        if
            let webRequestID = baseMessage.webRequestID,
            let createFileMessageRequest = webRequestMessage(
                for: webRequestID
            ) as? WebCreateFileMessageRequest {
            createFileMessageRequest.ack = WebAbstractMessageAcknowledgement(webRequestID, true, nil)
            createFileMessageResponse = WebCreateFileMessageResponse(
                message: baseMessage,
                request: createFileMessageRequest
            )
            if let bgIdentifier = createFileMessageRequest.backgroundIdentifier {
                backgroundIdentifier = bgIdentifier
            }
        }
        
        let objectMode: WebMessagesUpdate.ObjectMode = .new
        if createFileMessageResponse != nil,
           let webRequestID = baseMessage.webRequestID {
            DDLogVerbose("[Threema Web] MessagePack -> Send create/fileMessage")
            sendMessagePackToRequestedSession(
                with: webRequestID,
                messagePack: createFileMessageResponse!.messagePack(),
                blackListed: false
            )
            responseUpdateMessage(
                with: id,
                message: baseMessage,
                conversation: baseMessage.conversation,
                objectMode: objectMode,
                exclude: webRequestID
            )
            removeWebRequestMessage(with: webRequestID)
        }
        else {
            responseUpdateMessage(
                with: id,
                message: baseMessage,
                conversation: baseMessage.conversation,
                objectMode: objectMode
            )
        }
        
        // background task to send ack to server
        let backgroundKey = kAppAckBackgroundTask + baseMessage.id.hexEncodedString()
        BackgroundTaskManager.shared.newBackgroundTask(
            key: backgroundKey,
            timeout: Int(kAppAckBackgroundTaskTime),
            completionHandler: nil
        )
        
        if backgroundIdentifier != nil {
            BackgroundTaskManager.shared.cancelBackgroundTask(key: backgroundIdentifier!)
        }
    }
        
    private func baseMessageIdentity(_ baseMessage: BaseMessageEntity) -> String {
        if let sender = baseMessage.sender {
            return sender.identity
        }
        
        if let contact = baseMessage.conversation.contact {
            return contact.identity
        }
        
        return MyIdentityStore.shared().identity
    }
}

extension WCSessionManager {
    // MARK: BatteryNotifications
    
    @objc private func batteryLevelDidChange(_ notification: Notification) {
        let batteryResponse = WebBatteryStatusUpdate()
        DDLogVerbose("[Threema Web] MessagePack -> Send update/batteryStatus")
        sendMessagePackToAllActiveSessions(messagePack: batteryResponse.messagePack(), blackListed: true)
    }
    
    @objc private func batteryStateDidChange(_ notification: Notification) {
        let batteryResponse = WebBatteryStatusUpdate()
        DDLogVerbose("[Threema Web] MessagePack -> Send update/batteryStatus")
        sendMessagePackToAllActiveSessions(messagePack: batteryResponse.messagePack(), blackListed: true)
    }
    
    // MARK: ProfileNotifications
    
    @objc private func profileNicknameChanged(_ notification: Notification) {
        let profileUpdate = WebProfileUpdate(
            nicknameChanged: true,
            newNickname: MyIdentityStore.shared().pushFromName,
            newAvatar: nil,
            deleteAvatar: false
        )
        DDLogVerbose("[Threema Web] MessagePack -> Send update/profile")
        sendMessagePackToAllActiveSessions(messagePack: profileUpdate.messagePack(), blackListed: false)
    }
    
    @objc private func profilePictureChanged(_ notification: Notification) {
        let profileUpdate: WebProfileUpdate? =
            if let profileDict = MyIdentityStore.shared().profilePicture as? [AnyHashable: Any] {
                if let pictureData = profileDict["ProfilePicture"] {
                    WebProfileUpdate(
                        nicknameChanged: false,
                        newNickname: nil,
                        newAvatar: pictureData as? Data,
                        deleteAvatar: false
                    )
                }
                else {
                    WebProfileUpdate(
                        nicknameChanged: false,
                        newNickname: nil,
                        newAvatar: nil,
                        deleteAvatar: true
                    )
                }
            }
            else {
                WebProfileUpdate(
                    nicknameChanged: false,
                    newNickname: nil,
                    newAvatar: nil,
                    deleteAvatar: true
                )
            }
        DDLogVerbose("[Threema Web] MessagePack -> Send update/profile")
        sendMessagePackToAllActiveSessions(messagePack: profileUpdate!.messagePack(), blackListed: false)
    }
    
    @objc private func blackListChanged(_ notification: Notification) {
        guard !running.isEmpty else {
            return
        }
        
        let identity = notification.object as! String
        guard let contact = businessInjector.entityManager.entityFetcher.contactEntity(for: identity) else {
            return
        }
        
        responseUpdateContact(contact: contact, objectMode: .modified)
    }
    
    @objc private func changedManagedObjects(_ notification: Notification) {
        guard !running.isEmpty, let refreshedObjectIDs = notification.userInfo?[
            DatabaseContext.refreshedObjectIDsKey
        ] as? Set<NSManagedObjectID> else {
            return
        }

        for objectID in refreshedObjectIDs {
            guard let managedObject = businessInjector.entityManager.entityFetcher.managedObject(with: objectID) else {
                continue
            }
            
            DDLogInfo("[dirty-objects] Send dirty object to web client (insert and update): \(objectID)")
            var insertedObjects = Set<NSManagedObject>()
            insertedObjects.insert(managedObject)
            handleInsertedObjects(insertedObjects: insertedObjects)
            handleUpdatedObjects(updatedObjects: insertedObjects, true)
        }
    }
}

Messung
CHZG
88979894

¤ Dauer der Verarbeitung: 0.11 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.






                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Open Source Software

     Quellcodebibliothek
     Eigene Quellcodes
     Fremde Quellcodes
     Suchen

Jenseits des Üblichen ....
    

Besucherstatistik

Besucherstatistik

Statistik
#Sources=1104857
#Domains=439041