blob: 92855e9768ac54f75d8bc57e10cad563d1b52c2d [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
Kateryna Kostiukfd95c422023-05-10 16:39:12 -040064 var waitForCloning = false
kkostiuk74d1ae42021-06-17 11:10:15 -040065 private let tasksGroup = DispatchGroup()
Kateryna Kostiuk19437652022-08-02 13:02:21 -040066 var accountId = ""
Kateryna Kostiuk1213bbd2023-05-25 14:12:44 -040067 let thumbnailSize = 100
kkostiuke10e6572022-07-26 15:41:12 -040068
69 typealias LocalNotification = (content: UNMutableNotificationContent, type: LocalNotificationType)
70
71 private var pendingLocalNotifications = [String: [LocalNotification]]() /// local notification waiting for name lookup
72 private var pendingCalls = [String: [AnyHashable: Any]]() /// calls waiting for name lookup
73 private var names = [String: String]() /// map of peerId and best name
kkostiuk74d1ae42021-06-17 11:10:15 -040074 // swiftlint:disable cyclomatic_complexity
75 override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
76 self.contentHandler = contentHandler
77 defer {
78 finish()
79 }
80 let requestData = requestToDictionary(request: request)
81 if requestData.isEmpty {
82 return
83 }
84
85 /// if main app is active extension should save notification data and let app handle notification
86 saveData(data: requestData)
87 if appIsActive() {
88 return
89 }
90
Kateryna Kostiuk19437652022-08-02 13:02:21 -040091 guard let account = requestData[NotificationField.accountId.rawValue] else { return }
92 accountId = account
kkostiuke10e6572022-07-26 15:41:12 -040093
kkostiuk74d1ae42021-06-17 11:10:15 -040094 /// app is not active. Querry value from dht
95 guard let proxyURL = getProxyCaches(data: requestData),
Kateryna Kostiukfb10d702022-11-30 14:32:28 -050096 let url = getRequestURL(data: requestData, path: proxyURL) else {
kkostiuk74d1ae42021-06-17 11:10:15 -040097 return
98 }
99 tasksGroup.enter()
Kateryna Kostiuk0acc5f52022-08-15 10:21:26 -0400100 let defaultSession = URLSession(configuration: .default)
101 let task = defaultSession.dataTask(with: url) {[weak self] (data, _, _) in
kkostiuk74d1ae42021-06-17 11:10:15 -0400102 guard let self = self,
103 let data = data else {
Kateryna Kostiuk6fc15812022-08-12 10:17:34 -0400104 self?.verifyTasksStatus()
kkostiuk74d1ae42021-06-17 11:10:15 -0400105 return
106 }
107 let str = String(decoding: data, as: UTF8.self)
108 let lines = str.split(whereSeparator: \.isNewline)
109 for line in lines {
110 do {
111 guard let jsonData = line.data(using: .utf8),
112 let map = try JSONSerialization.jsonObject(with: jsonData, options: .allowFragments) as? [String: Any],
113 let keyPath = self.getKeyPath(data: requestData),
114 let treatedMessages = self.getTreatedMessagesPath(data: requestData) else {
Kateryna Kostiuk6fc15812022-08-12 10:17:34 -0400115 self.verifyTasksStatus()
116 return
117 }
Kateryna Kostiukba9a7b12023-05-02 09:33:52 -0400118 let result = self.adapterService.decrypt(keyPath: keyPath.path, accountId: self.accountId, messagesPath: treatedMessages.path, value: map)
kkostiuk74d1ae42021-06-17 11:10:15 -0400119 let handleCall: (String, String) -> Void = { [weak self] (peerId, hasVideo) in
120 guard let self = self else {
121 return
122 }
Kateryna Kostiukfd5e6f12022-08-02 11:24:05 -0400123 /// jami will be started. Set accounts to not active state
124 if self.accountIsActive {
125 self.accountIsActive = false
126 self.adapterService.stop()
kkostiuk74d1ae42021-06-17 11:10:15 -0400127 }
Kateryna Kostiukfd5e6f12022-08-02 11:24:05 -0400128 var info = request.content.userInfo
129 info["peerId"] = peerId
130 info["hasVideo"] = hasVideo
Kateryna Kostiuk19437652022-08-02 13:02:21 -0400131 let name = self.bestName(accountId: self.accountId, contactId: peerId)
Kateryna Kostiukfd5e6f12022-08-02 11:24:05 -0400132 if name.isEmpty {
133 info["displayName"] = peerId
134 self.pendingCalls[peerId] = info
Kateryna Kostiuk19437652022-08-02 13:02:21 -0400135 self.startAddressLookup(address: peerId, accountId: self.accountId)
Kateryna Kostiukfd5e6f12022-08-02 11:24:05 -0400136 return
137 }
138 info["displayName"] = name
139 self.presentCall(info: info)
kkostiuk74d1ae42021-06-17 11:10:15 -0400140 }
Kateryna Kostiuk28e72e02023-02-01 16:31:39 -0500141 switch result {
142 case .call(let peerId, let hasVideo):
143 handleCall(peerId, "\(hasVideo)")
144 return
145 case .gitMessage:
Kateryna Kostiuk3dc364c2023-03-31 12:02:31 -0400146 self.handleGitMessage()
Kateryna Kostiukfd95c422023-05-10 16:39:12 -0400147 case .clone:
148 // Should start daemon and wait until clone completed
149 self.waitForCloning = true
150 self.handleGitMessage()
Kateryna Kostiuk28e72e02023-02-01 16:31:39 -0500151 case .unknown:
152 break
153 }
kkostiuk74d1ae42021-06-17 11:10:15 -0400154 } catch {
155 print("serialization failed , \(error)")
156 }
157 }
158 self.verifyTasksStatus()
159 }
160 task.resume()
161 _ = tasksGroup.wait(timeout: .now() + notificationTimeout)
162 }
163
164 override func serviceExtensionTimeWillExpire() {
Kateryna Kostiuk97193412023-04-11 11:06:14 -0400165 if !self.tasksCompleted {
166 self.tasksCompleted = true
167 self.tasksGroup.leave()
168 }
kkostiuk74d1ae42021-06-17 11:10:15 -0400169 finish()
170 }
171
Kateryna Kostiuk3dc364c2023-03-31 12:02:31 -0400172 private func handleGitMessage() {
Kateryna Kostiuk28e72e02023-02-01 16:31:39 -0500173 /// check if account already acive
174 guard !self.accountIsActive else { return }
175 self.accountIsActive = true
176 self.adapterService.startAccountsWithListener(accountId: self.accountId) { [weak self] event, eventData in
177 guard let self = self else {
178 return
Alireza8154e1f2022-11-26 16:51:24 -0500179 }
Kateryna Kostiuk28e72e02023-02-01 16:31:39 -0500180 switch event {
181 case .message:
Kateryna Kostiukc24e1042023-05-23 11:11:43 -0400182 self.conversationUpdated(conversationId: eventData.conversationId, accountId: self.accountId)
Kateryna Kostiuk28e72e02023-02-01 16:31:39 -0500183 self.numberOfMessages += 1
184 self.configureMessageNotification(from: eventData.jamiId, body: eventData.content, accountId: self.accountId, conversationId: eventData.conversationId, groupTitle: "")
185 case .fileTransferDone:
Kateryna Kostiukc24e1042023-05-23 11:11:43 -0400186 self.conversationUpdated(conversationId: eventData.conversationId, accountId: self.accountId)
Kateryna Kostiuk28e72e02023-02-01 16:31:39 -0500187 if let url = URL(string: eventData.content) {
188 self.configureFileNotification(from: eventData.jamiId, url: url, accountId: self.accountId, conversationId: eventData.conversationId)
189 } else {
190 self.numberOfFiles -= 1
191 self.verifyTasksStatus()
192 }
193 case .syncCompleted:
194 self.syncCompleted = true
195 self.verifyTasksStatus()
196 case .fileTransferInProgress:
197 self.numberOfFiles += 1
Kateryna Kostiuk28e72e02023-02-01 16:31:39 -0500198 case .invitation:
Kateryna Kostiukc24e1042023-05-23 11:11:43 -0400199 self.conversationUpdated(conversationId: eventData.conversationId, accountId: self.accountId)
Kateryna Kostiuk28e72e02023-02-01 16:31:39 -0500200 self.syncCompleted = true
201 self.numberOfMessages += 1
202 self.configureMessageNotification(from: eventData.jamiId,
203 body: eventData.content,
204 accountId: self.accountId,
205 conversationId: eventData.conversationId,
206 groupTitle: eventData.groupTitle)
Kateryna Kostiukfd95c422023-05-10 16:39:12 -0400207 case .conversationCloned:
208 self.waitForCloning = false
209 self.verifyTasksStatus()
Kateryna Kostiuk28e72e02023-02-01 16:31:39 -0500210 }
Alireza8154e1f2022-11-26 16:51:24 -0500211 }
212 }
213
kkostiuk74d1ae42021-06-17 11:10:15 -0400214 private func verifyTasksStatus() {
215 guard !self.tasksCompleted else { return } /// we already left taskGroup
kkostiuke10e6572022-07-26 15:41:12 -0400216 /// waiting for lookup
217 if !pendingCalls.isEmpty || !pendingLocalNotifications.isEmpty {
218 return
219 }
kkostiuk74d1ae42021-06-17 11:10:15 -0400220 /// We could finish in two cases:
221 /// 1. we did not start account we are not waiting for the signals from the daemon
222 /// 2. conversation synchronization completed and all files downloaded
Kateryna Kostiukfd95c422023-05-10 16:39:12 -0400223 if !self.accountIsActive || (self.syncCompleted && self.numberOfFiles == 0 && self.numberOfMessages == 0 && !self.waitForCloning) {
kkostiuk74d1ae42021-06-17 11:10:15 -0400224 self.tasksCompleted = true
225 self.tasksGroup.leave()
226 }
227 }
228
229 private func finish() {
230 if self.accountIsActive {
231 self.accountIsActive = false
232 self.adapterService.stop()
Kateryna Kostiuk84834422023-04-11 10:53:56 -0400233 } else {
234 self.adapterService.removeDelegate()
kkostiuk74d1ae42021-06-17 11:10:15 -0400235 }
kkostiuke10e6572022-07-26 15:41:12 -0400236 /// cleanup pending notifications
237 if !self.pendingCalls.isEmpty, let info = self.pendingCalls.first?.value {
238 self.presentCall(info: info)
239 } else {
240 for notifications in pendingLocalNotifications {
241 for notification in notifications.value {
242 self.presentLocalNotification(notification: notification)
243 }
244 }
kkostiukabde7b92022-07-28 13:15:56 -0400245 pendingLocalNotifications.removeAll()
kkostiuke10e6572022-07-26 15:41:12 -0400246 }
kkostiuk74d1ae42021-06-17 11:10:15 -0400247 if let contentHandler = contentHandler {
248 contentHandler(self.bestAttemptContent)
249 }
250 }
251
252 private func appIsActive() -> Bool {
253 let group = DispatchGroup()
254 defer {
255 self.removeObserver()
256 group.leave()
257 }
258 var appIsActive = false
259 group.enter()
260 /// post darwin notification and wait for the answer from the main app. If answer received app is active
261 self.listenToMainAppResponse { _ in
262 appIsActive = true
263 }
264 CFNotificationCenterPostNotification(notificationCenter, CFNotificationName(Constants.notificationReceived), nil, nil, true)
265 /// wait fro 100 milliseconds. If no answer from main app is received app is not active.
266 _ = group.wait(timeout: .now() + 0.3)
267
268 return appIsActive
269 }
270
271 private func saveData(data: [String: String]) {
272 guard let userDefaults = UserDefaults(suiteName: Constants.appGroupIdentifier) else {
273 return
274 }
275 var notificationData = [[String: String]]()
276 if let existingData = userDefaults.object(forKey: Constants.notificationData) as? [[String: String]] {
277 notificationData = existingData
278 }
279 notificationData.append(data)
280 userDefaults.set(notificationData, forKey: Constants.notificationData)
281 }
282
Kateryna Kostiukc24e1042023-05-23 11:11:43 -0400283 private func conversationUpdated(conversationId: String, accountId: String) {
284 var conversationData = [String: String]()
285 conversationData[Constants.NotificationUserInfoKeys.conversationID.rawValue] = conversationId
286 conversationData[Constants.NotificationUserInfoKeys.accountID.rawValue] = accountId
287 self.setUpdatedConversations(conversation: conversationData)
288 }
289
290 private func setUpdatedConversations(conversation: [String: String]) {
291 /*
292 Save updated conversations so they can be reloaded when Jami
293 becomes active.
294 */
295 guard let userDefaults = UserDefaults(suiteName: Constants.appGroupIdentifier) else {
296 return
297 }
298 var conversationData = [[String: String]]()
299 if let existingData = userDefaults.object(forKey: Constants.updatedConversations) as? [[String: String]] {
300 conversationData = existingData
301 }
302 for data in conversationData
303 where data[Constants.NotificationUserInfoKeys.conversationID.rawValue] ==
304 conversation[Constants.NotificationUserInfoKeys.conversationID.rawValue] {
305 return
306 }
307
308 conversationData.append(conversation)
309 userDefaults.set(conversationData, forKey: Constants.updatedConversations)
310 }
311
kkostiuk74d1ae42021-06-17 11:10:15 -0400312 private func setNotificationCount(notification: UNMutableNotificationContent) {
313 guard let userDefaults = UserDefaults(suiteName: Constants.appGroupIdentifier) else {
314 return
315 }
316
317 if let count = userDefaults.object(forKey: Constants.notificationsCount) as? NSNumber {
318 let new: NSNumber = count.intValue + 1 as NSNumber
319 notification.badge = new
320 userDefaults.set(new, forKey: Constants.notificationsCount)
321 }
322 }
323
324 private func requestToDictionary(request: UNNotificationRequest) -> [String: String] {
325 var dictionary = [String: String]()
326 let userInfo = request.content.userInfo
327 for key in userInfo.keys {
328 /// "aps" is a field added for alert notification type, so it could be received in the extension. This field is not needed by dht
329 if String(describing: key) == NotificationField.aps.rawValue {
330 continue
331 }
332 if let value = userInfo[key] {
333 let keyString = String(describing: key)
334 let valueString = String(describing: value)
335 dictionary[keyString] = valueString
336 }
337 }
338 return dictionary
339 }
340
341 private func requestedData(request: UNNotificationRequest, map: [String: Any]) -> Bool {
342 guard let userInfo = request.content.userInfo as? [String: Any] else { return false }
343 guard let valueIds = userInfo["valueIds"] as? [String: String],
344 let id = map["id"] else {
345 return false
346 }
347 return valueIds.values.contains("\(id)")
348 }
kkostiuke10e6572022-07-26 15:41:12 -0400349
350 private func bestName(accountId: String, contactId: String) -> String {
351 if let name = self.names[contactId], !name.isEmpty {
352 return name
353 }
354 if let contactProfileName = self.contactProfileName(accountId: accountId, contactId: contactId),
355 !contactProfileName.isEmpty {
356 self.names[contactId] = contactProfileName
357 return contactProfileName
358 }
359 let registeredName = self.adapterService.getNameFor(address: contactId, accountId: accountId)
360 if !registeredName.isEmpty {
361 self.names[contactId] = registeredName
362 }
363 return registeredName
364 }
365
366 private func startAddressLookup(address: String, accountId: String) {
367 var nameServer = self.adapterService.getNameServerFor(accountId: accountId)
Kateryna Kostiuk1c0e7562022-08-23 15:17:44 -0400368 nameServer = ensureURLPrefix(urlString: nameServer)
kkostiuke10e6572022-07-26 15:41:12 -0400369 let urlString = nameServer + "/addr/" + address
Kateryna Kostiuk6fc15812022-08-12 10:17:34 -0400370 guard let url = URL(string: urlString) else {
371 self.lookupCompleted(address: address, name: nil)
372 return
373 }
Kateryna Kostiuk0acc5f52022-08-15 10:21:26 -0400374 let defaultSession = URLSession(configuration: .default)
375 let task = defaultSession.dataTask(with: url) {[weak self](data, response, _) in
kkostiuke10e6572022-07-26 15:41:12 -0400376 guard let self = self else { return }
377 var name: String?
378 defer {
379 self.lookupCompleted(address: address, name: name)
380 }
381 guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200,
382 let data = data else {
383 return
384 }
385 do {
386 guard let map = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: String] else { return }
387 if map["name"] != nil {
388 name = map["name"]
389 self.names[address] = name
390 }
391 } catch {
392 print("serialization failed , \(error)")
393 }
394 }
395 task.resume()
396 }
397
Kateryna Kostiuk1c0e7562022-08-23 15:17:44 -0400398 private func ensureURLPrefix(urlString: String) -> String {
399 var urlWithPrefix = urlString
400 if !urlWithPrefix.hasPrefix("http://") && !urlWithPrefix.hasPrefix("https://") {
401 urlWithPrefix = "http://" + urlWithPrefix
402 }
403 return urlWithPrefix
404 }
405
kkostiuke10e6572022-07-26 15:41:12 -0400406 private func lookupCompleted(address: String, name: String?) {
407 for call in pendingCalls where call.key == address {
408 var info = call.value
409 if let name = name {
410 info["displayName"] = name
411 }
412 presentCall(info: info)
kkostiuke10e6572022-07-26 15:41:12 -0400413 return
414 }
415 for pending in pendingLocalNotifications where pending.key == address {
416 let notifications = pending.value
417 for notification in notifications {
418 if let name = name {
419 notification.content.title = name
420 }
421 presentLocalNotification(notification: notification)
422 }
423 pendingLocalNotifications.removeValue(forKey: address)
424 }
425 }
426
427 private func needUpdateNotification(notification: LocalNotification, peerId: String, accountId: String) {
428 if var pending = pendingLocalNotifications[peerId] {
429 pending.append(notification)
430 pendingLocalNotifications[peerId] = pending
431 } else {
432 pendingLocalNotifications[peerId] = [notification]
433 }
434 startAddressLookup(address: peerId, accountId: accountId)
435 }
kkostiuk74d1ae42021-06-17 11:10:15 -0400436}
437// MARK: paths
438extension NotificationService {
439
440 private func getRequestURL(data: [String: String], proxyURL: URL) -> URL? {
441 guard let key = data[NotificationField.key.rawValue] else {
442 return nil
443 }
444 return proxyURL.appendingPathComponent(key)
445 }
446
Kateryna Kostiukfb10d702022-11-30 14:32:28 -0500447 private func getRequestURL(data: [String: String], path: URL) -> URL? {
448 guard let key = data[NotificationField.key.rawValue],
449 let jsonData = NSData(contentsOf: path) as? Data else {
450 return nil
451 }
452 guard let map = try? JSONSerialization.jsonObject(with: jsonData, options: .allowFragments) as? [String: String],
453 var proxyAddress = map.first?.value else {
454 return nil
455 }
456
457 proxyAddress = ensureURLPrefix(urlString: proxyAddress)
458 guard let urlPrpxy = URL(string: proxyAddress) else { return nil }
459 return urlPrpxy.appendingPathComponent(key)
460 }
461
kkostiuk74d1ae42021-06-17 11:10:15 -0400462 private func getKeyPath(data: [String: String]) -> URL? {
463 guard let documentsPath = Constants.documentsPath,
464 let accountId = data[NotificationField.accountId.rawValue] else {
465 return nil
466 }
467 return documentsPath.appendingPathComponent(accountId).appendingPathComponent("ring_device.key")
468 }
469
470 private func getTreatedMessagesPath(data: [String: String]) -> URL? {
471 guard let cachesPath = Constants.cachesPath,
472 let accountId = data[NotificationField.accountId.rawValue] else {
473 return nil
474 }
475 return cachesPath.appendingPathComponent(accountId).appendingPathComponent("treatedMessages")
476 }
477
478 private func getProxyCaches(data: [String: String]) -> URL? {
479 guard let cachesPath = Constants.cachesPath,
480 let accountId = data[NotificationField.accountId.rawValue] else {
481 return nil
482 }
483 return cachesPath.appendingPathComponent(accountId).appendingPathComponent("dhtproxy")
484 }
kkostiuke10e6572022-07-26 15:41:12 -0400485
486 private func contactProfileName(accountId: String, contactId: String) -> String? {
487 guard let documents = Constants.documentsPath else { return nil }
Kateryna Kostiukc664c6b2023-05-26 12:10:32 -0400488 let uri = "ring:" + contactId
489 let path = documents.path + "/" + "\(accountId)" + "/profiles/" + "\(Data(uri.utf8).base64EncodedString()).vcf"
490 if !FileManager.default.fileExists(atPath: path) { return nil }
kkostiuke10e6572022-07-26 15:41:12 -0400491
Kateryna Kostiukc664c6b2023-05-26 12:10:32 -0400492 return VCardUtils.getNameFromVCard(filePath: path)
kkostiuke10e6572022-07-26 15:41:12 -0400493 }
kkostiuk74d1ae42021-06-17 11:10:15 -0400494}
495
496// MARK: DarwinNotificationHandler
497extension NotificationService: DarwinNotificationHandler {
498 func listenToMainAppResponse(completion: @escaping (Bool) -> Void) {
499 let observer = Unmanaged.passUnretained(self).toOpaque()
500 CFNotificationCenterAddObserver(notificationCenter,
501 observer, { (_, _, _, _, _) in
kkostiuke10e6572022-07-26 15:41:12 -0400502 NotificationCenter.default.post(name: NotificationService.localNotificationName,
kkostiuk74d1ae42021-06-17 11:10:15 -0400503 object: nil,
504 userInfo: nil)
505 },
506 Constants.notificationAppIsActive,
507 nil,
508 .deliverImmediately)
kkostiuke10e6572022-07-26 15:41:12 -0400509 NotificationCenter.default.addObserver(forName: NotificationService.localNotificationName, object: nil, queue: nil) { _ in
kkostiuk74d1ae42021-06-17 11:10:15 -0400510 completion(true)
511 }
512 }
513
514 func removeObserver() {
515 let observer = Unmanaged.passUnretained(self).toOpaque()
516 CFNotificationCenterRemoveEveryObserver(notificationCenter, observer)
kkostiuke10e6572022-07-26 15:41:12 -0400517 NotificationCenter.default.removeObserver(self, name: NotificationService.localNotificationName, object: nil)
kkostiuk74d1ae42021-06-17 11:10:15 -0400518 }
519
520}
521
522// MARK: present notifications
523extension NotificationService {
524 private func createAttachment(identifier: String, image: UIImage, options: [NSObject: AnyObject]?) -> UNNotificationAttachment? {
525 let fileManager = FileManager.default
526 let tmpSubFolderName = ProcessInfo.processInfo.globallyUniqueString
527 let tmpSubFolderURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(tmpSubFolderName, isDirectory: true)
528 do {
529 try fileManager.createDirectory(at: tmpSubFolderURL, withIntermediateDirectories: true, attributes: nil)
530 let imageFileIdentifier = identifier
531 let fileURL = tmpSubFolderURL.appendingPathComponent(imageFileIdentifier)
Kateryna Kostiuk1213bbd2023-05-25 14:12:44 -0400532 let imageData = image.jpegData(compressionQuality: 0.7)
533 try imageData?.write(to: fileURL)
kkostiuk74d1ae42021-06-17 11:10:15 -0400534 let imageAttachment = try UNNotificationAttachment.init(identifier: identifier, url: fileURL, options: options)
535 return imageAttachment
536 } catch {}
537 return nil
538 }
539
Kateryna Kostiuk1213bbd2023-05-25 14:12:44 -0400540 func createThumbnailImage(fileURLString: String) -> UIImage? {
541 guard let escapedPath = fileURLString.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else {
542 return nil
543 }
544
545 // Construct the file URL with the correct scheme and path
546 guard let fileURL = URL(string: "file://" + escapedPath) else {
547 return nil
548 }
549
550 let size = CGSize(width: thumbnailSize, height: thumbnailSize)
551
552 guard let imageSource = CGImageSourceCreateWithURL(fileURL as CFURL, nil),
553 let imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [CFString: Any],
554 let pixelWidth = imageProperties[kCGImagePropertyPixelWidth] as? Int,
555 let pixelHeight = imageProperties[kCGImagePropertyPixelHeight] as? Int,
556 let downsampledImage = createDownsampledImage(imageSource: imageSource,
557 targetSize: size,
558 pixelWidth: pixelWidth,
559 pixelHeight: pixelHeight) else {
560 return nil
561 }
562 return UIImage(cgImage: downsampledImage)
563 }
564
565 func createDownsampledImage(imageSource: CGImageSource, targetSize: CGSize, pixelWidth: Int, pixelHeight: Int) -> CGImage? {
566 let maxDimension = max(targetSize.width, targetSize.height)
567 let options: [CFString: Any] = [
568 kCGImageSourceThumbnailMaxPixelSize: maxDimension,
569 kCGImageSourceCreateThumbnailFromImageAlways: true
570 ]
571
572 return CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary)
573 }
574
kkostiuk8e397cd2022-07-27 15:08:10 -0400575 private func configureFileNotification(from: String, url: URL, accountId: String, conversationId: String) {
kkostiuk74d1ae42021-06-17 11:10:15 -0400576 let content = UNMutableNotificationContent()
kkostiuke10e6572022-07-26 15:41:12 -0400577 content.sound = UNNotificationSound.default
kkostiuk74d1ae42021-06-17 11:10:15 -0400578 let imageName = url.lastPathComponent
kkostiuk74d1ae42021-06-17 11:10:15 -0400579 content.body = imageName
kkostiuk8e397cd2022-07-27 15:08:10 -0400580 var data = [String: String]()
581 data[Constants.NotificationUserInfoKeys.participantID.rawValue] = from
582 data[Constants.NotificationUserInfoKeys.accountID.rawValue] = accountId
583 data[Constants.NotificationUserInfoKeys.conversationID.rawValue] = conversationId
584 content.userInfo = data
Kateryna Kostiuk1213bbd2023-05-25 14:12:44 -0400585 if let image = createThumbnailImage(fileURLString: url.path), let attachement = createAttachment(identifier: imageName, image: image, options: nil) {
kkostiuk74d1ae42021-06-17 11:10:15 -0400586 content.attachments = [ attachement ]
587 }
kkostiuke10e6572022-07-26 15:41:12 -0400588 let title = self.bestName(accountId: accountId, contactId: from)
589 if title.isEmpty {
590 content.title = from
591 needUpdateNotification(notification: LocalNotification(content, .file), peerId: from, accountId: accountId)
592 } else {
593 content.title = title
594 presentLocalNotification(notification: LocalNotification(content, .file))
595 }
596 }
597
Kateryna Kostiuk2648aab2022-08-30 14:24:03 -0400598 private func configureMessageNotification(from: String, body: String, accountId: String, conversationId: String, groupTitle: String) {
kkostiuke10e6572022-07-26 15:41:12 -0400599 let content = UNMutableNotificationContent()
600 content.body = body
kkostiuk74d1ae42021-06-17 11:10:15 -0400601 content.sound = UNNotificationSound.default
kkostiuk8e397cd2022-07-27 15:08:10 -0400602 var data = [String: String]()
603 data[Constants.NotificationUserInfoKeys.participantID.rawValue] = from
604 data[Constants.NotificationUserInfoKeys.accountID.rawValue] = accountId
605 data[Constants.NotificationUserInfoKeys.conversationID.rawValue] = conversationId
606 content.userInfo = data
Kateryna Kostiuk2648aab2022-08-30 14:24:03 -0400607 let title = !groupTitle.isEmpty ? groupTitle : self.bestName(accountId: accountId, contactId: from)
kkostiuke10e6572022-07-26 15:41:12 -0400608 if title.isEmpty {
609 content.title = from
610 needUpdateNotification(notification: LocalNotification(content, .message), peerId: from, accountId: accountId)
611 } else {
612 content.title = title
613 presentLocalNotification(notification: LocalNotification(content, .message))
614 }
615 }
616
617 private func presentLocalNotification(notification: LocalNotification) {
618 let content = notification.content
kkostiuk74d1ae42021-06-17 11:10:15 -0400619 setNotificationCount(notification: content)
620 let notificationTrigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.01, repeats: false)
Kateryna Kostiuk3b581712022-07-21 08:42:14 -0400621 let notificationRequest = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: notificationTrigger)
622 UNUserNotificationCenter.current().add(notificationRequest) { [weak self] (error) in
kkostiuke10e6572022-07-26 15:41:12 -0400623 if notification.type == .message {
624 self?.numberOfMessages -= 1
625 } else {
626 self?.numberOfFiles -= 1
627 }
Kateryna Kostiuk3b581712022-07-21 08:42:14 -0400628 self?.verifyTasksStatus()
kkostiuk74d1ae42021-06-17 11:10:15 -0400629 if let error = error {
630 print("Unable to Add Notification Request (\(error), \(error.localizedDescription))")
631 }
632 }
633 }
634
kkostiuke10e6572022-07-26 15:41:12 -0400635 private func presentCall(info: [AnyHashable: Any]) {
Kateryna Kostiukfd5e6f12022-08-02 11:24:05 -0400636 CXProvider.reportNewIncomingVoIPPushPayload(info, completion: { error in
637 print("NotificationService", "Did report voip notification, error: \(String(describing: error))")
638 })
kkostiukabde7b92022-07-28 13:15:56 -0400639 self.pendingCalls.removeAll()
640 self.pendingLocalNotifications.removeAll()
kkostiuke10e6572022-07-26 15:41:12 -0400641 self.verifyTasksStatus()
kkostiuk74d1ae42021-06-17 11:10:15 -0400642 }
643}