diff --git a/.gitignore b/.gitignore
index c6fe5d3f7d402e7f930e53f8d4f913eb6c1e0f54..c383b719517f0288dbad05686f7ab48e68364bdb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,3 +24,6 @@ Ring/Build/
 
 # Carthage
 Ring/Carthage/
+
+# VSCode
+.vscode/
\ No newline at end of file
diff --git a/Ring/Ring.xcodeproj/project.pbxproj b/Ring/Ring.xcodeproj/project.pbxproj
index bb2d27118db5383a7f823029e904e9f1213789dd..a2c4d9f9f1ea229771b87446260469d7e5b9df48 100644
--- a/Ring/Ring.xcodeproj/project.pbxproj
+++ b/Ring/Ring.xcodeproj/project.pbxproj
@@ -457,6 +457,7 @@
 		5CE66F761FBF769B00EE9291 /* InitialLoadingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CE66F741FBF769B00EE9291 /* InitialLoadingViewController.swift */; };
 		62006E04203F4DD6003C3197 /* UITextField+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62006E03203F4DD6003C3197 /* UITextField+Helpers.swift */; };
 		621231F91F880EDF009B86F0 /* UILabel+Ring.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621231F81F880EDF009B86F0 /* UILabel+Ring.swift */; };
+		623196862BAE498900C2252C /* Atomics in Frameworks */ = {isa = PBXBuildFile; productRef = 623196852BAE498900C2252C /* Atomics */; };
 		623660AA20092081002598C1 /* src in Resources */ = {isa = PBXBuildFile; fileRef = 623660A920092081002598C1 /* src */; };
 		627F11F120348FBF006560B5 /* AvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 627F11F020348FBF006560B5 /* AvatarView.swift */; };
 		62A88D371F6C2ED400F8AB18 /* PresenceAdapterDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62A88D361F6C2ED400F8AB18 /* PresenceAdapterDelegate.swift */; };
@@ -1286,6 +1287,7 @@
 				269DA02E28D0D2E5007D51D6 /* libgmp.a in Frameworks */,
 				269DA07F28D0D5D0007D51D6 /* libgit2.a in Frameworks */,
 				269DA02B28D0D2E5007D51D6 /* libavformat.a in Frameworks */,
+				623196862BAE498900C2252C /* Atomics in Frameworks */,
 				26A88C7826700B7500888EED /* libc++.tbd in Frameworks */,
 				269DA03E28D0D2E5007D51D6 /* libpjsip-simple.a in Frameworks */,
 				26CA4C592AB23C4100AEF8F5 /* libixml.a in Frameworks */,
@@ -2574,6 +2576,9 @@
 			dependencies = (
 			);
 			name = jamiNotificationExtension;
+			packageProductDependencies = (
+				623196852BAE498900C2252C /* Atomics */,
+			);
 			productName = jamiNotificationExtension;
 			productReference = 26A88C04266FFFC800888EED /* jamiNotificationExtension.appex */;
 			productType = "com.apple.product-type.app-extension";
@@ -2692,6 +2697,7 @@
 			mainGroup = 043999EA1D1C2D9D00E99CD9;
 			packageReferences = (
 				5593D7D12B7FE00A00DA109C /* XCRemoteSwiftPackageReference "MCEmojiPicker" */,
+				623196842BAE498900C2252C /* XCRemoteSwiftPackageReference "swift-atomics" */,
 			);
 			productRefGroup = 043999F41D1C2D9D00E99CD9 /* Products */;
 			projectDirPath = "";
@@ -4140,6 +4146,14 @@
 				minimumVersion = 1.2.3;
 			};
 		};
+		623196842BAE498900C2252C /* XCRemoteSwiftPackageReference "swift-atomics" */ = {
+			isa = XCRemoteSwiftPackageReference;
+			repositoryURL = "https://github.com/apple/swift-atomics.git";
+			requirement = {
+				kind = upToNextMajorVersion;
+				minimumVersion = 1.2.0;
+			};
+		};
 /* End XCRemoteSwiftPackageReference section */
 
 /* Begin XCSwiftPackageProductDependency section */
@@ -4148,6 +4162,11 @@
 			package = 5593D7D12B7FE00A00DA109C /* XCRemoteSwiftPackageReference "MCEmojiPicker" */;
 			productName = MCEmojiPicker;
 		};
+		623196852BAE498900C2252C /* Atomics */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 623196842BAE498900C2252C /* XCRemoteSwiftPackageReference "swift-atomics" */;
+			productName = Atomics;
+		};
 /* End XCSwiftPackageProductDependency section */
 	};
 	rootObject = 043999EB1D1C2D9D00E99CD9 /* Project object */;
diff --git a/Ring/Ring/Bridging/DRingAdapter.mm b/Ring/Ring/Bridging/DRingAdapter.mm
index a7851b542e84419d1b41555d819a67e71c3bc4d6..59a7dd75fa36352e3e182e5a6f1f3289ea5ebd45 100644
--- a/Ring/Ring/Bridging/DRingAdapter.mm
+++ b/Ring/Ring/Bridging/DRingAdapter.mm
@@ -45,7 +45,7 @@ using namespace libjami;
 
 - (BOOL) initDaemonInternal {
 #if DEBUG
-    int flag = LIBJAMI_FLAG_CONSOLE_LOG | LIBJAMI_FLAG_DEBUG;
+    int flag = 0;
 #else
     int flag = 0;
 #endif
diff --git a/Ring/Ring/Constants/Constants.swift b/Ring/Ring/Constants/Constants.swift
index a2fd08bb4b4de4edaa5a952738332a79203cd936..d458c337ed678205313332ec2dacdef7296ac6d0 100644
--- a/Ring/Ring/Constants/Constants.swift
+++ b/Ring/Ring/Constants/Constants.swift
@@ -106,6 +106,7 @@ public class Constants: NSObject {
     @objc public static let updatedConversations = "updatedConversations"
     @objc public static let appGroupIdentifier = "group.com.savoirfairelinux.ring"
     @objc public static let notificationsCount = "notificationsCount"
+    @objc public static let appIdentifier = "com.savoirfairelinux.jami"
 
     @objc public static let documentsPath: URL? = {
         return FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier)?.appendingPathComponent("Documents")
diff --git a/Ring/jamiNotificationExtension/Adapter.mm b/Ring/jamiNotificationExtension/Adapter.mm
index 968341e7f4351cd98918a0deb11331f9e48cc96a..837151e23915238b13ca0cf55cdab1c0d2ff66dd 100644
--- a/Ring/jamiNotificationExtension/Adapter.mm
+++ b/Ring/jamiNotificationExtension/Adapter.mm
@@ -179,10 +179,9 @@ std::map<std::string, std::string> nameServers;
         loadAccountAndConversation(std::string([accountId UTF8String]), loadAll, std::string([convId UTF8String]));
         return true;
     }
-#if DEBUG
-    int flag = LIBJAMI_FLAG_CONSOLE_LOG | LIBJAMI_FLAG_DEBUG | LIBJAMI_FLAG_IOS_EXTENSION | LIBJAMI_FLAG_NO_AUTOSYNC | LIBJAMI_FLAG_NO_LOCAL_AUDIO | LIBJAMI_FLAG_NO_AUTOLOAD;
-#else
     int flag = LIBJAMI_FLAG_IOS_EXTENSION | LIBJAMI_FLAG_NO_AUTOSYNC | LIBJAMI_FLAG_NO_LOCAL_AUDIO | LIBJAMI_FLAG_NO_AUTOLOAD;
+#if DEBUG
+    flag |= LIBJAMI_FLAG_CONSOLE_LOG | LIBJAMI_FLAG_DEBUG;
 #endif
     if (![[NSThread currentThread] isMainThread]) {
         __block bool success;
@@ -236,6 +235,7 @@ std::map<std::string, std::string> nameServers;
         return {};
     }
 
+    // Build the key using the key path argument
     NSData* data = [[NSFileManager defaultManager] contentsAtPath:keyPath];
     const uint8_t* bytes = (const uint8_t*) [data bytes];
     dht::crypto::PrivateKey dhtKey(bytes, [data length], "");
diff --git a/Ring/jamiNotificationExtension/AdapterService.swift b/Ring/jamiNotificationExtension/AdapterService.swift
index 888d32554558bd0da577d8bdf6c16679aef87bf2..e6db3418964f63d02d00f23bb28417d32e274257 100644
--- a/Ring/jamiNotificationExtension/AdapterService.swift
+++ b/Ring/jamiNotificationExtension/AdapterService.swift
@@ -108,6 +108,9 @@ class AdapterService {
     }
 
     func decrypt(keyPath: String, accountId: String, messagesPath: String, value: [String: Any]) -> PeerConnectionRequestType {
+        if self.adapter == nil {
+            return .unknown
+        }
         let result = adapter.decrypt(keyPath, accountId: accountId, treated: messagesPath, value: value)
         guard let peerId = result?.keys.first,
               let type = result?.values.first else {
diff --git a/Ring/jamiNotificationExtension/NotificationService.swift b/Ring/jamiNotificationExtension/NotificationService.swift
index 32f3233b33fb57dd8a30be01b2b3266b9d5729ec..95e269438e252ea3fa87a29e9e769612be3fc567 100644
--- a/Ring/jamiNotificationExtension/NotificationService.swift
+++ b/Ring/jamiNotificationExtension/NotificationService.swift
@@ -1,5 +1,5 @@
 /*
- *  Copyright (C) 2021-2022 Savoir-faire Linux Inc.
+ *  Copyright (C) 2021-2024 Savoir-faire Linux Inc.
  *
  *  Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
  *
@@ -26,6 +26,41 @@ import CoreFoundation
 import os
 import Darwin
 import Contacts
+import RxSwift
+import Atomics
+
+/*
+ * This class is responsible for handling incoming notifications from the DHT proxy server.
+ * The steps are as follows:
+ *  1. The notification request is received as a JSON object and is processed to extract the necessary data
+ *  2. If the main app is active, the notification data is saved and the app is notified to handle synchronization
+ *     instead of the extension
+ *  3. If the main app is not active, the notification data is used to start a data stream from the proxy server
+ *     over HTTP
+ *  4. The data stream is processed line by line, and each line is decrypted and processed
+ *  5. The decrypted data is used to obtain the information needed to determine the action to take
+ *  6. The action is taken, which may involve presenting a local notification or stopping the current backend
+ *     instance and handing off control the foreground app (in the case of an incoming call)
+ *
+ * The class also handles the retrieval of contact names from the name server, which is done asynchronously.
+ * In the case of a name being required, the notification is enqueued and the name is retrieved before the
+ * notification is presented.
+ *
+ * The actions taken based on the notification data are as follows:
+ *  - If the data is a call, the call is presented (the extension can be stopped as the foreground app will take over)
+ *  - If the data is a message, the backend is started (if not already active) and events are parsed until the message
+ *    body is received and enqueue for presentation
+ *  - If the data is a file, the backend is started (if not already active) and events are parsed until the file is
+ *    downloaded and the notification is enqueued for presentation
+ *  - If the data is a clone request, the backend is started and events are parsed until the clone is completed
+ *
+ * Backend event handling is kept alive using a simple reference counting mechanism `itemsToPresent` and `syncCompleted`
+ * which are incremented and decremented as events are processed and completed. When all events are processed and the
+ * clone is completed, the backend is stopped. The HTTP stream is cancelled once all value IDs are processed or the
+ * notification times out (25 seconds).
+ */
+
+// swiftlint:disable file_length
 
 protocol DarwinNotificationHandler {
     func listenToMainAppResponse(completion: @escaping (Bool) -> Void)
@@ -43,131 +78,317 @@ enum LocalNotificationType: String {
     case file
 }
 
-class NotificationService: UNNotificationServiceExtension {
+struct NotificationConfig {
+    let from: String
+    var url: URL?
+    let body: String
+    let conversationId: String
+    let groupTitle: String
+}
 
-    private static let localNotificationName = Notification.Name("com.savoirfairelinux.jami.appActive.internal")
+// A local log helper that prints an easy to see log with the thread info
+func log(_ messages: String...) {
+    let message = messages.joined(separator: " ")
+    print("------ [\(Unmanaged.passUnretained(Thread.current).toOpaque())] \(message)")
+}
 
-    private let notificationTimeout = DispatchTimeInterval.seconds(25)
+// MARK: AutoDispatchGroup helper
+class AutoDispatchGroup {
+    private var taskIds = Set<String>()
+    private let group = DispatchGroup()
+    private let tasksQueue = DispatchQueue(label: Constants.appIdentifier + ".AutoDispatchGroup.queue")
 
-    private let notificationCenter = CFNotificationCenterGetDarwinNotifyCenter()
+    func enter(id: String) {
+        tasksQueue.sync {
+            if taskIds.contains(id) {
+                log("Task with ID \(id) already exists")
+            } else {
+                log("AutoDispatchGroup entering new task: \(id)")
+                taskIds.insert(id)
+                group.enter()
+            }
+        }
+    }
 
-    private var contentHandler: ((UNNotificationContent) -> Void)?
-    private var bestAttemptContent = UNMutableNotificationContent()
+    func leave(id: String) {
+        tasksQueue.sync {
+            guard taskIds.contains(id) else { return }
+            log("AutoDispatchGroup leaving task: \(id)")
+            taskIds.remove(id)
+            group.leave()
+        }
+    }
 
-    private var adapterService: AdapterService = AdapterService(withAdapter: Adapter())
+    func wait(timeout: DispatchTime = .distantFuture) -> DispatchTimeoutResult {
+        group.wait(timeout: timeout)
+    }
+}
+
+class HTTPStreamHandler: NSObject, URLSessionDataDelegate {
+    private lazy var session: URLSession = {
+        let config = URLSessionConfiguration.default
+        config.timeoutIntervalForRequest = 10
+        config.timeoutIntervalForResource = 10
+        config.requestCachePolicy = .reloadIgnoringLocalCacheData
+        return URLSession(configuration: config, delegate: self, delegateQueue: nil)
+    }()
+
+    private var dataBuffer = Data()
+    private var task: URLSessionDataTask?
+    private var subject = PublishSubject<String>()
+    private let taskQueue = DispatchQueue(label: Constants.appIdentifier + ".HTTPStreamHandler.queue")
+    
+    func invalidateAndCancelSession() {
+        session.invalidateAndCancel()
+    }
+
+    private func startTask(url: URL) {
+        taskQueue.sync { [weak self] in
+            guard let self = self else { return }
+            self.task = self.session.dataTask(with: url)
+            log("Starting URL Stream: \(url)")
+            self.task?.resume()
+        }
+    }
+
+    func startStreaming(from url: URL) -> Observable<String> {
+        return subject
+            .do(onSubscribe: { [weak self] in
+                self?.startTask(url: url)
+            })
+            .do(onDispose: { [weak self] in
+                _ = self?.cancelPendingDataTask()
+            })
+    }
+
+    private func cancelPendingDataTask() -> Bool {
+        taskQueue.sync { [weak self] in
+            guard let self = self else { return false }
+            if self.task?.state == .running {
+                self.task?.cancel()
+                return true
+            }
+            return false
+        }
+    }
 
-    private var accountIsActive = false
-    var tasksCompleted = false /// all values from dht parsed, conversation synchronized if needed and files downloaded
-    var numberOfFiles = 0 /// number of files need to be downloaded
-    var numberOfMessages = 0 /// number of scheduled messages
-    var syncCompleted = false
-    var waitForCloning = false
-    private let tasksGroup = DispatchGroup()
-    var accountId = ""
-    let thumbnailSize = 100
+    func cancelStreaming() {
+        if cancelPendingDataTask() {
+            log("Stream handling canceled")
+            subject.onCompleted()
+        }
+    }
 
+    // MARK: URLSessionDataDelegate
+    func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
+        var receivedStrings = [String]()
+        taskQueue.sync { [weak self] in
+            guard let self = self else { return }
+            self.dataBuffer.append(data)
+            while let range = self.dataBuffer.range(of: "\n".data(using: .utf8)!) {
+                let lineData = self.dataBuffer.subdata(in: 0..<range.lowerBound)
+                self.dataBuffer.removeSubrange(0..<range.upperBound)
+                if let lineString = String(data: lineData, encoding: .utf8) {
+                    receivedStrings.append(lineString)
+                }
+            }
+        }
+        for string in receivedStrings {
+            subject.onNext(string)
+        }
+    }
+
+    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
+        if let error = error {
+            self.subject.onError(error)
+        } else {
+            self.subject.onCompleted()
+        }
+    }
+}
+
+// MARK: NotificationService
+class NotificationService: UNNotificationServiceExtension {
     typealias LocalNotification = (content: UNMutableNotificationContent, type: LocalNotificationType)
 
-    private var pendingLocalNotifications = [String: [LocalNotification]]() /// local notification waiting for name lookup
-    private var pendingCalls = [String: [AnyHashable: Any]]() /// calls waiting for name lookup
-    private var names = [String: String]() /// map of peerId and best name
-    // swiftlint:disable cyclomatic_complexity
+    private static let localNotificationName = Notification.Name(Constants.appIdentifier + ".appActive.internal")
+
+    private let notificationTimeout = DispatchTimeInterval.seconds(25)
+    private let notificationCenter = CFNotificationCenterGetDarwinNotifyCenter()
+    private var contentHandler: ((UNNotificationContent) -> Void)?
+    private var bestAttemptContent = UNMutableNotificationContent()
+
+    // All asynchronous tasks are managed using the AutoDispatchGroup which tracks tasks using
+    // IDs. Both the streaming and Jami backend tasks will be waited upon using this group.
+    private let autoDispatchGroup = AutoDispatchGroup()
+    private let httpStreamHandler = HTTPStreamHandler()
+    private let disposeBag = DisposeBag()
+
+    // The following objects are used to manage access to the Jami backend for synchronization
+    private var accountIsActive = ManagedAtomic<Bool>(false)
+    private var accountId: String = ""
+    private var adapterService: AdapterService = AdapterService(withAdapter: Adapter())
+    private var jamiTaskId: String = ""
+    private var idsToProcess: Set<String> = []
+    private var processAll: Bool = false
+    private let taskPropertyQueue = DispatchQueue(label: Constants.appIdentifier + ".TaskProperty.queue")
+    // The following describe scheduled events and will be synchronized with the DispatchQueue
+    private var itemsToPresent = 0
+    private var syncCompleted = false
+    private var waitForCloning = false
+
+    // A queue of pending local notifications, waiting for a name lookup
+    private let notificationQueue = DispatchQueue(label: Constants.appIdentifier + ".Notification.queue")
+    private var pendingLocalNotifications = [String: [LocalNotification]]() // local notification waiting for name lookup
+    private var pendingCalls = [String: [AnyHashable: Any]]() // calls waiting for name lookup
+    private var names = [String: String]() // map of peerId and best name
+    private let thumbnailSize = 100
+    
+    deinit {
+        httpStreamHandler.invalidateAndCancelSession()
+    }
+
+    // Entry point for processing incoming notification requests.
     override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
         self.contentHandler = contentHandler
-        defer {
-            finish()
+
+        Task {
+            self.processNotificationRequest(request)
+            _ = autoDispatchGroup.wait(timeout: .now() + notificationTimeout)
+            self.finish()
         }
+    }
+
+    // Handles the initial processing of the notification request.
+    private func processNotificationRequest(_ request: UNNotificationRequest) {
         let requestData = requestToDictionary(request: request)
-        if requestData.isEmpty {
+        guard !requestData.isEmpty,
+              let accountId = requestData[NotificationField.accountId.rawValue] else {
+            log("There was an error processing the notification request")
             return
         }
 
-        /// if main app is active extension should save notification data and let app handle notification
-        saveData(data: requestData)
-        if appIsActive() {
+        // Save notification data so that if the main app is active, the extension can let the app handle notification
+        saveDataIfNeeded(data: requestData)
+        guard !appIsActive() else {
+            log("App is in foreground")
             return
         }
 
-        guard let account = requestData[NotificationField.accountId.rawValue] else { return }
-        accountId = account
-        if isResubscribe(accountId: accountId, data: requestData) {
+        guard !isResubscribe(accountId: accountId, data: requestData) else {
+            log("This is a resubscribe notification")
             return
         }
-        /// app is not active. Querry value from dht
-        guard let proxyURL = getProxyCaches(data: requestData),
+
+        log("Handling new notification (app in background)")
+
+        self.accountId = accountId
+        prepareAndStartStreaming(for: request, with: requestData)
+    }
+
+    // Prepares for and starts the data stream based on notification data.
+    private func prepareAndStartStreaming(for request: UNNotificationRequest, with requestData: [String: String]) {
+        guard let keyURL = getKeyURL(data: requestData),
+              let treatedMessagesURL = getTreatedMessagesURL(data: requestData),
+              let proxyURL = getProxyCaches(data: requestData),
               let url = getRequestURL(data: requestData, path: proxyURL) else {
             return
         }
-        tasksGroup.enter()
-        let defaultSession = URLSession(configuration: .default)
-        let task = defaultSession.dataTask(with: url) {[weak self] (data, _, _) in
-            guard let self = self,
-                  let data = data else {
-                self?.verifyTasksStatus()
+
+        // Transform the comma-separated ids string
+        if let idsString = requestData["ids"] {
+            self.idsToProcess = Set(idsString.split(separator: ",").map { String($0) })
+        }
+        self.processAll = self.idsToProcess.isEmpty
+
+        startStreaming(from: url, for: request, keyURL: keyURL, treatedMessagesURL: treatedMessagesURL)
+    }
+
+    // Starts streaming data from a specified URL and processes received lines.
+    private func startStreaming(from url: URL, for request: UNNotificationRequest, keyURL: URL, treatedMessagesURL: URL) {
+        let taskId = UUID().uuidString
+        autoDispatchGroup.enter(id: taskId)
+
+        httpStreamHandler.startStreaming(from: url)
+            .subscribe(onNext: { [weak self] line in
+                self?.processStreamLine(line, with: request, keyURL: keyURL, treatedMessagesURL: treatedMessagesURL)
+            }, onError: { [weak self] error in
+                log("Error streaming data: \(error)")
+                self?.autoDispatchGroup.leave(id: taskId)
+            }, onCompleted: { [weak self] in
+                self?.autoDispatchGroup.leave(id: taskId)
+            })
+            .disposed(by: disposeBag)
+    }
+
+    // Processes each line received from the data stream.
+    private func processStreamLine(_ line: String, with request: UNNotificationRequest, keyURL: URL, treatedMessagesURL: URL) {
+        do {
+            guard let jsonData = line.data(using: .utf8),
+                  let map = try JSONSerialization.jsonObject(with: jsonData, options: .allowFragments) as? [String: Any],
+                  let id = map["id"] as? String,
+                  map["cypher"] != nil else {
+                log("Line doesn't contain a valid schema")
                 return
             }
-            let str = String(decoding: data, as: UTF8.self)
-            let lines = str.split(whereSeparator: \.isNewline)
-            for line in lines {
-                do {
-                    guard let jsonData = line.data(using: .utf8),
-                          let map = try JSONSerialization.jsonObject(with: jsonData, options: .allowFragments) as? [String: Any],
-                          let keyPath = self.getKeyPath(data: requestData),
-                          let treatedMessages = self.getTreatedMessagesPath(data: requestData) else {
-                        self.verifyTasksStatus()
-                        return
-                    }
-                    let result = self.adapterService.decrypt(keyPath: keyPath.path, accountId: self.accountId, messagesPath: treatedMessages.path, value: map)
-                    let handleCall: (String, String) -> Void = { [weak self] (peerId, hasVideo) in
-                        guard let self = self else {
-                            return
-                        }
-                        var info = request.content.userInfo
-                        info["peerId"] = peerId
-                        info["hasVideo"] = hasVideo
-                        let name = self.bestName(accountId: self.accountId, contactId: peerId)
-                        /// jami will be started. Set accounts to not active state
-                        if self.accountIsActive {
-                            self.accountIsActive = false
-                            self.adapterService.stop(accountId: self.accountId)
-                        }
-                        if name.isEmpty {
-                            info["displayName"] = peerId
-                            self.pendingCalls[peerId] = info
-                            self.startAddressLookup(address: peerId, accountId: self.accountId)
-                            return
-                        }
-                        info["displayName"] = name
-                        self.presentCall(info: info)
-                    }
-                    switch result {
-                    case .call(let peerId, let hasVideo):
-                        handleCall(peerId, "\(hasVideo)")
-                        return
-                    case .gitMessage(let convId):
-                        self.handleGitMessage(convId: convId, loadAll: convId.isEmpty)
-                    case .clone:
-                        // Should start daemon and wait until clone completed
-                        self.waitForCloning = true
-                        self.handleGitMessage(convId: "", loadAll: false)
-                    case .unknown:
-                        break
-                    }
-                } catch {
-                    print("serialization failed , \(error)")
-                }
+
+            guard idsToProcess.contains(id) || processAll else {
+                log("Skipping line; ID is not in the list: \(id)")
+                return
             }
-            self.verifyTasksStatus()
+
+            log("Processing ID: \(id)")
+            idsToProcess.remove(id)
+            processMap(map: map, keyURL: keyURL, treatedMessagesURL: treatedMessagesURL, userInfo: request.content.userInfo)
+            if !processAll && idsToProcess.isEmpty {
+                log("All IDs processed; Canceling stream")
+                httpStreamHandler.cancelStreaming()
+            }
+        } catch {
+            log("Stream decoding error: \(error) line: \(line)")
         }
-        task.resume()
-        _ = tasksGroup.wait(timeout: .now() + notificationTimeout)
     }
 
-    override func serviceExtensionTimeWillExpire() {
-        if !self.tasksCompleted {
-            self.tasksCompleted = true
-            self.tasksGroup.leave()
+    private func processMap(map: [String: Any], keyURL: URL, treatedMessagesURL: URL, userInfo: [AnyHashable: Any]) {
+        let result = adapterService.decrypt(keyPath: keyURL.path, accountId: self.accountId, messagesPath: treatedMessagesURL.path, value: map)
+        log("Notification type: \(result)")
+        switch result {
+        case .call(let peerId, let hasVideo):
+            ({ [weak self] (peerId, hasVideo) in
+                guard let self = self else {
+                    return
+                }
+                var info = userInfo
+                info["peerId"] = peerId
+                info["hasVideo"] = hasVideo
+                let name = self.bestName(accountId: self.accountId, contactId: peerId)
+                // jami will be started. Set accounts to not active state
+                if self.accountIsActive.compareExchange(expected: true, desired: false, ordering: .relaxed).original {
+                    self.adapterService.stop(accountId: self.accountId)
+                }
+                if name.isEmpty {
+                    info["displayName"] = peerId
+                    self.pendingCalls[peerId] = info
+                    self.startAddressLookup(address: peerId)
+                    return
+                }
+                info["displayName"] = name
+                self.presentCall(info: info)
+            })(peerId, "\(hasVideo)")
+            return
+        case .gitMessage(let convId):
+            self.handleGitMessage(convId: convId, loadAll: convId.isEmpty) // async
+        case .clone:
+            // Should start daemon and wait until clone completed
+            self.taskPropertyQueue.sync { self.waitForCloning = true }
+            self.handleGitMessage(convId: "", loadAll: false) // async
+        case .unknown:
+            break
         }
+    }
+
+    override func serviceExtensionTimeWillExpire() {
+        log("Notification handling timeout")
         finish()
     }
 
@@ -176,92 +397,108 @@ class NotificationService: UNNotificationServiceExtension {
         if !isResubscribe {
             return false
         }
-        self.accountIsActive = true
+        self.accountIsActive.store(true, ordering: .relaxed)
         self.adapterService.startAccount(accountId: accountId, convId: "", loadAll: false)
         self.adapterService.pushNotificationReceived(accountId: accountId, data: data)
+        // TODO: comment this a bit more
         // wait to proceed pushNotificationReceived
         sleep(5)
         return true
     }
 
     private func handleGitMessage(convId: String, loadAll: Bool) {
-        /// check if account already acive
-        guard !self.accountIsActive else { return }
-        self.accountIsActive = true
+        // If the account is already active, return, otherwise we set it to active and continue
+        if self.accountIsActive.compareExchange(expected: false, desired: true, ordering: .relaxed).original {
+            return
+        }
+
+        jamiTaskId = UUID().uuidString
+        self.autoDispatchGroup.enter(id: jamiTaskId)
         self.adapterService.startAccountsWithListener(accountId: self.accountId, convId: convId, loadAll: loadAll) { [weak self] event, eventData in
             guard let self = self else {
                 return
             }
+
+            var notifConfig = NotificationConfig(from: eventData.jamiId, url: nil, body: eventData.content,
+                                                 conversationId: eventData.conversationId, groupTitle: eventData.groupTitle)
+
             switch event {
             case .message:
                 self.conversationUpdated(conversationId: eventData.conversationId, accountId: self.accountId)
-                self.numberOfMessages += 1
-                self.configureMessageNotification(from: eventData.jamiId, body: eventData.content, accountId: self.accountId, conversationId: eventData.conversationId, groupTitle: "")
+                self.taskPropertyQueue.sync { self.itemsToPresent += 1 }
+                self.configureAndPresentNotification(config: notifConfig, type: LocalNotificationType.message)
             case .fileTransferDone:
                 self.conversationUpdated(conversationId: eventData.conversationId, accountId: self.accountId)
+                // If the content is a URL then we have already downloaded the file and can present the notification,
+                // otherwise we need to download the file first, so add it to the items to present
                 if let url = URL(string: eventData.content) {
-                    self.configureFileNotification(from: eventData.jamiId, url: url, accountId: self.accountId, conversationId: eventData.conversationId)
+                    notifConfig.url = url
+                    self.configureAndPresentNotification(config: notifConfig, type: LocalNotificationType.file)
                 } else {
-                    self.numberOfFiles -= 1
+                    self.taskPropertyQueue.sync { self.itemsToPresent -= 1 }
                     self.verifyTasksStatus()
                 }
             case .syncCompleted:
-                self.syncCompleted = true
+                self.taskPropertyQueue.sync { self.syncCompleted = true }
                 self.verifyTasksStatus()
             case .fileTransferInProgress:
-                self.numberOfFiles += 1
+                self.taskPropertyQueue.sync { self.itemsToPresent += 1 }
             case .invitation:
                 self.conversationUpdated(conversationId: eventData.conversationId, accountId: self.accountId)
-                self.syncCompleted = true
-                self.numberOfMessages += 1
-                self.configureMessageNotification(from: eventData.jamiId,
-                                                  body: eventData.content,
-                                                  accountId: self.accountId,
-                                                  conversationId: eventData.conversationId,
-                                                  groupTitle: eventData.groupTitle)
+                self.taskPropertyQueue.sync {
+                    self.syncCompleted = true
+                    self.itemsToPresent += 1
+                }
+                self.configureAndPresentNotification(config: notifConfig, type: LocalNotificationType.message)
             case .conversationCloned:
-                self.waitForCloning = false
+                self.taskPropertyQueue.sync { self.waitForCloning = false }
                 self.verifyTasksStatus()
             }
         }
     }
 
     private func verifyTasksStatus() {
-        guard !self.tasksCompleted else { return } /// we already left taskGroup
-        /// waiting for lookup
-        if !pendingCalls.isEmpty || !pendingLocalNotifications.isEmpty {
-            return
+        // waiting for lookup
+        self.notificationQueue.sync {
+            if !pendingCalls.isEmpty || !pendingLocalNotifications.isEmpty {
+                return
+            }
         }
-        /// We could finish in two cases:
-        /// 1. we did not start account we are not waiting for the signals from the daemon
-        /// 2. conversation synchronization completed and all files downloaded
-        if !self.accountIsActive || (self.syncCompleted && self.numberOfFiles == 0 && self.numberOfMessages == 0 && !self.waitForCloning) {
-            self.tasksCompleted = true
-            self.tasksGroup.leave()
+        self.taskPropertyQueue.sync {
+            // We could finish in two cases:
+            // 1. we did not start account we are not waiting for the signals from the daemon
+            // 2. conversation synchronization completed and all files downloaded
+            if !self.accountIsActive.load(ordering: .relaxed) ||
+                (self.syncCompleted && self.itemsToPresent == 0 && !self.waitForCloning) {
+                self.autoDispatchGroup.leave(id: jamiTaskId)
+            }
         }
     }
 
     private func finish() {
-        if self.accountIsActive {
-            self.accountIsActive = false
+        if self.accountIsActive.compareExchange(expected: true, desired: false, ordering: .relaxed).original {
             self.adapterService.stop(accountId: self.accountId)
         } else {
             self.adapterService.removeDelegate()
         }
-        /// cleanup pending notifications
-        if !self.pendingCalls.isEmpty, let info = self.pendingCalls.first?.value {
-            self.presentCall(info: info)
-        } else {
-            for notifications in pendingLocalNotifications {
-                for notification in notifications.value {
-                    self.presentLocalNotification(notification: notification)
+        // cleanup pending notifications
+        self.notificationQueue.sync {
+            if !self.pendingCalls.isEmpty, let info = self.pendingCalls.first?.value {
+                self.presentCall(info: info)
+            } else {
+                for notifications in pendingLocalNotifications {
+                    for notification in notifications.value {
+                        self.presentLocalNotification(notification: notification)
+                    }
                 }
+                pendingLocalNotifications.removeAll()
             }
-            pendingLocalNotifications.removeAll()
         }
+        self.httpStreamHandler.cancelStreaming()
         if let contentHandler = contentHandler {
             contentHandler(self.bestAttemptContent)
         }
+        log("Finished handling notification")
     }
 
     private func appIsActive() -> Bool {
@@ -272,18 +509,18 @@ class NotificationService: UNNotificationServiceExtension {
         }
         var appIsActive = false
         group.enter()
-        /// post darwin notification and wait for the answer from the main app. If answer received app is active
+        // post darwin notification and wait for the answer from the main app. If answer received app is active
         self.listenToMainAppResponse { _ in
             appIsActive = true
         }
         CFNotificationCenterPostNotification(notificationCenter, CFNotificationName(Constants.notificationReceived), nil, nil, true)
-        /// wait fro 100 milliseconds. If no answer from main app is received app is not active.
+        // wait fro 300 milliseconds. If no answer from main app is received app is not active.
         _ = group.wait(timeout: .now() + 0.3)
 
         return appIsActive
     }
 
-    private func saveData(data: [String: String]) {
+    private func saveDataIfNeeded(data: [String: String]) {
         guard let userDefaults = UserDefaults(suiteName: Constants.appGroupIdentifier) else {
             return
         }
@@ -324,18 +561,6 @@ class NotificationService: UNNotificationServiceExtension {
         userDefaults.set(conversationData, forKey: Constants.updatedConversations)
     }
 
-    private func setNotificationCount(notification: UNMutableNotificationContent) {
-        guard let userDefaults = UserDefaults(suiteName: Constants.appGroupIdentifier) else {
-            return
-        }
-
-        if let count = userDefaults.object(forKey: Constants.notificationsCount) as? NSNumber {
-            let new: NSNumber = count.intValue + 1 as NSNumber
-            notification.badge = new
-            userDefaults.set(new, forKey: Constants.notificationsCount)
-        }
-    }
-
     private func requestToDictionary(request: UNNotificationRequest) -> [String: String] {
         var dictionary = [String: String]()
         let userInfo = request.content.userInfo
@@ -352,16 +577,10 @@ class NotificationService: UNNotificationServiceExtension {
         }
         return dictionary
     }
+}
 
-    private func requestedData(request: UNNotificationRequest, map: [String: Any]) -> Bool {
-        guard let userInfo = request.content.userInfo as? [String: Any] else { return false }
-        guard let valueIds = userInfo["valueIds"] as? [String: String],
-              let id = map["id"] else {
-            return false
-        }
-        return valueIds.values.contains("\(id)")
-    }
-
+// MARK: Name retrieval
+extension NotificationService {
     private func bestName(accountId: String, contactId: String) -> String {
         if let name = self.names[contactId], !name.isEmpty {
             return name
@@ -378,12 +597,12 @@ class NotificationService: UNNotificationServiceExtension {
         return registeredName
     }
 
-    private func startAddressLookup(address: String, accountId: String) {
-        var nameServer = self.adapterService.getNameServerFor(accountId: accountId)
+    private func startAddressLookup(address: String) {
+        var nameServer = self.adapterService.getNameServerFor(accountId: self.accountId)
         nameServer = ensureURLPrefix(urlString: nameServer)
         let urlString = nameServer + "/addr/" + address
         guard let url = URL(string: urlString) else {
-            self.lookupCompleted(address: address, name: nil)
+            self.lookupCompleted(address: address)
             return
         }
         let defaultSession = URLSession(configuration: .default)
@@ -391,7 +610,7 @@ class NotificationService: UNNotificationServiceExtension {
             guard let self = self else { return }
             var name: String?
             defer {
-                self.lookupCompleted(address: address, name: name)
+                self.lookupCompleted(address: address)
             }
             guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200,
                   let data = data else {
@@ -404,7 +623,7 @@ class NotificationService: UNNotificationServiceExtension {
                     self.names[address] = name
                 }
             } catch {
-                print("serialization failed , \(error)")
+                log("Serialization failed: \(error)")
             }
         }
         task.resume()
@@ -418,7 +637,8 @@ class NotificationService: UNNotificationServiceExtension {
         return urlWithPrefix
     }
 
-    private func lookupCompleted(address: String, name: String?) {
+    private func lookupCompleted(address: String) {
+        let name = self.names[address]
         for call in pendingCalls where call.key == address {
             var info = call.value
             if let name = name {
@@ -438,20 +658,10 @@ class NotificationService: UNNotificationServiceExtension {
             pendingLocalNotifications.removeValue(forKey: address)
         }
     }
-
-    private func needUpdateNotification(notification: LocalNotification, peerId: String, accountId: String) {
-        if var pending = pendingLocalNotifications[peerId] {
-            pending.append(notification)
-            pendingLocalNotifications[peerId] = pending
-        } else {
-            pendingLocalNotifications[peerId] = [notification]
-        }
-        startAddressLookup(address: peerId, accountId: accountId)
-    }
 }
-// MARK: paths
-extension NotificationService {
 
+// MARK: Paths and URLs
+extension NotificationService {
     private func getRequestURL(data: [String: String], proxyURL: URL) -> URL? {
         guard let key = data[NotificationField.key.rawValue] else {
             return nil
@@ -474,7 +684,7 @@ extension NotificationService {
         return urlPrpxy.appendingPathComponent(key)
     }
 
-    private func getKeyPath(data: [String: String]) -> URL? {
+    private func getKeyURL(data: [String: String]) -> URL? {
         guard let documentsPath = Constants.documentsPath,
               let accountId = data[NotificationField.accountId.rawValue] else {
             return nil
@@ -482,7 +692,7 @@ extension NotificationService {
         return documentsPath.appendingPathComponent(accountId).appendingPathComponent("ring_device.key")
     }
 
-    private func getTreatedMessagesPath(data: [String: String]) -> URL? {
+    private func getTreatedMessagesURL(data: [String: String]) -> URL? {
         guard let cachesPath = Constants.cachesPath,
               let accountId = data[NotificationField.accountId.rawValue] else {
             return nil
@@ -534,7 +744,7 @@ extension NotificationService: DarwinNotificationHandler {
 
 }
 
-// MARK: present notifications
+// MARK: Present and update notifications
 extension NotificationService {
     private func createAttachment(identifier: String, image: UIImage, options: [NSObject: AnyObject]?) -> UNNotificationAttachment? {
         let fileManager = FileManager.default
@@ -587,45 +797,46 @@ extension NotificationService {
         return CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary)
     }
 
-    private func configureFileNotification(from: String, url: URL, accountId: String, conversationId: String) {
+    // A generic function that configures a notification with the given content and type, and returns the notification
+    private func configureNotification(config: NotificationConfig, type: LocalNotificationType) -> LocalNotification {
         let content = UNMutableNotificationContent()
         content.sound = UNNotificationSound.default
-        let imageName = url.lastPathComponent
-        content.body = imageName
         var data = [String: String]()
-        data[Constants.NotificationUserInfoKeys.participantID.rawValue] = from
-        data[Constants.NotificationUserInfoKeys.accountID.rawValue] = accountId
-        data[Constants.NotificationUserInfoKeys.conversationID.rawValue] = conversationId
+        data[Constants.NotificationUserInfoKeys.participantID.rawValue] = config.from
+        data[Constants.NotificationUserInfoKeys.accountID.rawValue] = self.accountId
+        data[Constants.NotificationUserInfoKeys.conversationID.rawValue] = config.conversationId
         content.userInfo = data
-        if let image = createThumbnailImage(fileURLString: url.path), let attachement = createAttachment(identifier: imageName, image: image, options: nil) {
-            content.attachments = [ attachement ]
+        switch type {
+        case .message:
+            content.body = config.body
+        case .file:
+            if let url = config.url {
+                let imageName = url.lastPathComponent
+                content.body = imageName
+                if let image = createThumbnailImage(fileURLString: url.path),
+                   let attachement = createAttachment(identifier: imageName, image: image, options: nil) {
+                    content.attachments = [ attachement ]
+                }
+            }
         }
-        let title = self.bestName(accountId: accountId, contactId: from)
-        if title.isEmpty {
-            content.title = from
-            needUpdateNotification(notification: LocalNotification(content, .file), peerId: from, accountId: accountId)
+        if !config.groupTitle.isEmpty {
+            content.title = config.groupTitle
         } else {
-            content.title = title
-            presentLocalNotification(notification: LocalNotification(content, .file))
+            content.title = self.bestName(accountId: self.accountId, contactId: config.from)
         }
+        if content.title.isEmpty {
+            content.title = config.from
+        }
+        return (content, type)
     }
 
-    private func configureMessageNotification(from: String, body: String, accountId: String, conversationId: String, groupTitle: String) {
-        let content = UNMutableNotificationContent()
-        content.body = body
-        content.sound = UNNotificationSound.default
-        var data = [String: String]()
-        data[Constants.NotificationUserInfoKeys.participantID.rawValue] = from
-        data[Constants.NotificationUserInfoKeys.accountID.rawValue] = accountId
-        data[Constants.NotificationUserInfoKeys.conversationID.rawValue] = conversationId
-        content.userInfo = data
-        let title = !groupTitle.isEmpty ? groupTitle : self.bestName(accountId: accountId, contactId: from)
-        if title.isEmpty {
-            content.title = from
-            needUpdateNotification(notification: LocalNotification(content, .message), peerId: from, accountId: accountId)
+    private func configureAndPresentNotification(config: NotificationConfig, type: LocalNotificationType) {
+        let notif = self.configureNotification(config: config, type: type)
+        // If the title is a URI, do a lookup and queue presentation
+        if notif.content.title == config.from {
+            enqueueNotificationForNameUpdate(notification: notif, peerId: config.from)
         } else {
-            content.title = title
-            presentLocalNotification(notification: LocalNotification(content, .message))
+            self.presentLocalNotification(notification: notif)
         }
     }
 
@@ -635,24 +846,48 @@ extension NotificationService {
         let notificationTrigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.01, repeats: false)
         let notificationRequest = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: notificationTrigger)
         UNUserNotificationCenter.current().add(notificationRequest) { [weak self] (error) in
-            if notification.type == .message {
-                self?.numberOfMessages -= 1
-            } else {
-                self?.numberOfFiles -= 1
-            }
-            self?.verifyTasksStatus()
             if let error = error {
-                print("Unable to Add Notification Request (\(error), \(error.localizedDescription))")
+                log("Unable to Add Notification Request: (\(error), \(error.localizedDescription))")
             }
+            guard let self = self else { return }
+            self.taskPropertyQueue.sync { self.itemsToPresent -= 1 }
+            self.verifyTasksStatus()
         }
     }
 
     private func presentCall(info: [AnyHashable: Any]) {
+        // TODO: see if this should sync after daemon stop
         CXProvider.reportNewIncomingVoIPPushPayload(info, completion: { error in
-            print("NotificationService", "Did report voip notification, error: \(String(describing: error))")
+            log("NotificationService", "Did report voip notification, error: \(String(describing: error))")
         })
-        self.pendingCalls.removeAll()
-        self.pendingLocalNotifications.removeAll()
+        self.notificationQueue.sync {
+            self.pendingCalls.removeAll()
+            self.pendingLocalNotifications.removeAll()
+        }
         self.verifyTasksStatus()
     }
+
+    private func enqueueNotificationForNameUpdate(notification: LocalNotification, peerId: String) {
+        self.notificationQueue.sync {
+            if var pending = pendingLocalNotifications[peerId] {
+                pending.append(notification)
+                pendingLocalNotifications[peerId] = pending
+            } else {
+                pendingLocalNotifications[peerId] = [notification]
+            }
+        }
+        startAddressLookup(address: peerId)
+    }
+
+    private func setNotificationCount(notification: UNMutableNotificationContent) {
+        guard let userDefaults = UserDefaults(suiteName: Constants.appGroupIdentifier) else {
+            return
+        }
+
+        if let count = userDefaults.object(forKey: Constants.notificationsCount) as? NSNumber {
+            let new: NSNumber = count.intValue + 1 as NSNumber
+            notification.badge = new
+            userDefaults.set(new, forKey: Constants.notificationsCount)
+        }
+    }
 }