import Foundation import UIKit import UserNotifications import SwiftSignalKit import Postbox import TelegramCore import SyncCore import TelegramPresentationData import TelegramUIPreferences import TelegramCallsUI import AccountContext private final class PollStateContext { let subscribers = Bag<(Bool) -> Void>() var disposable: Disposable? deinit { self.disposable?.dispose() } var isEmpty: Bool { return self.disposable == nil && self.subscribers.isEmpty } } private final class NotificationInfo { let dict: [AnyHashable: Any] init(dict: [AnyHashable: Any]) { self.dict = dict } } public final class SharedNotificationManager { private let episodeId: UInt32 private let application: UIApplication private let clearNotificationsManager: ClearNotificationsManager? private let pollLiveLocationOnce: (AccountRecordId) -> Void private var inForeground: Bool = false private var inForegroundDisposable: Disposable? private var accountManager: AccountManager? private var accountsAndKeys: [(Account, Bool, MasterNotificationKey)]? private var accountsAndKeysDisposable: Disposable? private var notifications: [NotificationInfo] = [] private var pollStateContexts: [AccountRecordId: PollStateContext] = [:] init(episodeId: UInt32, application: UIApplication, clearNotificationsManager: ClearNotificationsManager?, inForeground: Signal, accounts: Signal<[(Account, Bool)], NoError>, pollLiveLocationOnce: @escaping (AccountRecordId) -> Void) { assert(Queue.mainQueue().isCurrent()) self.episodeId = episodeId self.application = application self.clearNotificationsManager = clearNotificationsManager self.pollLiveLocationOnce = pollLiveLocationOnce self.inForegroundDisposable = (inForeground |> deliverOnMainQueue).start(next: { [weak self] value in guard let strongSelf = self else { return } strongSelf.inForeground = value }) self.accountsAndKeysDisposable = (accounts |> mapToSignal { accounts -> Signal<[(Account, Bool, MasterNotificationKey)], NoError> in let signals = accounts.map { account, isCurrent -> Signal<(Account, Bool, MasterNotificationKey), NoError> in return masterNotificationsKey(account: account, ignoreDisabled: true) |> map { key -> (Account, Bool, MasterNotificationKey) in return (account, isCurrent, key) } } return combineLatest(signals) } |> deliverOnMainQueue).start(next: { [weak self] accountsAndKeys in guard let strongSelf = self else { return } let shouldProcess = strongSelf.accountsAndKeys == nil strongSelf.accountsAndKeys = accountsAndKeys if shouldProcess { strongSelf.process() } }) } deinit { self.inForegroundDisposable?.dispose() self.accountsAndKeysDisposable?.dispose() } func isPollingState(accountId: AccountRecordId) -> Signal { return Signal { subscriber in let context: PollStateContext if let current = self.pollStateContexts[accountId] { context = current } else { context = PollStateContext() self.pollStateContexts[accountId] = context } subscriber.putNext(context.disposable != nil) let index = context.subscribers.add({ value in subscriber.putNext(value) }) return ActionDisposable { [weak context] in Queue.mainQueue().async { if let current = self.pollStateContexts[accountId], current === context { current.subscribers.remove(index) if current.isEmpty { self.pollStateContexts.removeValue(forKey: accountId) } } } } } } func beginPollingState(account: Account) { let accountId = account.id let context: PollStateContext if let current = self.pollStateContexts[accountId] { context = current } else { context = PollStateContext() self.pollStateContexts[accountId] = context } let previousDisposable = context.disposable context.disposable = (account.stateManager.pollStateUpdateCompletion() |> mapToSignal { messageIds -> Signal<[MessageId], NoError> in return .single(messageIds) |> delay(1.0, queue: Queue.mainQueue()) } |> deliverOnMainQueue).start(next: { [weak self, weak context] _ in guard let strongSelf = self else { return } if let current = strongSelf.pollStateContexts[accountId], current === context { if let disposable = current.disposable { disposable.dispose() current.disposable = nil for f in current.subscribers.copyItems() { f(false) } } if current.isEmpty { strongSelf.pollStateContexts.removeValue(forKey: accountId) } } }) previousDisposable?.dispose() if previousDisposable == nil { for f in context.subscribers.copyItems() { f(true) } } } func addNotification(_ dict: [AnyHashable: Any]) { self.notifications.append(NotificationInfo(dict: dict)) if self.accountsAndKeys != nil { self.process() } } private func process() { guard let accountsAndKeys = self.accountsAndKeys else { return } var decryptedNotifications: [(Account, Bool, [AnyHashable: Any])] = [] for notification in self.notifications { if var encryptedPayload = notification.dict["p"] as? String { encryptedPayload = encryptedPayload.replacingOccurrences(of: "-", with: "+") encryptedPayload = encryptedPayload.replacingOccurrences(of: "_", with: "/") while encryptedPayload.count % 4 != 0 { encryptedPayload.append("=") } if let data = Data(base64Encoded: encryptedPayload) { inner: for (account, isCurrent, key) in accountsAndKeys { if let decryptedData = decryptedNotificationPayload(key: key, data: data) { if let decryptedDict = (try? JSONSerialization.jsonObject(with: decryptedData, options: [])) as? [AnyHashable: Any] { decryptedNotifications.append((account, isCurrent, decryptedDict)) } break inner } } } } } self.notifications.removeAll() for (account, isCurrent, payload) in decryptedNotifications { var redactedPayload = payload if var aps = redactedPayload["aps"] as? [AnyHashable: Any] { if Logger.shared.redactSensitiveData { if aps["alert"] != nil { aps["alert"] = "[[redacted]]" } if aps["body"] != nil { aps["body"] = "[[redacted]]" } } redactedPayload["aps"] = aps } Logger.shared.log("Apns \(self.episodeId)", "\(redactedPayload)") let aps = payload["aps"] as? [AnyHashable: Any] var readMessageId: MessageId? var isForcedLogOut = false var isCall = false var isAnnouncement = false var isLocationPolling = false var notificationRequestId: NotificationManagedNotificationRequestId? var shouldPollState = false var title: String = "" var body: String? var apnsSound: String? var configurationUpdate: (Int32, String, Int32, Data?)? var messagesDeleted: [MessageId] = [] if let aps = aps, let alert = aps["alert"] as? String { if let range = alert.range(of: ": ") { title = String(alert[.. Void in transaction.applyIncomingReadMaxId(readMessageId) }).start() } for messageId in messagesDeleted { self.clearNotificationsManager?.append(messageId) } if !messagesDeleted.isEmpty { let _ = account.postbox.transaction(ignoreDisabled: true, { transaction -> Void in deleteMessages(transaction: transaction, mediaBox: account.postbox.mediaBox, ids: messagesDeleted) }).start() } if readMessageId != nil || !messagesDeleted.isEmpty { self.clearNotificationsManager?.commitNow() } if let (datacenterId, host, port, secret) = configurationUpdate { account.network.mergeBackupDatacenterAddress(datacenterId: datacenterId, host: host, port: port, secret: secret) } } } private var currentNotificationCall: (peer: Peer?, internalId: CallSessionInternalId)? private func updateNotificationCall(call: (peer: Peer?, internalId: CallSessionInternalId)?, strings: PresentationStrings, nameOrder: PresentationPersonNameOrder) { if let previousCall = currentNotificationCall { if #available(iOS 10.0, *) { let center = UNUserNotificationCenter.current() center.removeDeliveredNotifications(withIdentifiers: ["call_\(previousCall.internalId)"]) } else { if let notifications = self.application.scheduledLocalNotifications { for notification in notifications { if let userInfo = notification.userInfo, let callId = userInfo["callId"] as? String, callId == String(describing: previousCall.internalId) { self.application.cancelLocalNotification(notification) } } } } } self.currentNotificationCall = call if let notificationCall = call { let rawText = strings.PUSH_PHONE_CALL_REQUEST(notificationCall.peer?.displayTitle(strings: strings, displayOrder: nameOrder) ?? "").0 let title: String? let body: String if let index = rawText.firstIndex(of: "|") { title = String(rawText[rawText.startIndex ..< index]) body = String(rawText[rawText.index(after: index)...]) } else { title = nil body = rawText } if #available(iOS 10.0, *) { let content = UNMutableNotificationContent() if let title = title { content.title = title } content.body = body content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: "0.m4a")) content.categoryIdentifier = "incomingCall" content.userInfo = [:] let request = UNNotificationRequest(identifier: "call_\(notificationCall.internalId)", content: content, trigger: nil) let center = UNUserNotificationCenter.current() Logger.shared.log("NotificationManager", "adding call \(notificationCall.internalId)") center.add(request, withCompletionHandler: { error in if let error = error { Logger.shared.log("NotificationManager", "error adding call \(notificationCall.internalId), error: \(String(describing: error))") } }) } else { let notification = UILocalNotification() if #available(iOS 8.2, *) { notification.alertTitle = title notification.alertBody = body } else { if let title = title { notification.alertBody = "\(title): \(body)" } else { notification.alertBody = body } } notification.category = "incomingCall" notification.userInfo = ["callId": String(describing: notificationCall.internalId)] notification.soundName = "0.m4a" self.application.presentLocalNotificationNow(notification) } } } private let notificationCallStateDisposable = MetaDisposable() private(set) var notificationCall: PresentationCall? func setNotificationCall(_ call: PresentationCall?, strings: PresentationStrings) { if self.notificationCall?.internalId != call?.internalId { self.notificationCall = call if let notificationCall = self.notificationCall { let peer = notificationCall.peer let internalId = notificationCall.internalId let isIntegratedWithCallKit = notificationCall.isIntegratedWithCallKit self.notificationCallStateDisposable.set((notificationCall.state |> map { state -> (Peer?, CallSessionInternalId)? in if isIntegratedWithCallKit { return nil } if case .ringing = state.state { return (peer, internalId) } else { return nil } } |> distinctUntilChanged(isEqual: { $0?.1 == $1?.1 })).start(next: { [weak self] peerAndInternalId in self?.updateNotificationCall(call: peerAndInternalId, strings: strings, nameOrder: .firstLast) })) } else { self.notificationCallStateDisposable.set(nil) self.updateNotificationCall(call: nil, strings: strings, nameOrder: .firstLast) } } } }