blob: 5ac3385e8497671d15041b200a3a2db93b93060b [file] [log] [blame]
kkostiuk74d1ae42021-06-17 11:10:15 -04001/*
2 * Copyright (C) 2021-2022 Savoir-faire Linux Inc.
3 *
4 * Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 3 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19 */
20
21import UserNotifications
22import UIKit
23import CallKit
24import Foundation
25import CoreFoundation
26import os
27import Darwin
kkostiuke10e6572022-07-26 15:41:12 -040028import Contacts
kkostiuk74d1ae42021-06-17 11:10:15 -040029
30protocol DarwinNotificationHandler {
31 func listenToMainAppResponse(completion: @escaping (Bool) -> Void)
32 func removeObserver()
33}
34
35enum NotificationField: String {
36 case key
37 case accountId = "to"
38 case aps
39}
40
kkostiuke10e6572022-07-26 15:41:12 -040041enum LocalNotificationType: String {
42 case message
43 case file
44}
45
kkostiuk74d1ae42021-06-17 11:10:15 -040046class NotificationService: UNNotificationServiceExtension {
47
kkostiuke10e6572022-07-26 15:41:12 -040048 private static let localNotificationName = Notification.Name("com.savoirfairelinux.jami.appActive.internal")
kkostiuk74d1ae42021-06-17 11:10:15 -040049
Kateryna Kostiuk6fc15812022-08-12 10:17:34 -040050 private let notificationTimeout = DispatchTimeInterval.seconds(25)
kkostiuk74d1ae42021-06-17 11:10:15 -040051
52 private let notificationCenter = CFNotificationCenterGetDarwinNotifyCenter()
53
54 private var contentHandler: ((UNNotificationContent) -> Void)?
55 private var bestAttemptContent = UNMutableNotificationContent()
56
57 private var adapterService: AdapterService = AdapterService(withAdapter: Adapter())
58
59 private var accountIsActive = false
60 var tasksCompleted = false /// all values from dht parsed, conversation synchronized if needed and files downloaded
61 var numberOfFiles = 0 /// number of files need to be downloaded
Kateryna Kostiuk3b581712022-07-21 08:42:14 -040062 var numberOfMessages = 0 /// number of scheduled messages
kkostiuk74d1ae42021-06-17 11:10:15 -040063 var syncCompleted = false
64 private let tasksGroup = DispatchGroup()
Kateryna Kostiuk19437652022-08-02 13:02:21 -040065 var accountId = ""
kkostiuke10e6572022-07-26 15:41:12 -040066
67 typealias LocalNotification = (content: UNMutableNotificationContent, type: LocalNotificationType)
68
69 private var pendingLocalNotifications = [String: [LocalNotification]]() /// local notification waiting for name lookup
70 private var pendingCalls = [String: [AnyHashable: Any]]() /// calls waiting for name lookup
71 private var names = [String: String]() /// map of peerId and best name
kkostiuk74d1ae42021-06-17 11:10:15 -040072 // swiftlint:disable cyclomatic_complexity
73 override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
74 self.contentHandler = contentHandler
75 defer {
76 finish()
77 }
78 let requestData = requestToDictionary(request: request)
79 if requestData.isEmpty {
80 return
81 }
82
83 /// if main app is active extension should save notification data and let app handle notification
84 saveData(data: requestData)
85 if appIsActive() {
86 return
87 }
88
Kateryna Kostiuk19437652022-08-02 13:02:21 -040089 guard let account = requestData[NotificationField.accountId.rawValue] else { return }
90 accountId = account
kkostiuke10e6572022-07-26 15:41:12 -040091
kkostiuk74d1ae42021-06-17 11:10:15 -040092 /// app is not active. Querry value from dht
93 guard let proxyURL = getProxyCaches(data: requestData),
Kateryna Kostiukfb10d702022-11-30 14:32:28 -050094 let url = getRequestURL(data: requestData, path: proxyURL) else {
kkostiuk74d1ae42021-06-17 11:10:15 -040095 return
96 }
97 tasksGroup.enter()
Kateryna Kostiuk0acc5f52022-08-15 10:21:26 -040098 let defaultSession = URLSession(configuration: .default)
99 let task = defaultSession.dataTask(with: url) {[weak self] (data, _, _) in
kkostiuk74d1ae42021-06-17 11:10:15 -0400100 guard let self = self,
101 let data = data else {
Kateryna Kostiuk6fc15812022-08-12 10:17:34 -0400102 self?.verifyTasksStatus()
kkostiuk74d1ae42021-06-17 11:10:15 -0400103 return
104 }
105 let str = String(decoding: data, as: UTF8.self)
106 let lines = str.split(whereSeparator: \.isNewline)
107 for line in lines {
108 do {
109 guard let jsonData = line.data(using: .utf8),
110 let map = try JSONSerialization.jsonObject(with: jsonData, options: .allowFragments) as? [String: Any],
111 let keyPath = self.getKeyPath(data: requestData),
112 let treatedMessages = self.getTreatedMessagesPath(data: requestData) else {
Kateryna Kostiuk6fc15812022-08-12 10:17:34 -0400113 self.verifyTasksStatus()
114 return
115 }
Kateryna Kostiukba9a7b12023-05-02 09:33:52 -0400116 let result = self.adapterService.decrypt(keyPath: keyPath.path, accountId: self.accountId, messagesPath: treatedMessages.path, value: map)
kkostiuk74d1ae42021-06-17 11:10:15 -0400117 let handleCall: (String, String) -> Void = { [weak self] (peerId, hasVideo) in
118 guard let self = self else {
119 return
120 }
Kateryna Kostiukfd5e6f12022-08-02 11:24:05 -0400121 /// jami will be started. Set accounts to not active state
122 if self.accountIsActive {
123 self.accountIsActive = false
124 self.adapterService.stop()
kkostiuk74d1ae42021-06-17 11:10:15 -0400125 }
Kateryna Kostiukfd5e6f12022-08-02 11:24:05 -0400126 var info = request.content.userInfo
127 info["peerId"] = peerId
128 info["hasVideo"] = hasVideo
Kateryna Kostiuk19437652022-08-02 13:02:21 -0400129 let name = self.bestName(accountId: self.accountId, contactId: peerId)
Kateryna Kostiukfd5e6f12022-08-02 11:24:05 -0400130 if name.isEmpty {
131 info["displayName"] = peerId
132 self.pendingCalls[peerId] = info
Kateryna Kostiuk19437652022-08-02 13:02:21 -0400133 self.startAddressLookup(address: peerId, accountId: self.accountId)
Kateryna Kostiukfd5e6f12022-08-02 11:24:05 -0400134 return
135 }
136 info["displayName"] = name
137 self.presentCall(info: info)
kkostiuk74d1ae42021-06-17 11:10:15 -0400138 }
Kateryna Kostiuk28e72e02023-02-01 16:31:39 -0500139 switch result {
140 case .call(let peerId, let hasVideo):
141 handleCall(peerId, "\(hasVideo)")
142 return
143 case .gitMessage:
Kateryna Kostiuk3dc364c2023-03-31 12:02:31 -0400144 self.handleGitMessage()
Kateryna Kostiuk28e72e02023-02-01 16:31:39 -0500145 case .unknown:
146 break
147 }
kkostiuk74d1ae42021-06-17 11:10:15 -0400148 } catch {
149 print("serialization failed , \(error)")
150 }
151 }
152 self.verifyTasksStatus()
153 }
154 task.resume()
155 _ = tasksGroup.wait(timeout: .now() + notificationTimeout)
156 }
157
158 override func serviceExtensionTimeWillExpire() {
Kateryna Kostiuk97193412023-04-11 11:06:14 -0400159 if !self.tasksCompleted {
160 self.tasksCompleted = true
161 self.tasksGroup.leave()
162 }
kkostiuk74d1ae42021-06-17 11:10:15 -0400163 finish()
164 }
165
Kateryna Kostiuk3dc364c2023-03-31 12:02:31 -0400166 private func handleGitMessage() {
Kateryna Kostiuk28e72e02023-02-01 16:31:39 -0500167 /// check if account already acive
168 guard !self.accountIsActive else { return }
169 self.accountIsActive = true
170 self.adapterService.startAccountsWithListener(accountId: self.accountId) { [weak self] event, eventData in
171 guard let self = self else {
172 return
Alireza8154e1f2022-11-26 16:51:24 -0500173 }
Kateryna Kostiuk28e72e02023-02-01 16:31:39 -0500174 switch event {
175 case .message:
Kateryna Kostiukc24e1042023-05-23 11:11:43 -0400176 self.conversationUpdated(conversationId: eventData.conversationId, accountId: self.accountId)
Kateryna Kostiuk28e72e02023-02-01 16:31:39 -0500177 self.numberOfMessages += 1
178 self.configureMessageNotification(from: eventData.jamiId, body: eventData.content, accountId: self.accountId, conversationId: eventData.conversationId, groupTitle: "")
179 case .fileTransferDone:
Kateryna Kostiukc24e1042023-05-23 11:11:43 -0400180 self.conversationUpdated(conversationId: eventData.conversationId, accountId: self.accountId)
Kateryna Kostiuk28e72e02023-02-01 16:31:39 -0500181 if let url = URL(string: eventData.content) {
182 self.configureFileNotification(from: eventData.jamiId, url: url, accountId: self.accountId, conversationId: eventData.conversationId)
183 } else {
184 self.numberOfFiles -= 1
185 self.verifyTasksStatus()
186 }
187 case .syncCompleted:
188 self.syncCompleted = true
189 self.verifyTasksStatus()
190 case .fileTransferInProgress:
191 self.numberOfFiles += 1
Kateryna Kostiuk28e72e02023-02-01 16:31:39 -0500192 case .invitation:
Kateryna Kostiukc24e1042023-05-23 11:11:43 -0400193 self.conversationUpdated(conversationId: eventData.conversationId, accountId: self.accountId)
Kateryna Kostiuk28e72e02023-02-01 16:31:39 -0500194 self.syncCompleted = true
195 self.numberOfMessages += 1
196 self.configureMessageNotification(from: eventData.jamiId,
197 body: eventData.content,
198 accountId: self.accountId,
199 conversationId: eventData.conversationId,
200 groupTitle: eventData.groupTitle)
201 }
Alireza8154e1f2022-11-26 16:51:24 -0500202 }
203 }
204
kkostiuk74d1ae42021-06-17 11:10:15 -0400205 private func verifyTasksStatus() {
206 guard !self.tasksCompleted else { return } /// we already left taskGroup
kkostiuke10e6572022-07-26 15:41:12 -0400207 /// waiting for lookup
208 if !pendingCalls.isEmpty || !pendingLocalNotifications.isEmpty {
209 return
210 }
kkostiuk74d1ae42021-06-17 11:10:15 -0400211 /// We could finish in two cases:
212 /// 1. we did not start account we are not waiting for the signals from the daemon
213 /// 2. conversation synchronization completed and all files downloaded
Kateryna Kostiuk3b581712022-07-21 08:42:14 -0400214 if !self.accountIsActive || (self.syncCompleted && self.numberOfFiles == 0 && self.numberOfMessages == 0) {
kkostiuk74d1ae42021-06-17 11:10:15 -0400215 self.tasksCompleted = true
216 self.tasksGroup.leave()
217 }
218 }
219
220 private func finish() {
221 if self.accountIsActive {
222 self.accountIsActive = false
223 self.adapterService.stop()
Kateryna Kostiuk84834422023-04-11 10:53:56 -0400224 } else {
225 self.adapterService.removeDelegate()
kkostiuk74d1ae42021-06-17 11:10:15 -0400226 }
kkostiuke10e6572022-07-26 15:41:12 -0400227 /// cleanup pending notifications
228 if !self.pendingCalls.isEmpty, let info = self.pendingCalls.first?.value {
229 self.presentCall(info: info)
230 } else {
231 for notifications in pendingLocalNotifications {
232 for notification in notifications.value {
233 self.presentLocalNotification(notification: notification)
234 }
235 }
kkostiukabde7b92022-07-28 13:15:56 -0400236 pendingLocalNotifications.removeAll()
kkostiuke10e6572022-07-26 15:41:12 -0400237 }
kkostiuk74d1ae42021-06-17 11:10:15 -0400238 if let contentHandler = contentHandler {
239 contentHandler(self.bestAttemptContent)
240 }
241 }
242
243 private func appIsActive() -> Bool {
244 let group = DispatchGroup()
245 defer {
246 self.removeObserver()
247 group.leave()
248 }
249 var appIsActive = false
250 group.enter()
251 /// post darwin notification and wait for the answer from the main app. If answer received app is active
252 self.listenToMainAppResponse { _ in
253 appIsActive = true
254 }
255 CFNotificationCenterPostNotification(notificationCenter, CFNotificationName(Constants.notificationReceived), nil, nil, true)
256 /// wait fro 100 milliseconds. If no answer from main app is received app is not active.
257 _ = group.wait(timeout: .now() + 0.3)
258
259 return appIsActive
260 }
261
262 private func saveData(data: [String: String]) {
263 guard let userDefaults = UserDefaults(suiteName: Constants.appGroupIdentifier) else {
264 return
265 }
266 var notificationData = [[String: String]]()
267 if let existingData = userDefaults.object(forKey: Constants.notificationData) as? [[String: String]] {
268 notificationData = existingData
269 }
270 notificationData.append(data)
271 userDefaults.set(notificationData, forKey: Constants.notificationData)
272 }
273
Kateryna Kostiukc24e1042023-05-23 11:11:43 -0400274 private func conversationUpdated(conversationId: String, accountId: String) {
275 var conversationData = [String: String]()
276 conversationData[Constants.NotificationUserInfoKeys.conversationID.rawValue] = conversationId
277 conversationData[Constants.NotificationUserInfoKeys.accountID.rawValue] = accountId
278 self.setUpdatedConversations(conversation: conversationData)
279 }
280
281 private func setUpdatedConversations(conversation: [String: String]) {
282 /*
283 Save updated conversations so they can be reloaded when Jami
284 becomes active.
285 */
286 guard let userDefaults = UserDefaults(suiteName: Constants.appGroupIdentifier) else {
287 return
288 }
289 var conversationData = [[String: String]]()
290 if let existingData = userDefaults.object(forKey: Constants.updatedConversations) as? [[String: String]] {
291 conversationData = existingData
292 }
293 for data in conversationData
294 where data[Constants.NotificationUserInfoKeys.conversationID.rawValue] ==
295 conversation[Constants.NotificationUserInfoKeys.conversationID.rawValue] {
296 return
297 }
298
299 conversationData.append(conversation)
300 userDefaults.set(conversationData, forKey: Constants.updatedConversations)
301 }
302
kkostiuk74d1ae42021-06-17 11:10:15 -0400303 private func setNotificationCount(notification: UNMutableNotificationContent) {
304 guard let userDefaults = UserDefaults(suiteName: Constants.appGroupIdentifier) else {
305 return
306 }
307
308 if let count = userDefaults.object(forKey: Constants.notificationsCount) as? NSNumber {
309 let new: NSNumber = count.intValue + 1 as NSNumber
310 notification.badge = new
311 userDefaults.set(new, forKey: Constants.notificationsCount)
312 }
313 }
314
315 private func requestToDictionary(request: UNNotificationRequest) -> [String: String] {
316 var dictionary = [String: String]()
317 let userInfo = request.content.userInfo
318 for key in userInfo.keys {
319 /// "aps" is a field added for alert notification type, so it could be received in the extension. This field is not needed by dht
320 if String(describing: key) == NotificationField.aps.rawValue {
321 continue
322 }
323 if let value = userInfo[key] {
324 let keyString = String(describing: key)
325 let valueString = String(describing: value)
326 dictionary[keyString] = valueString
327 }
328 }
329 return dictionary
330 }
331
332 private func requestedData(request: UNNotificationRequest, map: [String: Any]) -> Bool {
333 guard let userInfo = request.content.userInfo as? [String: Any] else { return false }
334 guard let valueIds = userInfo["valueIds"] as? [String: String],
335 let id = map["id"] else {
336 return false
337 }
338 return valueIds.values.contains("\(id)")
339 }
kkostiuke10e6572022-07-26 15:41:12 -0400340
341 private func bestName(accountId: String, contactId: String) -> String {
342 if let name = self.names[contactId], !name.isEmpty {
343 return name
344 }
345 if let contactProfileName = self.contactProfileName(accountId: accountId, contactId: contactId),
346 !contactProfileName.isEmpty {
347 self.names[contactId] = contactProfileName
348 return contactProfileName
349 }
350 let registeredName = self.adapterService.getNameFor(address: contactId, accountId: accountId)
351 if !registeredName.isEmpty {
352 self.names[contactId] = registeredName
353 }
354 return registeredName
355 }
356
357 private func startAddressLookup(address: String, accountId: String) {
358 var nameServer = self.adapterService.getNameServerFor(accountId: accountId)
Kateryna Kostiuk1c0e7562022-08-23 15:17:44 -0400359 nameServer = ensureURLPrefix(urlString: nameServer)
kkostiuke10e6572022-07-26 15:41:12 -0400360 let urlString = nameServer + "/addr/" + address
Kateryna Kostiuk6fc15812022-08-12 10:17:34 -0400361 guard let url = URL(string: urlString) else {
362 self.lookupCompleted(address: address, name: nil)
363 return
364 }
Kateryna Kostiuk0acc5f52022-08-15 10:21:26 -0400365 let defaultSession = URLSession(configuration: .default)
366 let task = defaultSession.dataTask(with: url) {[weak self](data, response, _) in
kkostiuke10e6572022-07-26 15:41:12 -0400367 guard let self = self else { return }
368 var name: String?
369 defer {
370 self.lookupCompleted(address: address, name: name)
371 }
372 guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200,
373 let data = data else {
374 return
375 }
376 do {
377 guard let map = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: String] else { return }
378 if map["name"] != nil {
379 name = map["name"]
380 self.names[address] = name
381 }
382 } catch {
383 print("serialization failed , \(error)")
384 }
385 }
386 task.resume()
387 }
388
Kateryna Kostiuk1c0e7562022-08-23 15:17:44 -0400389 private func ensureURLPrefix(urlString: String) -> String {
390 var urlWithPrefix = urlString
391 if !urlWithPrefix.hasPrefix("http://") && !urlWithPrefix.hasPrefix("https://") {
392 urlWithPrefix = "http://" + urlWithPrefix
393 }
394 return urlWithPrefix
395 }
396
kkostiuke10e6572022-07-26 15:41:12 -0400397 private func lookupCompleted(address: String, name: String?) {
398 for call in pendingCalls where call.key == address {
399 var info = call.value
400 if let name = name {
401 info["displayName"] = name
402 }
403 presentCall(info: info)
kkostiuke10e6572022-07-26 15:41:12 -0400404 return
405 }
406 for pending in pendingLocalNotifications where pending.key == address {
407 let notifications = pending.value
408 for notification in notifications {
409 if let name = name {
410 notification.content.title = name
411 }
412 presentLocalNotification(notification: notification)
413 }
414 pendingLocalNotifications.removeValue(forKey: address)
415 }
416 }
417
418 private func needUpdateNotification(notification: LocalNotification, peerId: String, accountId: String) {
419 if var pending = pendingLocalNotifications[peerId] {
420 pending.append(notification)
421 pendingLocalNotifications[peerId] = pending
422 } else {
423 pendingLocalNotifications[peerId] = [notification]
424 }
425 startAddressLookup(address: peerId, accountId: accountId)
426 }
kkostiuk74d1ae42021-06-17 11:10:15 -0400427}
428// MARK: paths
429extension NotificationService {
430
431 private func getRequestURL(data: [String: String], proxyURL: URL) -> URL? {
432 guard let key = data[NotificationField.key.rawValue] else {
433 return nil
434 }
435 return proxyURL.appendingPathComponent(key)
436 }
437
Kateryna Kostiukfb10d702022-11-30 14:32:28 -0500438 private func getRequestURL(data: [String: String], path: URL) -> URL? {
439 guard let key = data[NotificationField.key.rawValue],
440 let jsonData = NSData(contentsOf: path) as? Data else {
441 return nil
442 }
443 guard let map = try? JSONSerialization.jsonObject(with: jsonData, options: .allowFragments) as? [String: String],
444 var proxyAddress = map.first?.value else {
445 return nil
446 }
447
448 proxyAddress = ensureURLPrefix(urlString: proxyAddress)
449 guard let urlPrpxy = URL(string: proxyAddress) else { return nil }
450 return urlPrpxy.appendingPathComponent(key)
451 }
452
kkostiuk74d1ae42021-06-17 11:10:15 -0400453 private func getKeyPath(data: [String: String]) -> URL? {
454 guard let documentsPath = Constants.documentsPath,
455 let accountId = data[NotificationField.accountId.rawValue] else {
456 return nil
457 }
458 return documentsPath.appendingPathComponent(accountId).appendingPathComponent("ring_device.key")
459 }
460
461 private func getTreatedMessagesPath(data: [String: String]) -> URL? {
462 guard let cachesPath = Constants.cachesPath,
463 let accountId = data[NotificationField.accountId.rawValue] else {
464 return nil
465 }
466 return cachesPath.appendingPathComponent(accountId).appendingPathComponent("treatedMessages")
467 }
468
469 private func getProxyCaches(data: [String: String]) -> URL? {
470 guard let cachesPath = Constants.cachesPath,
471 let accountId = data[NotificationField.accountId.rawValue] else {
472 return nil
473 }
474 return cachesPath.appendingPathComponent(accountId).appendingPathComponent("dhtproxy")
475 }
kkostiuke10e6572022-07-26 15:41:12 -0400476
477 private func contactProfileName(accountId: String, contactId: String) -> String? {
478 guard let documents = Constants.documentsPath else { return nil }
479 let profileURI = "ring:" + contactId
480 let profilePath = documents.path + "/" + "\(accountId)" + "/profiles/" + "\(Data(profileURI.utf8).base64EncodedString()).vcf"
481 if !FileManager.default.fileExists(atPath: profilePath) { return nil }
482
483 guard let data = FileManager.default.contents(atPath: profilePath),
Kateryna Kostiuk6c97c952023-03-30 08:35:53 -0400484 let profile = VCardUtils.parseToProfile(data: data) else { return nil }
485 return profile.alias
kkostiuke10e6572022-07-26 15:41:12 -0400486 }
kkostiuk74d1ae42021-06-17 11:10:15 -0400487}
488
489// MARK: DarwinNotificationHandler
490extension NotificationService: DarwinNotificationHandler {
491 func listenToMainAppResponse(completion: @escaping (Bool) -> Void) {
492 let observer = Unmanaged.passUnretained(self).toOpaque()
493 CFNotificationCenterAddObserver(notificationCenter,
494 observer, { (_, _, _, _, _) in
kkostiuke10e6572022-07-26 15:41:12 -0400495 NotificationCenter.default.post(name: NotificationService.localNotificationName,
kkostiuk74d1ae42021-06-17 11:10:15 -0400496 object: nil,
497 userInfo: nil)
498 },
499 Constants.notificationAppIsActive,
500 nil,
501 .deliverImmediately)
kkostiuke10e6572022-07-26 15:41:12 -0400502 NotificationCenter.default.addObserver(forName: NotificationService.localNotificationName, object: nil, queue: nil) { _ in
kkostiuk74d1ae42021-06-17 11:10:15 -0400503 completion(true)
504 }
505 }
506
507 func removeObserver() {
508 let observer = Unmanaged.passUnretained(self).toOpaque()
509 CFNotificationCenterRemoveEveryObserver(notificationCenter, observer)
kkostiuke10e6572022-07-26 15:41:12 -0400510 NotificationCenter.default.removeObserver(self, name: NotificationService.localNotificationName, object: nil)
kkostiuk74d1ae42021-06-17 11:10:15 -0400511 }
512
513}
514
515// MARK: present notifications
516extension NotificationService {
517 private func createAttachment(identifier: String, image: UIImage, options: [NSObject: AnyObject]?) -> UNNotificationAttachment? {
518 let fileManager = FileManager.default
519 let tmpSubFolderName = ProcessInfo.processInfo.globallyUniqueString
520 let tmpSubFolderURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(tmpSubFolderName, isDirectory: true)
521 do {
522 try fileManager.createDirectory(at: tmpSubFolderURL, withIntermediateDirectories: true, attributes: nil)
523 let imageFileIdentifier = identifier
524 let fileURL = tmpSubFolderURL.appendingPathComponent(imageFileIdentifier)
525 let imageData = UIImage.pngData(image)
526 try imageData()?.write(to: fileURL)
527 let imageAttachment = try UNNotificationAttachment.init(identifier: identifier, url: fileURL, options: options)
528 return imageAttachment
529 } catch {}
530 return nil
531 }
532
kkostiuk8e397cd2022-07-27 15:08:10 -0400533 private func configureFileNotification(from: String, url: URL, accountId: String, conversationId: String) {
kkostiuk74d1ae42021-06-17 11:10:15 -0400534 let content = UNMutableNotificationContent()
kkostiuke10e6572022-07-26 15:41:12 -0400535 content.sound = UNNotificationSound.default
kkostiuk74d1ae42021-06-17 11:10:15 -0400536 let imageName = url.lastPathComponent
kkostiuk74d1ae42021-06-17 11:10:15 -0400537 content.body = imageName
kkostiuk8e397cd2022-07-27 15:08:10 -0400538 var data = [String: String]()
539 data[Constants.NotificationUserInfoKeys.participantID.rawValue] = from
540 data[Constants.NotificationUserInfoKeys.accountID.rawValue] = accountId
541 data[Constants.NotificationUserInfoKeys.conversationID.rawValue] = conversationId
542 content.userInfo = data
kkostiuk74d1ae42021-06-17 11:10:15 -0400543 if let image = UIImage(contentsOfFile: url.path), let attachement = createAttachment(identifier: imageName, image: image, options: nil) {
544 content.attachments = [ attachement ]
545 }
kkostiuke10e6572022-07-26 15:41:12 -0400546 let title = self.bestName(accountId: accountId, contactId: from)
547 if title.isEmpty {
548 content.title = from
549 needUpdateNotification(notification: LocalNotification(content, .file), peerId: from, accountId: accountId)
550 } else {
551 content.title = title
552 presentLocalNotification(notification: LocalNotification(content, .file))
553 }
554 }
555
Kateryna Kostiuk2648aab2022-08-30 14:24:03 -0400556 private func configureMessageNotification(from: String, body: String, accountId: String, conversationId: String, groupTitle: String) {
kkostiuke10e6572022-07-26 15:41:12 -0400557 let content = UNMutableNotificationContent()
558 content.body = body
kkostiuk74d1ae42021-06-17 11:10:15 -0400559 content.sound = UNNotificationSound.default
kkostiuk8e397cd2022-07-27 15:08:10 -0400560 var data = [String: String]()
561 data[Constants.NotificationUserInfoKeys.participantID.rawValue] = from
562 data[Constants.NotificationUserInfoKeys.accountID.rawValue] = accountId
563 data[Constants.NotificationUserInfoKeys.conversationID.rawValue] = conversationId
564 content.userInfo = data
Kateryna Kostiuk2648aab2022-08-30 14:24:03 -0400565 let title = !groupTitle.isEmpty ? groupTitle : self.bestName(accountId: accountId, contactId: from)
kkostiuke10e6572022-07-26 15:41:12 -0400566 if title.isEmpty {
567 content.title = from
568 needUpdateNotification(notification: LocalNotification(content, .message), peerId: from, accountId: accountId)
569 } else {
570 content.title = title
571 presentLocalNotification(notification: LocalNotification(content, .message))
572 }
573 }
574
575 private func presentLocalNotification(notification: LocalNotification) {
576 let content = notification.content
kkostiuk74d1ae42021-06-17 11:10:15 -0400577 setNotificationCount(notification: content)
578 let notificationTrigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.01, repeats: false)
Kateryna Kostiuk3b581712022-07-21 08:42:14 -0400579 let notificationRequest = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: notificationTrigger)
580 UNUserNotificationCenter.current().add(notificationRequest) { [weak self] (error) in
kkostiuke10e6572022-07-26 15:41:12 -0400581 if notification.type == .message {
582 self?.numberOfMessages -= 1
583 } else {
584 self?.numberOfFiles -= 1
585 }
Kateryna Kostiuk3b581712022-07-21 08:42:14 -0400586 self?.verifyTasksStatus()
kkostiuk74d1ae42021-06-17 11:10:15 -0400587 if let error = error {
588 print("Unable to Add Notification Request (\(error), \(error.localizedDescription))")
589 }
590 }
591 }
592
kkostiuke10e6572022-07-26 15:41:12 -0400593 private func presentCall(info: [AnyHashable: Any]) {
Kateryna Kostiukfd5e6f12022-08-02 11:24:05 -0400594 CXProvider.reportNewIncomingVoIPPushPayload(info, completion: { error in
595 print("NotificationService", "Did report voip notification, error: \(String(describing: error))")
596 })
kkostiukabde7b92022-07-28 13:15:56 -0400597 self.pendingCalls.removeAll()
598 self.pendingLocalNotifications.removeAll()
kkostiuke10e6572022-07-26 15:41:12 -0400599 self.verifyTasksStatus()
kkostiuk74d1ae42021-06-17 11:10:15 -0400600 }
601}