diff --git a/LocalPushProvider/LocalPushProvider.swift b/LocalPushProvider/LocalPushProvider.swift index 2c81fdbb9..7fdbf2203 100644 --- a/LocalPushProvider/LocalPushProvider.swift +++ b/LocalPushProvider/LocalPushProvider.swift @@ -23,42 +23,62 @@ import NetworkExtension import UserNotifications import linphonesw import os +import linphone let APP_GROUP_ID = "group.org.linphone.phone.msgNotification" -class LocalPushProvider: NEAppPushProvider, CoreDelegate { +extension String: Error {} + +class LocalPushProvider: NEAppPushProvider { var core: Core? = nil let log = LoggingService.Instance var logDelegate: LinphoneLoggingServiceManager! var coreDelegateStub : CoreDelegateStub? = nil let defaults = UserDefaults.init(suiteName: APP_GROUP_ID) + var coreIteratorTimer:Timer? = nil + var aggretatorTimer:Timer? = nil + var aggregagor:[ChatMessage] = [] - func createAndStartCore() { + func createCore() throws { + coreDelegateStub = CoreDelegateStub( + onMessageReceived: { (core:Core, chatRoom:ChatRoom, message:ChatMessage) -> Void in + if (self.ignoredContentTypes.contains(message.contentType)) { + self.log.error(message: "Received unexpected content type.\(message.contentType)") + } else { + self.aggregagor.append(message) + } + } + ) guard let configString = providerConfiguration?["coreconfig"] as? String, let config = Config.newFromBuffer(buffer: configString) else { - self.log.error(message: "Unable to get core config through provider configuration") - return + log.error(message: "Unable to get core config through provider configuration") + throw "Unable to get core config through provider configuration" } + logDelegate = try LinphoneLoggingServiceManager(config: config, log: log, domain: "LocalPushProvider") // Ensure a separate UUID from app is used, use previously generated one or a new one if n/a. + if let uuid = defaults?.string(forKey: "misc_uuid") { config.setString(section: "misc", key: "uuid", value: uuid) } else { config.cleanEntry(section: "misc", key: "uuid") } - logDelegate = try! LinphoneLoggingServiceManager(config: config, log: log, domain: "LocalPushProvider") - core = try! Factory.Instance.createCoreWithConfig(config: config, systemContext: nil) - core?.autoIterateEnabled = false // 20ms auto-iterations are too frequent for NE, it gets killed by the OS. (limit of 150 wakeups per second over 300 seconds) - core!.addDelegate(delegate: self) - let timer = Timer(timeInterval: 0.5, target: self, selector: #selector(iterate), userInfo: nil, repeats: true) // 0.5 second - RunLoop.main.add(timer, forMode: .default) - try?core?.start() - core?.addDelegate(delegate: coreDelegateStub!) - log.message(message: "core started") - // Keep generated UUID to avoid re-creating one every time the NE is starting. - if (core!.config!.hasEntry(section: "misc", key: "uuid") != 0) { - defaults?.set(core!.config!.getString(section: "misc", key: "uuid", defaultString: ""), forKey: "misc_uuid") - self.log.message(message: "storing generated UUID \(String(describing: defaults?.string(forKey: "misc_uuid")))") + log.message(message: "Creating LocalPushProvider core with configuration : \(config.dump())") + core = try Factory.Instance.createCoreWithConfig(config: config, systemContext: nil) + core?.autoIterateEnabled = false // 20ms auto-iterations are too frequent for NE, sometimes it gets killed by the OS. (triggers limit exceed of 150 wakeups per second over 300 seconds) + core?.addDelegate(delegate: coreDelegateStub!) + coreIteratorTimer = Timer(timeInterval: 0.1, target: self, selector: #selector(iterate), userInfo: nil, repeats: true) // 0.1 second + RunLoop.main.add(coreIteratorTimer!, forMode: .default) + + let aggregateTime = config.getInt(section: "local_push", key: "notif_aggregation_period", defaultValue: 3) + aggretatorTimer = Timer(timeInterval: TimeInterval(aggregateTime), target: self, selector: #selector(flushAggregator), userInfo: nil, repeats: true) // 0.1 second + RunLoop.main.add(aggretatorTimer!, forMode: .default) + + + core?.accountList.forEach { account in + let params = account.params?.clone() + params?.expires = 60 // handleTimerEvent(), called by the OS, refreshes registers every 60 seconds, we don't need to expiration longer as if it's not refreshed it means the extension is not reachable. + account.params = params } } @@ -66,28 +86,59 @@ class LocalPushProvider: NEAppPushProvider, CoreDelegate { core?.iterate() } - override init() { - super.init() - coreDelegateStub = CoreDelegateStub( - onMessageReceived: { (core:Core, chatRoom:ChatRoom, message:ChatMessage) -> Void in - self.showLocalNotification(message: message) + @objc func flushAggregator() { + if (aggregagor.count == 1) { + showLocalNotification(message: aggregagor[0]) + } else if (aggregagor.count > 1) { + var displayNames : [String] = [] + aggregagor.forEach { message in + let displayName = getDisplayName(message: message) + if (!displayNames.contains(displayName)) { + displayNames.append(displayName) + } } - ) + displayNames.sort() + showMultipleMessagesNotifications(count: aggregagor.count,displayNames: displayNames.joined(separator: ",")) + } + aggregagor.removeAll() } - // MARK: - NEAppPushProvider Life Cycle + - override func start() { - createAndStartCore() + // MARK: - NEAppPushProvider Life Cycle + + override func start(completionHandler: @escaping (Error?) -> Void) { + do { + if (core == nil) { + try createCore() + log.message(message: "Creating core") + } + try core?.start() + coreIteratorTimer?.fire() + aggretatorTimer?.fire() + log.message(message: "Core started") + // Keep freshly generated UUID after start to avoid re-creating one every time the NE is starting. + if (core?.config?.hasEntry(section: "misc", key: "uuid") != 0) { + defaults?.set(core?.config?.getString(section: "misc", key: "uuid", defaultString: ""), forKey: "misc_uuid") + log.message(message: "storing generated UUID \(String(describing: defaults?.string(forKey: "misc_uuid")))") + } + completionHandler(nil) + } catch { + completionHandler(error) + } } override func stop(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { + log.message(message: "Received stop for reason \(reason)") core?.stopAsync() + coreIteratorTimer?.invalidate() + aggretatorTimer?.invalidate() + flushAggregator() completionHandler() } override func handleTimerEvent() { - self.log.message(message: "Refreshing registers (handleTimerEvent)") + log.message(message: "Refreshing registers (handleTimerEvent)") core?.refreshRegisters() } @@ -95,12 +146,31 @@ class LocalPushProvider: NEAppPushProvider, CoreDelegate { let ignoredContentTypes = ["message/imdn+xml","application/im-iscomposing+xml"] - func showLocalNotification(message: ChatMessage) { - - if (ignoredContentTypes.contains(message.contentType)) { - self.log.error(message: "Received unexpected content type.\(message.contentType)") - return + func showMultipleMessagesNotifications(count:Int, displayNames: String) { + let content = UNMutableNotificationContent() + content.title = NSLocalizedString("%s messages received", comment: "").replacingOccurrences(of: "%s", with: String(count)) + content.body = NSLocalizedString("from: %s", comment: "").replacingOccurrences(of: "%s", with: displayNames) + content.sound = UNNotificationSound(named: UNNotificationSoundName("msg.caf")) + content.categoryIdentifier = "app_active" + let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil) + UNUserNotificationCenter.current().add(request) { error in + if let error = error { + self.log.error(message: "Error submitting local notification: \(error)") + return + } + self.log.message(message: "Local notification posted successfully") } + } + + func getDisplayName(message:ChatMessage) -> String { + let fromAddr = message.chatRoom?.peerAddress?.asStringUriOnly() + var displayName = fromAddr?.getDisplayNameFromSipAddress(lc: core!, logger: log, groupId: APP_GROUP_ID) + displayName = displayName != nil ? displayName : message.chatRoom?.peerAddress?.displayName + displayName = displayName != nil && displayName?.isEmpty != true ? displayName :message.chatRoom?.peerAddress?.username + return displayName! + } + + func showLocalNotification(message: ChatMessage) { var messageContent = "" if (message.hasConferenceInvitationContent()) { @@ -109,15 +179,10 @@ class LocalPushProvider: NEAppPushProvider, CoreDelegate { messageContent = message.hasTextContent() ? message.utf8Text : "🗻" } - let fromAddr = message.chatRoom?.peerAddress?.asStringUriOnly() - var displayName = fromAddr?.getDisplayNameFromSipAddress(lc: core!, logger: log, groupId: APP_GROUP_ID) - displayName = displayName != nil ? displayName : message.chatRoom?.peerAddress?.displayName - displayName = displayName != nil && displayName?.isEmpty != true ? displayName :message.chatRoom?.peerAddress?.username - let content = UNMutableNotificationContent() - content.title = displayName! + content.title = getDisplayName(message:message) content.body = messageContent - content.sound = .default + content.sound = UNNotificationSound(named: UNNotificationSoundName("msg.caf")) content.categoryIdentifier = "app_active" let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil) UNUserNotificationCenter.current().add(request) { error in