From f9191aba6be705bd50d257427dc3a3162748def1 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Sun, 6 Apr 2025 18:55:42 +0400 Subject: [PATCH] Conference updates --- .../Sources/NotificationService.swift | 89 ++++-- .../Sources/PresentationCallManager.swift | 4 +- .../Sources/CallControllerNodeV2.swift | 9 +- .../Sources/PresentationCall.swift | 64 +++-- .../Sources/State/AccountStateManager.swift | 2 +- .../Sources/State/CallSessionManager.swift | 141 +++++++--- .../TelegramUI/Sources/AppDelegate.swift | 260 +++++++++++------- .../Sources/SharedWakeupManager.swift | 12 +- 8 files changed, 405 insertions(+), 176 deletions(-) diff --git a/Telegram/NotificationService/Sources/NotificationService.swift b/Telegram/NotificationService/Sources/NotificationService.swift index 87d3a46560..5470c8bb81 100644 --- a/Telegram/NotificationService/Sources/NotificationService.swift +++ b/Telegram/NotificationService/Sources/NotificationService.swift @@ -926,13 +926,17 @@ private final class NotificationServiceHandler { var localContactId: String? } - struct ConferenceCallData { + struct GroupCallData { var id: Int64 - var updates: String + var fromId: PeerId + var fromTitle: String + var isVideo: Bool + var messageId: Int32 + var accountId: Int64 } var callData: CallData? - var conferenceCallData: ConferenceCallData? + var groupCallData: GroupCallData? if let messageIdString = payloadJson["msg_id"] as? String { messageId = Int32(messageIdString) @@ -958,21 +962,25 @@ private final class NotificationServiceHandler { peerId = PeerId(namespace: Namespaces.Peer.SecretChat, id: PeerId.Id._internalFromInt64Value(encryptionIdValue)) } } + + #if DEBUG + if let locKey = payloadJson["loc-key"] as? String, locKey == "CONF_CALL_REQUEST" { + } + #endif - if let locKey = payloadJson["loc-key"] as? String, (locKey == "CONF_CALL_REQUEST" || locKey == "CONF_CALL_MISSED"), let callIdString = payloadJson["call_id"] as? String { - if let callId = Int64(callIdString) { - if let updates = payloadJson["updates"] as? String { - var updateString = updates - updateString = updateString.replacingOccurrences(of: "-", with: "+") - updateString = updateString.replacingOccurrences(of: "_", with: "/") - while updateString.count % 4 != 0 { - updateString.append("=") - } - if let updateData = Data(base64Encoded: updateString) { - if let callUpdate = AccountStateManager.extractIncomingCallUpdate(data: updateData) { - let _ = callUpdate - } - } + if let peerId, let locKey = payloadJson["loc-key"] as? String, (locKey == "CONF_CALL_REQUEST" || locKey == "CONF_VIDEOCALL_REQUEST"), let callIdString = payloadJson["call_id"] as? String, let messageIdString = payloadJson["msg_id"] as? String { + if let callId = Int64(callIdString), let messageId = Int32(messageIdString) { + if let fromTitle = payloadJson["call_conference_from"] as? String { + let isVideo = locKey == "CONF_VIDEOCALL_REQUEST" + + groupCallData = GroupCallData( + id: callId, + fromId: peerId, + fromTitle: fromTitle, + isVideo: isVideo, + messageId: messageId, + accountId: recordId.int64 + ) } } } else if let callIdString = payloadJson["call_id"] as? String, let callAccessHashString = payloadJson["call_ah"] as? String, let peerId = peerId, let updates = payloadJson["updates"] as? String { @@ -1011,12 +1019,15 @@ private final class NotificationServiceHandler { case readMessage(MessageId) case readStories(peerId: PeerId, maxId: Int32) case call(CallData) + case groupCall(GroupCallData) } var action: Action? if let callData = callData { action = .call(callData) + } else if let groupCallData { + action = .groupCall(groupCallData) } else if let locKey = payloadJson["loc-key"] as? String { switch locKey { case "SESSION_REVOKE": @@ -1265,6 +1276,50 @@ private final class NotificationServiceHandler { content.body = "Incoming Call" } + updateCurrentContent(content) + completed() + } + }) + } + case let .groupCall(groupCallData): + if let stateManager = strongSelf.stateManager { + let content = NotificationContent(isLockedMessage: nil) + updateCurrentContent(content) + + let _ = (stateManager.postbox.transaction { transaction -> TelegramUser? in + return transaction.getPeer(groupCallData.fromId) as? TelegramUser + }).start(next: { fromPeer in + var voipPayload: [AnyHashable: Any] = [ + "group_call_id": "\(groupCallData.id)", + "msg_id": "\(groupCallData.messageId)", + "video": "0", + "from_id": "\(groupCallData.fromId.id._internalGetInt64Value())", + "from_title": groupCallData.fromTitle, + "accountId": "\(groupCallData.accountId)" + ] + if let phoneNumber = fromPeer?.phone { + voipPayload["phoneNumber"] = phoneNumber + } + + if #available(iOS 14.5, *), voiceCallSettings.enableSystemIntegration { + Logger.shared.log("NotificationService \(episode)", "Will report voip notification") + let content = NotificationContent(isLockedMessage: nil) + updateCurrentContent(content) + + CXProvider.reportNewIncomingVoIPPushPayload(voipPayload, completion: { error in + Logger.shared.log("NotificationService \(episode)", "Did report voip notification, error: \(String(describing: error))") + + completed() + }) + } else { + var content = NotificationContent(isLockedMessage: nil) + if let peer = fromPeer { + content.title = peer.debugDisplayTitle + content.body = incomingCallMessage + } else { + content.body = "Incoming Call" + } + updateCurrentContent(content) completed() } diff --git a/submodules/AccountContext/Sources/PresentationCallManager.swift b/submodules/AccountContext/Sources/PresentationCallManager.swift index 280a392754..e47d704bbf 100644 --- a/submodules/AccountContext/Sources/PresentationCallManager.swift +++ b/submodules/AccountContext/Sources/PresentationCallManager.swift @@ -82,13 +82,15 @@ public struct PresentationCallState: Equatable { public var remoteVideoState: RemoteVideoState public var remoteAudioState: RemoteAudioState public var remoteBatteryLevel: RemoteBatteryLevel + public var supportsConferenceCalls: Bool - public init(state: State, videoState: VideoState, remoteVideoState: RemoteVideoState, remoteAudioState: RemoteAudioState, remoteBatteryLevel: RemoteBatteryLevel) { + public init(state: State, videoState: VideoState, remoteVideoState: RemoteVideoState, remoteAudioState: RemoteAudioState, remoteBatteryLevel: RemoteBatteryLevel, supportsConferenceCalls: Bool) { self.state = state self.videoState = videoState self.remoteVideoState = remoteVideoState self.remoteAudioState = remoteAudioState self.remoteBatteryLevel = remoteBatteryLevel + self.supportsConferenceCalls = supportsConferenceCalls } } diff --git a/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift b/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift index 9f5f9a24db..f66ebcf25b 100644 --- a/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift +++ b/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift @@ -167,11 +167,6 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP self.conferenceAddParticipant?() } - var isConferencePossible = true - if let data = self.call.context.currentAppConfiguration.with({ $0 }).data, let value = data["ios_enable_conference"] as? Double { - isConferencePossible = value != 0.0 - } - self.callScreenState = PrivateCallScreen.State( strings: presentationData.strings, lifecycleState: .connecting, @@ -185,7 +180,7 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP remoteVideo: nil, isRemoteBatteryLow: false, isEnergySavingEnabled: !self.sharedContext.energyUsageSettings.fullTranslucency, - isConferencePossible: isConferencePossible + isConferencePossible: false ) self.isMicrophoneMutedDisposable = (call.isMuted @@ -548,6 +543,8 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP case .active: callScreenState.isRemoteAudioMuted = false } + + callScreenState.isConferencePossible = callState.supportsConferenceCalls if self.callScreenState != callScreenState { self.callScreenState = callScreenState diff --git a/submodules/TelegramCallsUI/Sources/PresentationCall.swift b/submodules/TelegramCallsUI/Sources/PresentationCall.swift index 46dc2b77c9..297b119251 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationCall.swift @@ -274,6 +274,8 @@ public final class PresentationCallImpl: PresentationCall { private var callWasActive = false private var shouldPresentCallRating = false + + private var supportsConferenceCalls: Bool = false private var previousVideoState: PresentationCallState.VideoState? private var previousRemoteVideoState: PresentationCallState.RemoteVideoState? @@ -444,9 +446,9 @@ public final class PresentationCallImpl: PresentationCall { self.isVideo = startWithVideo if self.isVideo { self.videoCapturer = OngoingCallVideoCapturer() - self.statePromise.set(PresentationCallState(state: isOutgoing ? .waiting : .ringing, videoState: .active(isScreencast: self.isScreencastActive, endpointId: ""), remoteVideoState: .inactive, remoteAudioState: .active, remoteBatteryLevel: .normal)) + self.statePromise.set(PresentationCallState(state: isOutgoing ? .waiting : .ringing, videoState: .active(isScreencast: self.isScreencastActive, endpointId: ""), remoteVideoState: .inactive, remoteAudioState: .active, remoteBatteryLevel: .normal, supportsConferenceCalls: self.supportsConferenceCalls)) } else { - self.statePromise.set(PresentationCallState(state: isOutgoing ? .waiting : .ringing, videoState: self.isVideoPossible ? .inactive : .notAvailable, remoteVideoState: .inactive, remoteAudioState: .active, remoteBatteryLevel: .normal)) + self.statePromise.set(PresentationCallState(state: isOutgoing ? .waiting : .ringing, videoState: self.isVideoPossible ? .inactive : .notAvailable, remoteVideoState: .inactive, remoteAudioState: .active, remoteBatteryLevel: .normal, supportsConferenceCalls: self.supportsConferenceCalls)) } self.serializedData = serializedData @@ -457,11 +459,25 @@ public final class PresentationCallImpl: PresentationCall { var didReceiveAudioOutputs = false - if let incomingConferenceSource = incomingConferenceSource { - self.sessionStateDisposable = (context.engine.data.subscribe( - TelegramEngine.EngineData.Item.Messages.Message(id: incomingConferenceSource) + if let incomingConferenceSource { + let isRinging = context.account.callSessionManager.ringingStates() + |> map { ringingStates -> Bool in + for ringingState in ringingStates { + if ringingState.id == internalId { + return true + } + } + return false + } + |> distinctUntilChanged + |> take(1) + self.sessionStateDisposable = (combineLatest(queue: .mainQueue(), + isRinging, + context.engine.data.subscribe( + TelegramEngine.EngineData.Item.Messages.Message(id: incomingConferenceSource) + ) ) - |> deliverOnMainQueue).startStrict(next: { [weak self] message in + |> deliverOnMainQueue).startStrict(next: { [weak self] isRinging, message in guard let self else { return } @@ -485,6 +501,8 @@ public final class PresentationCallImpl: PresentationCall { } else { state = .terminated(id: nil, reason: .ended(.hungUp), options: CallTerminationOptions()) } + } else if isRinging { + state = .ringing } else { state = .terminated(id: nil, reason: .ended(.hungUp), options: CallTerminationOptions()) } @@ -816,6 +834,10 @@ public final class PresentationCallImpl: PresentationCall { } } } + + if case let .active(_, _, _, _, _, _, _, _, supportsConferenceCallsValue) = sessionState.state { + self.supportsConferenceCalls = supportsConferenceCallsValue + } let mappedVideoState: PresentationCallState.VideoState let mappedRemoteVideoState: PresentationCallState.RemoteVideoState @@ -883,7 +905,7 @@ public final class PresentationCallImpl: PresentationCall { switch sessionState.state { case .ringing: - presentationState = PresentationCallState(state: .ringing, videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, remoteAudioState: mappedRemoteAudioState, remoteBatteryLevel: mappedRemoteBatteryLevel) + presentationState = PresentationCallState(state: .ringing, videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, remoteAudioState: mappedRemoteAudioState, remoteBatteryLevel: mappedRemoteBatteryLevel, supportsConferenceCalls: self.supportsConferenceCalls) if previous == nil || previousControl == nil { if !self.reportedIncomingCall, let stableId = sessionState.stableId { self.reportedIncomingCall = true @@ -922,17 +944,17 @@ public final class PresentationCallImpl: PresentationCall { } case .accepting: self.callWasActive = true - presentationState = PresentationCallState(state: .connecting(nil), videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, remoteAudioState: mappedRemoteAudioState, remoteBatteryLevel: mappedRemoteBatteryLevel) + presentationState = PresentationCallState(state: .connecting(nil), videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, remoteAudioState: mappedRemoteAudioState, remoteBatteryLevel: mappedRemoteBatteryLevel, supportsConferenceCalls: self.supportsConferenceCalls) case let .dropping(reason): if case .ended(.switchedToConference) = reason { } else { - presentationState = PresentationCallState(state: .terminating(reason), videoState: mappedVideoState, remoteVideoState: .inactive, remoteAudioState: mappedRemoteAudioState, remoteBatteryLevel: mappedRemoteBatteryLevel) + presentationState = PresentationCallState(state: .terminating(reason), videoState: mappedVideoState, remoteVideoState: .inactive, remoteAudioState: mappedRemoteAudioState, remoteBatteryLevel: mappedRemoteBatteryLevel, supportsConferenceCalls: self.supportsConferenceCalls) } case let .terminated(id, reason, options): - presentationState = PresentationCallState(state: .terminated(id, reason, self.callWasActive && (options.contains(.reportRating) || self.shouldPresentCallRating)), videoState: mappedVideoState, remoteVideoState: .inactive, remoteAudioState: mappedRemoteAudioState, remoteBatteryLevel: mappedRemoteBatteryLevel) + presentationState = PresentationCallState(state: .terminated(id, reason, self.callWasActive && (options.contains(.reportRating) || self.shouldPresentCallRating)), videoState: mappedVideoState, remoteVideoState: .inactive, remoteAudioState: mappedRemoteAudioState, remoteBatteryLevel: mappedRemoteBatteryLevel, supportsConferenceCalls: self.supportsConferenceCalls) case let .requesting(ringing): - presentationState = PresentationCallState(state: .requesting(ringing), videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, remoteAudioState: mappedRemoteAudioState, remoteBatteryLevel: mappedRemoteBatteryLevel) - case .active(_, _, _, _, _, _, _, _), .switchedToConference: + presentationState = PresentationCallState(state: .requesting(ringing), videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, remoteAudioState: mappedRemoteAudioState, remoteBatteryLevel: mappedRemoteBatteryLevel, supportsConferenceCalls: self.supportsConferenceCalls) + case .active(_, _, _, _, _, _, _, _, _), .switchedToConference: self.callWasActive = true var isConference = false @@ -940,12 +962,12 @@ public final class PresentationCallImpl: PresentationCall { isConference = true } - if let callContextState = callContextState, !isConference, case let .active(_, _, keyVisualHash, _, _, _, _, _) = sessionState.state { + if let callContextState = callContextState, !isConference, case let .active(_, _, keyVisualHash, _, _, _, _, _, _) = sessionState.state { switch callContextState.state { case .initializing: - presentationState = PresentationCallState(state: .connecting(keyVisualHash), videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, remoteAudioState: mappedRemoteAudioState, remoteBatteryLevel: mappedRemoteBatteryLevel) + presentationState = PresentationCallState(state: .connecting(keyVisualHash), videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, remoteAudioState: mappedRemoteAudioState, remoteBatteryLevel: mappedRemoteBatteryLevel, supportsConferenceCalls: self.supportsConferenceCalls) case .failed: - presentationState = PresentationCallState(state: .terminating(.error(.disconnected)), videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, remoteAudioState: mappedRemoteAudioState, remoteBatteryLevel: mappedRemoteBatteryLevel) + presentationState = PresentationCallState(state: .terminating(.error(.disconnected)), videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, remoteAudioState: mappedRemoteAudioState, remoteBatteryLevel: mappedRemoteBatteryLevel, supportsConferenceCalls: self.supportsConferenceCalls) self.callSessionManager.drop(internalId: self.internalId, reason: .disconnect, debugLog: .single(nil)) case .connected: let timestamp: Double @@ -955,7 +977,7 @@ public final class PresentationCallImpl: PresentationCall { timestamp = CFAbsoluteTimeGetCurrent() self.activeTimestamp = timestamp } - presentationState = PresentationCallState(state: .active(timestamp, reception, keyVisualHash), videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, remoteAudioState: mappedRemoteAudioState, remoteBatteryLevel: mappedRemoteBatteryLevel) + presentationState = PresentationCallState(state: .active(timestamp, reception, keyVisualHash), videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, remoteAudioState: mappedRemoteAudioState, remoteBatteryLevel: mappedRemoteBatteryLevel, supportsConferenceCalls: self.supportsConferenceCalls) case .reconnecting: let timestamp: Double if let activeTimestamp = self.activeTimestamp { @@ -964,10 +986,10 @@ public final class PresentationCallImpl: PresentationCall { timestamp = CFAbsoluteTimeGetCurrent() self.activeTimestamp = timestamp } - presentationState = PresentationCallState(state: .reconnecting(timestamp, reception, keyVisualHash), videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, remoteAudioState: mappedRemoteAudioState, remoteBatteryLevel: mappedRemoteBatteryLevel) + presentationState = PresentationCallState(state: .reconnecting(timestamp, reception, keyVisualHash), videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, remoteAudioState: mappedRemoteAudioState, remoteBatteryLevel: mappedRemoteBatteryLevel, supportsConferenceCalls: self.supportsConferenceCalls) } - } else if !isConference, case let .active(_, _, keyVisualHash, _, _, _, _, _) = sessionState.state { - presentationState = PresentationCallState(state: .connecting(keyVisualHash), videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, remoteAudioState: mappedRemoteAudioState, remoteBatteryLevel: mappedRemoteBatteryLevel) + } else if !isConference, case let .active(_, _, keyVisualHash, _, _, _, _, _, _) = sessionState.state { + presentationState = PresentationCallState(state: .connecting(keyVisualHash), videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, remoteAudioState: mappedRemoteAudioState, remoteBatteryLevel: mappedRemoteBatteryLevel, supportsConferenceCalls: self.supportsConferenceCalls) } } @@ -1025,7 +1047,7 @@ public final class PresentationCallImpl: PresentationCall { subscribedToScheduled: false, isStream: false ), conferenceCallData), - internalId: CallSessionInternalId(), + internalId: self.internalId, peerId: nil, isChannel: false, invite: nil, @@ -1155,7 +1177,7 @@ public final class PresentationCallImpl: PresentationCall { if let _ = audioSessionControl { self.audioSessionShouldBeActive.set(true) } - case let .active(id, key, _, connections, maxLayer, version, customParameters, allowsP2P): + case let .active(id, key, _, connections, maxLayer, version, customParameters, allowsP2P, _): self.audioSessionShouldBeActive.set(true) if conferenceCallData != nil { diff --git a/submodules/TelegramCore/Sources/State/AccountStateManager.swift b/submodules/TelegramCore/Sources/State/AccountStateManager.swift index 40612411db..ebb08aa4de 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManager.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManager.swift @@ -1146,7 +1146,7 @@ public final class AccountStateManager { strongSelf.reportMessageDeliveryDisposable.add(_internal_reportMessageDelivery(postbox: strongSelf.postbox, network: strongSelf.network, messageIds: Array(events.reportMessageDelivery), fromPushNotification: false).start()) } if !events.addedConferenceInvitationMessagesIds.isEmpty { - strongSelf.callSessionManager?.addConferenceInvitationMessages(ids: events.addedConferenceInvitationMessagesIds) + strongSelf.callSessionManager?.addConferenceInvitationMessages(ids: events.addedConferenceInvitationMessagesIds.map { ($0, nil) }) } if !events.isContactUpdates.isEmpty { strongSelf.addIsContactUpdates(events.isContactUpdates) diff --git a/submodules/TelegramCore/Sources/State/CallSessionManager.swift b/submodules/TelegramCore/Sources/State/CallSessionManager.swift index f418269540..8dc741a563 100644 --- a/submodules/TelegramCore/Sources/State/CallSessionManager.swift +++ b/submodules/TelegramCore/Sources/State/CallSessionManager.swift @@ -85,7 +85,7 @@ enum CallSessionInternalState { case requesting(a: Data, disposable: Disposable) case requested(id: Int64, accessHash: Int64, a: Data, gA: Data, config: SecretChatEncryptionConfig, remoteConfirmationTimestamp: Int32?) case confirming(id: Int64, accessHash: Int64, key: Data, keyId: Int64, keyVisualHash: Data, disposable: Disposable) - case active(id: Int64, accessHash: Int64, beginTimestamp: Int32, key: Data, keyId: Int64, keyVisualHash: Data, connections: CallSessionConnectionSet, maxLayer: Int32, version: String, customParameters: String?, allowsP2P: Bool) + case active(id: Int64, accessHash: Int64, beginTimestamp: Int32, key: Data, keyId: Int64, keyVisualHash: Data, connections: CallSessionConnectionSet, maxLayer: Int32, version: String, customParameters: String?, allowsP2P: Bool, supportsConferenceCalls: Bool) case switchedToConference(slug: String) case dropping(reason: CallSessionTerminationReason, disposable: Disposable) case terminated(id: Int64?, accessHash: Int64?, reason: CallSessionTerminationReason, reportRating: Bool, sendDebugLogs: Bool) @@ -104,7 +104,7 @@ enum CallSessionInternalState { return id case let .confirming(id, _, _, _, _, _): return id - case let .active(id, _, _, _, _, _, _, _, _, _, _): + case let .active(id, _, _, _, _, _, _, _, _, _, _, _): return id case .switchedToConference: return nil @@ -120,19 +120,36 @@ public typealias CallSessionInternalId = UUID typealias CallSessionStableId = Int64 private final class StableIncomingUUIDs { + private enum Key: Hashable { + case global(callId: Int64) + case group(peerId: Int64, messageId: Int32) + } + static let shared = Atomic(value: StableIncomingUUIDs()) - private var dict: [Int64: UUID] = [:] + private var dict: [Key: UUID] = [:] private init() { } func get(id: Int64) -> UUID { - if let value = self.dict[id] { + let key = Key.global(callId: id) + if let value = self.dict[key] { return value } else { let value = UUID() - self.dict[id] = value + self.dict[key] = value + return value + } + } + + func get(peerId: Int64, messageId: Int32) -> UUID { + let key = Key.group(peerId: peerId, messageId: messageId) + if let value = self.dict[key] { + return value + } else { + let value = UUID() + self.dict[key] = value return value } } @@ -173,7 +190,7 @@ public enum CallSessionState { case ringing case accepting case requesting(ringing: Bool) - case active(id: CallId, key: Data, keyVisualHash: Data, connections: CallSessionConnectionSet, maxLayer: Int32, version: String, customParameters: String?, allowsP2P: Bool) + case active(id: CallId, key: Data, keyVisualHash: Data, connections: CallSessionConnectionSet, maxLayer: Int32, version: String, customParameters: String?, allowsP2P: Bool, supportsConferenceCalls: Bool) case switchedToConference(slug: String) case dropping(reason: CallSessionTerminationReason) case terminated(id: CallId?, reason: CallSessionTerminationReason, options: CallTerminationOptions) @@ -190,8 +207,8 @@ public enum CallSessionState { self = .requesting(ringing: true) case let .requested(_, _, _, _, _, remoteConfirmationTimestamp): self = .requesting(ringing: remoteConfirmationTimestamp != nil) - case let .active(id, accessHash, _, key, _, keyVisualHash, connections, maxLayer, version, customParameters, allowsP2P): - self = .active(id: CallId(id: id, accessHash: accessHash), key: key, keyVisualHash: keyVisualHash, connections: connections, maxLayer: maxLayer, version: version, customParameters: customParameters, allowsP2P: allowsP2P) + case let .active(id, accessHash, _, key, _, keyVisualHash, connections, maxLayer, version, customParameters, allowsP2P, supportsConferenceCalls): + self = .active(id: CallId(id: id, accessHash: accessHash), key: key, keyVisualHash: keyVisualHash, connections: connections, maxLayer: maxLayer, version: version, customParameters: customParameters, allowsP2P: allowsP2P, supportsConferenceCalls: supportsConferenceCalls) case let .dropping(reason, _): self = .dropping(reason: reason) case let .terminated(id, accessHash, reason, reportRating, sendDebugLogs): @@ -379,6 +396,16 @@ private final class CallSessionContext { } } +public struct IncomingConferenceTermporaryExternalInfo { + public var callId: Int64 + public var isVideo: Bool + + public init(callId: Int64, isVideo: Bool) { + self.callId = callId + self.isVideo = isVideo + } +} + private final class IncomingConferenceInvitationContext { enum State: Equatable { case pending @@ -393,19 +420,52 @@ private final class IncomingConferenceInvitationContext { private(set) var state: State = .pending - init(queue: Queue, postbox: Postbox, messageId: MessageId, updated: @escaping () -> Void) { + init(queue: Queue, postbox: Postbox, internalId: CallSessionInternalId, messageId: MessageId, externalInfo: IncomingConferenceTermporaryExternalInfo?, updated: @escaping () -> Void) { self.queue = queue - self.internalId = CallSessionInternalId() + self.internalId = internalId let key = PostboxViewKey.messages(Set([messageId])) - self.disposable = (postbox.combinedView(keys: [key]) - |> map { view -> Message? in - guard let view = view.views[key] as? MessagesView else { - return nil - } - return view.messages[messageId] + + if let externalInfo { + self.state = .ringing( + callId: externalInfo.callId, + isVideo: externalInfo.isVideo, + otherParticipants: [] + ) } + + let waitSignal: Signal + if externalInfo != nil { + waitSignal = postbox.combinedView(keys: [key]) + |> mapToSignal { view -> Signal in + guard let view = view.views[key] as? MessagesView else { + return .never() + } + if view.messages[messageId] == nil { + return .never() + } + return .single(Void()) + } + |> take(1) + |> timeout(5.0, queue: self.queue, alternate: deferred { + Logger.shared.log("CallSessionManagerContext", "IncomingConferenceInvitationContext timeout for message \(messageId)") + return Signal.single(Void()) + }) + } else { + waitSignal = .single(Void()) + } + + self.disposable = (waitSignal |> map { _ -> Message? in return nil } + |> then( + postbox.combinedView(keys: [key]) + |> map { view -> Message? in + guard let view = view.views[key] as? MessagesView else { + return nil + } + return view.messages[messageId] + } + ) |> deliverOn(self.queue)).startStrict(next: { [weak self] message in guard let self = self else { return @@ -782,7 +842,7 @@ private final class CallSessionManagerContext { case let .accepting(id, accessHash, _, _, disposable): dropData = (id, accessHash, .abort) disposable.dispose() - case let .active(id, accessHash, beginTimestamp, _, _, _, _, _, _, _, _): + case let .active(id, accessHash, beginTimestamp, _, _, _, _, _, _, _, _, _): let duration = max(0, Int32(CFAbsoluteTimeGetCurrent()) - beginTimestamp) let internalReason: DropCallSessionReason switch reason { @@ -884,7 +944,7 @@ private final class CallSessionManagerContext { var dropData: (CallSessionStableId, Int64)? let isVideo = context.type == .video switch context.state { - case let .active(id, accessHash, _, _, _, _, _, _, _, _, _): + case let .active(id, accessHash, _, _, _, _, _, _, _, _, _, _): dropData = (id, accessHash) default: break @@ -934,9 +994,9 @@ private final class CallSessionManagerContext { case let .waiting(config): context.state = .awaitingConfirmation(id: id, accessHash: accessHash, gAHash: gAHash, b: b, config: config) strongSelf.contextUpdated(internalId: internalId) - case let .call(config, gA, timestamp, connections, maxLayer, version, customParameters, allowsP2P): + case let .call(config, gA, timestamp, connections, maxLayer, version, customParameters, allowsP2P, supportsConferenceCalls): if let (key, keyId, keyVisualHash) = strongSelf.makeSessionEncryptionKey(config: config, gAHash: gAHash, b: b, gA: gA) { - context.state = .active(id: id, accessHash: accessHash, beginTimestamp: timestamp, key: key, keyId: keyId, keyVisualHash: keyVisualHash, connections: connections, maxLayer: maxLayer, version: version, customParameters: customParameters, allowsP2P: allowsP2P) + context.state = .active(id: id, accessHash: accessHash, beginTimestamp: timestamp, key: key, keyId: keyId, keyVisualHash: keyVisualHash, connections: connections, maxLayer: maxLayer, version: version, customParameters: customParameters, allowsP2P: allowsP2P, supportsConferenceCalls: supportsConferenceCalls) strongSelf.contextUpdated(internalId: internalId) } else { strongSelf.drop(internalId: internalId, reason: .disconnect, debugLog: .single(nil)) @@ -957,7 +1017,7 @@ private final class CallSessionManagerContext { func sendSignalingData(internalId: CallSessionInternalId, data: Data) { if let context = self.contexts[internalId] { switch context.state { - case let .active(id, accessHash, _, _, _, _, _, _, _, _, _): + case let .active(id, accessHash, _, _, _, _, _, _, _, _, _, _): context.signalingDisposables.add(self.network.request(Api.functions.phone.sendSignalingData(peer: .inputPhoneCall(id: id, accessHash: accessHash), data: Buffer(data: data))).start()) default: break @@ -973,7 +1033,7 @@ private final class CallSessionManagerContext { var idAndAccessHash: (id: Int64, accessHash: Int64)? switch context.state { - case let .active(id, accessHash, _, _, _, _, _, _, _, _, _): + case let .active(id, accessHash, _, _, _, _, _, _, _, _, _, _): idAndAccessHash = (id, accessHash) default: break @@ -1097,7 +1157,7 @@ private final class CallSessionManagerContext { disposable.dispose() context.state = .terminated(id: id, accessHash: accessHash, reason: parsedReason, reportRating: reportRating, sendDebugLogs: sendDebugLogs) self.contextUpdated(internalId: internalId) - case let .active(id, accessHash, _, _, _, _, _, _, _, _, _): + case let .active(id, accessHash, _, _, _, _, _, _, _, _, _, _): context.state = .terminated(id: id, accessHash: accessHash, reason: parsedReason, reportRating: reportRating, sendDebugLogs: sendDebugLogs) self.contextUpdated(internalId: internalId) case let .awaitingConfirmation(id, accessHash, _, _, _): @@ -1127,13 +1187,14 @@ private final class CallSessionManagerContext { } case let .phoneCall(flags, id, _, _, _, _, gAOrB, keyFingerprint, callProtocol, connections, startDate, customParameters): let allowsP2P = (flags & (1 << 5)) != 0 + let supportsConferenceCalls = (flags & (1 << 8)) != 0 if let internalId = self.contextIdByStableId[id] { if let context = self.contexts[internalId] { switch context.state { case .accepting, .dropping, .requesting, .ringing, .terminated, .requested, .switchedToConference: break - case let .active(id, accessHash, beginTimestamp, key, keyId, keyVisualHash, connections, maxLayer, version, customParameters, allowsP2P): - context.state = .active(id: id, accessHash: accessHash, beginTimestamp: beginTimestamp, key: key, keyId: keyId, keyVisualHash: keyVisualHash, connections: connections, maxLayer: maxLayer, version: version, customParameters: customParameters, allowsP2P: allowsP2P) + case let .active(id, accessHash, beginTimestamp, key, keyId, keyVisualHash, connections, maxLayer, version, customParameters, allowsP2P, supportsConferenceCalls): + context.state = .active(id: id, accessHash: accessHash, beginTimestamp: beginTimestamp, key: key, keyId: keyId, keyVisualHash: keyVisualHash, connections: connections, maxLayer: maxLayer, version: version, customParameters: customParameters, allowsP2P: allowsP2P, supportsConferenceCalls: supportsConferenceCalls) self.contextUpdated(internalId: internalId) case let .awaitingConfirmation(_, accessHash, gAHash, b, config): if let (key, calculatedKeyId, keyVisualHash) = self.makeSessionEncryptionKey(config: config, gAHash: gAHash, b: b, gA: gAOrB.makeData()) { @@ -1152,7 +1213,7 @@ private final class CallSessionManagerContext { let isVideoPossible = self.videoVersions().contains(where: { versions.contains($0) }) context.isVideoPossible = isVideoPossible - context.state = .active(id: id, accessHash: accessHash, beginTimestamp: startDate, key: key, keyId: calculatedKeyId, keyVisualHash: keyVisualHash, connections: parseConnectionSet(primary: connections.first!, alternative: Array(connections[1...])), maxLayer: maxLayer, version: versions[0], customParameters: customParametersValue, allowsP2P: allowsP2P) + context.state = .active(id: id, accessHash: accessHash, beginTimestamp: startDate, key: key, keyId: calculatedKeyId, keyVisualHash: keyVisualHash, connections: parseConnectionSet(primary: connections.first!, alternative: Array(connections[1...])), maxLayer: maxLayer, version: versions[0], customParameters: customParametersValue, allowsP2P: allowsP2P, supportsConferenceCalls: supportsConferenceCalls) self.contextUpdated(internalId: internalId) } else { self.drop(internalId: internalId, reason: .disconnect, debugLog: .single(nil)) @@ -1179,7 +1240,7 @@ private final class CallSessionManagerContext { let isVideoPossible = self.videoVersions().contains(where: { versions.contains($0) }) context.isVideoPossible = isVideoPossible - context.state = .active(id: id, accessHash: accessHash, beginTimestamp: startDate, key: key, keyId: keyId, keyVisualHash: keyVisualHash, connections: parseConnectionSet(primary: connections.first!, alternative: Array(connections[1...])), maxLayer: maxLayer, version: versions[0], customParameters: customParametersValue, allowsP2P: allowsP2P) + context.state = .active(id: id, accessHash: accessHash, beginTimestamp: startDate, key: key, keyId: keyId, keyVisualHash: keyVisualHash, connections: parseConnectionSet(primary: connections.first!, alternative: Array(connections[1...])), maxLayer: maxLayer, version: versions[0], customParameters: customParametersValue, allowsP2P: allowsP2P, supportsConferenceCalls: supportsConferenceCalls) self.contextUpdated(internalId: internalId) } else { self.drop(internalId: internalId, reason: .disconnect, debugLog: .single(nil)) @@ -1256,10 +1317,12 @@ private final class CallSessionManagerContext { } } - func addConferenceInvitationMessages(ids: [MessageId]) { - for id in ids { + func addConferenceInvitationMessages(ids: [(id: MessageId, externalInfo: IncomingConferenceTermporaryExternalInfo?)]) { + var updateRingingStates = false + for (id, externalInfo) in ids { if self.incomingConferenceInvitationContexts[id] == nil { - let context = IncomingConferenceInvitationContext(queue: self.queue, postbox: self.postbox, messageId: id, updated: { [weak self] in + Logger.shared.log("CallSessionManagerContext", "Adding incoming conference invitation context for message \(id)") + let context = IncomingConferenceInvitationContext(queue: self.queue, postbox: self.postbox, internalId: CallSessionManager.getStableIncomingUUID(peerId: id.peerId.id._internalGetInt64Value(), messageId: id.id), messageId: id, externalInfo: externalInfo, updated: { [weak self] in guard let self else { return } @@ -1275,8 +1338,14 @@ private final class CallSessionManagerContext { } }) self.incomingConferenceInvitationContexts[id] = context + updateRingingStates = true + } else { + Logger.shared.log("CallSessionManagerContext", "Conference invitation context for message \(id) already exists") } } + if updateRingingStates { + self.ringingStatesUpdated() + } } private func makeSessionEncryptionKey(config: SecretChatEncryptionConfig, gAHash: Data, b: Data, gA: Data) -> (key: Data, keyId: Int64, keyVisualHash: Data)? { @@ -1349,6 +1418,12 @@ public final class CallSessionManager { return impl.get(id: stableId) } } + + public static func getStableIncomingUUID(peerId: Int64, messageId: Int32) -> UUID { + return StableIncomingUUIDs.shared.with { impl in + return impl.get(peerId: peerId, messageId: messageId) + } + } private let queue = Queue() private var contextRef: Unmanaged? @@ -1388,7 +1463,7 @@ public final class CallSessionManager { } } - func addConferenceInvitationMessages(ids: [MessageId]) { + public func addConferenceInvitationMessages(ids: [(id: MessageId, externalInfo: IncomingConferenceTermporaryExternalInfo?)]) { self.withContext { context in context.addConferenceInvitationMessages(ids: ids) } @@ -1500,7 +1575,7 @@ public final class CallSessionManager { private enum AcceptedCall { case waiting(config: SecretChatEncryptionConfig) - case call(config: SecretChatEncryptionConfig, gA: Data, timestamp: Int32, connections: CallSessionConnectionSet, maxLayer: Int32, version: String, customParameters: String?, allowsP2P: Bool) + case call(config: SecretChatEncryptionConfig, gA: Data, timestamp: Int32, connections: CallSessionConnectionSet, maxLayer: Int32, version: String, customParameters: String?, allowsP2P: Bool, supportsConferenceCalls: Bool) } private enum AcceptCallResult { @@ -1553,7 +1628,7 @@ private func acceptCallSession(accountPeerId: PeerId, postbox: Postbox, network: customParametersValue = data } - return .success(.call(config: config, gA: gAOrB.makeData(), timestamp: startDate, connections: parseConnectionSet(primary: connections.first!, alternative: Array(connections[1...])), maxLayer: maxLayer, version: versions[0], customParameters: customParametersValue, allowsP2P: (flags & (1 << 5)) != 0)) + return .success(.call(config: config, gA: gAOrB.makeData(), timestamp: startDate, connections: parseConnectionSet(primary: connections.first!, alternative: Array(connections[1...])), maxLayer: maxLayer, version: versions[0], customParameters: customParametersValue, allowsP2P: (flags & (1 << 5)) != 0, supportsConferenceCalls: (flags & (1 << 8)) != 0)) } else { return .failed } diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index 29f6e34a44..046a024610 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -2057,12 +2057,6 @@ private func extractAccountManagerState(records: AccountRecordsView take(1) - |> deliverOnMainQueue).start(next: { sharedApplicationContext in - let _ = (sharedApplicationContext.sharedContext.activeAccountContexts - |> take(1) - |> deliverOnMainQueue).start(next: { activeAccounts in - var processed = false - for (_, context, _) in activeAccounts.accounts { - if context.account.id == accountId { - context.account.stateManager.processIncomingCallUpdate(data: updateData, completion: { _ in - }) - - //callUpdate.callId - let disposable = MetaDisposable() - self.watchedCallsDisposables.add(disposable) - - disposable.set((context.account.callSessionManager.callState(internalId: CallSessionManager.getStableIncomingUUID(stableId: callUpdate.callId)) - |> deliverOnMainQueue).start(next: { state in - switch state.state { - case .terminated: - callKitIntegration.dropCall(uuid: CallSessionManager.getStableIncomingUUID(stableId: callUpdate.callId)) - default: - break - } - })) - - processed = true - - break + if let fromIdString = payloadJson["from_id"] as? String, let fromId = Int64(fromIdString), let groupCallIdString = payloadJson["group_call_id"] as? String, let groupCallId = Int64(groupCallIdString), let messageIdString = payloadJson["msg_id"] as? String, let messageId = Int32(messageIdString), let isVideoString = payloadJson["video"] as? String, let isVideo = Int32(isVideoString), let fromTitle = payloadJson["from_title"] as? String { + guard let callKitIntegration = CallKitIntegration.shared else { + Logger.shared.log("App \(self.episodeId) PushRegistry", "CallKitIntegration is not available") + completion() + return + } + + let fromPeerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(fromId)) + let messageId = MessageId(peerId: fromPeerId, namespace: Namespaces.Message.Cloud, id: messageId) + + let internalId = CallSessionManager.getStableIncomingUUID(peerId: fromPeerId.id._internalGetInt64Value(), messageId: messageId.id) + + //TODO:localize + let displayTitle: "\(fromTitle)" + + callKitIntegration.reportIncomingCall( + uuid: internalId, + stableId: groupCallId, + handle: "\(fromPeerId.id._internalGetInt64Value())", + phoneNumber: phoneNumber.flatMap(formatPhoneNumber), + isVideo: isVideo != 0, + displayTitle: displayTitle, + completion: { error in + if let error = error { + if error.domain == "com.apple.CallKit.error.incomingcall" && (error.code == -3 || error.code == 3) { + Logger.shared.log("PresentationCall", "reportIncomingCall device in DND mode") + } else { + Logger.shared.log("PresentationCall", "reportIncomingCall error \(error)") + /*Queue.mainQueue().async { + if let strongSelf = self { + strongSelf.callSessionManager.drop(internalId: strongSelf.internalId, reason: .hangUp, debugLog: .single(nil)) + } + }*/ + } } } + ) + + let _ = (self.sharedContextPromise.get() + |> take(1) + |> deliverOnMainQueue).start(next: { sharedApplicationContext in + let _ = (sharedApplicationContext.sharedContext.activeAccountContexts + |> take(1) + |> deliverOnMainQueue).start(next: { activeAccounts in + var processed = false + for (_, context, _) in activeAccounts.accounts { + if context.account.id == accountId { + context.account.callSessionManager.addConferenceInvitationMessages(ids: [(messageId, IncomingConferenceTermporaryExternalInfo(callId: groupCallId, isVideo: isVideo != 0))]) + + /*disposable.set((context.account.callSessionManager.callState(internalId: internalId) + |> deliverOnMainQueue).start(next: { state in + switch state.state { + case .terminated: + callKitIntegration.dropCall(uuid: internalId) + default: + break + } + }))*/ + + processed = true + + break + } + } + + if !processed { + callKitIntegration.dropCall(uuid: internalId) + } + }) - if !processed { - callKitIntegration.dropCall(uuid: CallSessionManager.getStableIncomingUUID(stableId: callUpdate.callId)) + sharedApplicationContext.wakeupManager.allowBackgroundTimeExtension(timeout: 2.0) + + if case PKPushType.voIP = type { + Logger.shared.log("App \(self.episodeId) PushRegistry", "pushRegistry payload: \(payload.dictionaryPayload)") + sharedApplicationContext.notificationManager.addNotification(payload.dictionaryPayload) } }) - - sharedApplicationContext.wakeupManager.allowBackgroundTimeExtension(timeout: 2.0) - - if case PKPushType.voIP = type { - Logger.shared.log("App \(self.episodeId) PushRegistry", "pushRegistry payload: \(payload.dictionaryPayload)") - sharedApplicationContext.notificationManager.addNotification(payload.dictionaryPayload) + } else { + guard var updateString = payloadJson["updates"] as? String else { + Logger.shared.log("App \(self.episodeId) PushRegistry", "updates is nil") + completion() + return } - }) + + updateString = updateString.replacingOccurrences(of: "-", with: "+") + updateString = updateString.replacingOccurrences(of: "_", with: "/") + while updateString.count % 4 != 0 { + updateString.append("=") + } + guard let updateData = Data(base64Encoded: updateString) else { + Logger.shared.log("App \(self.episodeId) PushRegistry", "Couldn't decode updateData") + completion() + return + } + guard let callUpdate = AccountStateManager.extractIncomingCallUpdate(data: updateData) else { + Logger.shared.log("App \(self.episodeId) PushRegistry", "Couldn't extract call update") + completion() + return + } + guard let callKitIntegration = CallKitIntegration.shared else { + Logger.shared.log("App \(self.episodeId) PushRegistry", "CallKitIntegration is not available") + completion() + return + } + + callKitIntegration.reportIncomingCall( + uuid: CallSessionManager.getStableIncomingUUID(stableId: callUpdate.callId), + stableId: callUpdate.callId, + handle: "\(callUpdate.peer.id.id._internalGetInt64Value())", + phoneNumber: phoneNumber.flatMap(formatPhoneNumber), + isVideo: callUpdate.isVideo, + displayTitle: callUpdate.peer.debugDisplayTitle, + completion: { error in + if let error = error { + if error.domain == "com.apple.CallKit.error.incomingcall" && (error.code == -3 || error.code == 3) { + Logger.shared.log("PresentationCall", "reportIncomingCall device in DND mode") + } else { + Logger.shared.log("PresentationCall", "reportIncomingCall error \(error)") + /*Queue.mainQueue().async { + if let strongSelf = self { + strongSelf.callSessionManager.drop(internalId: strongSelf.internalId, reason: .hangUp, debugLog: .single(nil)) + } + }*/ + } + } + } + ) + + let _ = (self.sharedContextPromise.get() + |> take(1) + |> deliverOnMainQueue).start(next: { sharedApplicationContext in + let _ = (sharedApplicationContext.sharedContext.activeAccountContexts + |> take(1) + |> deliverOnMainQueue).start(next: { activeAccounts in + var processed = false + for (_, context, _) in activeAccounts.accounts { + if context.account.id == accountId { + context.account.stateManager.processIncomingCallUpdate(data: updateData, completion: { _ in + }) + + let disposable = MetaDisposable() + self.watchedCallsDisposables.add(disposable) + + disposable.set((context.account.callSessionManager.callState(internalId: CallSessionManager.getStableIncomingUUID(stableId: callUpdate.callId)) + |> deliverOnMainQueue).start(next: { state in + switch state.state { + case .terminated: + callKitIntegration.dropCall(uuid: CallSessionManager.getStableIncomingUUID(stableId: callUpdate.callId)) + default: + break + } + })) + + processed = true + + break + } + } + + if !processed { + callKitIntegration.dropCall(uuid: CallSessionManager.getStableIncomingUUID(stableId: callUpdate.callId)) + } + }) + + sharedApplicationContext.wakeupManager.allowBackgroundTimeExtension(timeout: 2.0) + + if case PKPushType.voIP = type { + Logger.shared.log("App \(self.episodeId) PushRegistry", "pushRegistry payload: \(payload.dictionaryPayload)") + sharedApplicationContext.notificationManager.addNotification(payload.dictionaryPayload) + } + }) + } Logger.shared.log("App \(self.episodeId) PushRegistry", "Invoking completion handler") diff --git a/submodules/TelegramUI/Sources/SharedWakeupManager.swift b/submodules/TelegramUI/Sources/SharedWakeupManager.swift index fca146e0e5..7b4c2e5731 100644 --- a/submodules/TelegramUI/Sources/SharedWakeupManager.swift +++ b/submodules/TelegramUI/Sources/SharedWakeupManager.swift @@ -155,6 +155,12 @@ public final class SharedWakeupManager { } |> distinctUntilChanged + let keepUpdatesForCalls = combineLatest(queue: .mainQueue(), hasActiveCalls, hasActiveGroupCalls) + |> map { hasActiveCalls, hasActiveGroupCalls -> Bool in + return hasActiveCalls || hasActiveGroupCalls + } + |> distinctUntilChanged + let isPlayingBackgroundActiveCall = combineLatest(queue: .mainQueue(), hasActiveCalls, hasActiveGroupCalls, hasActiveAudioSession) |> map { hasActiveCalls, hasActiveGroupCalls, hasActiveAudioSession -> Bool in return (hasActiveCalls || hasActiveGroupCalls) && hasActiveAudioSession @@ -181,9 +187,9 @@ public final class SharedWakeupManager { let userInterfaceInUse = accountUserInterfaceInUse(account.id) - return combineLatest(queue: .mainQueue(), account.importantTasksRunning, notificationManager?.isPollingState(accountId: account.id) ?? .single(false), hasActiveAudio, hasActiveCalls, hasActiveLiveLocationPolling, hasWatchTasks, userInterfaceInUse) - |> map { importantTasksRunning, isPollingState, hasActiveAudio, hasActiveCalls, hasActiveLiveLocationPolling, hasWatchTasks, userInterfaceInUse -> (Account, Bool, AccountTasks) in - return (account, primary?.id == account.id, AccountTasks(stateSynchronization: isPollingState, importantTasks: importantTasksRunning, backgroundLocation: hasActiveLiveLocationPolling, backgroundDownloads: false, backgroundAudio: hasActiveAudio, activeCalls: hasActiveCalls, watchTasks: hasWatchTasks, userInterfaceInUse: userInterfaceInUse)) + return combineLatest(queue: .mainQueue(), account.importantTasksRunning, notificationManager?.isPollingState(accountId: account.id) ?? .single(false), hasActiveAudio, keepUpdatesForCalls, hasActiveLiveLocationPolling, hasWatchTasks, userInterfaceInUse) + |> map { importantTasksRunning, isPollingState, hasActiveAudio, keepUpdatesForCalls, hasActiveLiveLocationPolling, hasWatchTasks, userInterfaceInUse -> (Account, Bool, AccountTasks) in + return (account, primary?.id == account.id, AccountTasks(stateSynchronization: isPollingState, importantTasks: importantTasksRunning, backgroundLocation: hasActiveLiveLocationPolling, backgroundDownloads: false, backgroundAudio: hasActiveAudio, activeCalls: keepUpdatesForCalls, watchTasks: hasWatchTasks, userInterfaceInUse: userInterfaceInUse)) } } return combineLatest(signals)