From afc4332716fbd01c1e758714a06cf9b433c4a4ba Mon Sep 17 00:00:00 2001 From: Mauro <34335419+Velin92@users.noreply.github.com> Date: Thu, 22 Jun 2023 19:23:33 +0200 Subject: [PATCH] Two sync loop experimental solution to decrypt push notifications (#1082) * Experimental two sync loop solution * better logging * improved the code and handled invite notifications display better * improved invite notifications * new exerimental branch * new sync loop * code updated * code improvements * code improvements * fix typo * code improvements * removed some unused code and added a respawn * fixing some NSE issues * code improvements * new version of the branch * more logging * running the nse process ONLY IF necessary * finally works! made also the feature flag * also the encryption value of the room list api will depend on the flag now * changelog * code improvements * code improvement * updated proj * fixing some compilation error after the rebase * opt-in for the encryption sync * fix --- ElementX.xcodeproj/project.pbxproj | 6 ++ .../xcshareddata/swiftpm/Package.resolved | 2 +- .../Sources/Application/AppCoordinator.swift | 10 ++++ .../Sources/Application/AppSettings.swift | 6 +- .../Extensions/UNNotificationContent.swift | 13 ----- .../Other/SharedUserDefaultsKeys.swift | 2 +- .../DeveloperOptionsScreenModels.swift | 2 + .../DeveloperOptionsScreenViewModel.swift | 5 +- .../View/DeveloperOptionsScreen.swift | 8 +++ .../Sources/Services/Client/ClientProxy.swift | 58 +++++++++++++++++-- .../Proxy/EncryptionSyncListenerProxy.swift | 31 ++++++++++ .../Proxy/NotificationItemProxy.swift | 12 ---- .../NotificationServiceExtension.swift | 44 +++++++++----- NSE/Sources/Other/NSESettings.swift | 6 +- NSE/Sources/Other/NSEUserSession.swift | 34 +++++++++-- NSE/SupportingFiles/target.yml | 2 +- changelog.d/1083.feature | 1 + project.yml | 2 +- 18 files changed, 184 insertions(+), 60 deletions(-) create mode 100644 ElementX/Sources/Services/Notification/Proxy/EncryptionSyncListenerProxy.swift create mode 100644 changelog.d/1083.feature diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index f53c0b339e..32c7ac828f 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -264,6 +264,7 @@ 6860721DB3091BE08164C132 /* MapAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B48B7AD4908C5C374517B892 /* MapAssets.xcassets */; }; 68AC3C84E2B438036B174E30 /* EmoteRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 471EB7D96AFEA8D787659686 /* EmoteRoomTimelineView.swift */; }; 695825D20A761C678809345D /* MessageForwardingScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52135BD9E0E7A091688F627A /* MessageForwardingScreenModels.swift */; }; + 69ABFBAF05D7EF11E7C88CEA /* EncryptionSyncListenerProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68356CB936A8814A3FEA66A8 /* EncryptionSyncListenerProxy.swift */; }; 69BCBB4FB2DC3D61A28D3FD8 /* TimelineStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DC2C9E0E15C79BBDA80F0A2 /* TimelineStyle.swift */; }; 69C7B956B74BEC3DB88224EA /* NavigationSplitCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78913D6E120D46138E97C107 /* NavigationSplitCoordinatorTests.swift */; }; 6A0E7551E0D1793245F34CDD /* ClientError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09A267106B9585D3D0CFC0D /* ClientError.swift */; }; @@ -672,6 +673,7 @@ F7BC744FFA7FE248FAE7F570 /* UserIndicatorToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F57C8022B8A871A1DCD1750A /* UserIndicatorToastView.swift */; }; F86102DC2C68BBBB0521BAAE /* SoftLogoutScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BB385E148DE55C85C0A02D6 /* SoftLogoutScreenModels.swift */; }; F8E725D42023ECA091349245 /* AudioRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57EAAF82432B0B53881CF826 /* AudioRoomTimelineItem.swift */; }; + F91B4629E4AF51A4FE8E7608 /* EncryptionSyncListenerProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68356CB936A8814A3FEA66A8 /* EncryptionSyncListenerProxy.swift */; }; F94000E3D91B11C527DA8807 /* UserProfileCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 923485F85E1D765EF9D20E88 /* UserProfileCell.swift */; }; F9842667B68DC6FA1F9ECCBB /* NSItemProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F72EFC8C634469F9262659C7 /* NSItemProvider.swift */; }; F99FB21EFC6D99D247FE7CBE /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = DE8DC9B3FBA402117DC4C49F /* Kingfisher */; }; @@ -992,6 +994,7 @@ 6654859746B0BE9611459391 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = cs; path = cs.lproj/Localizable.stringsdict; sourceTree = ""; }; 669F35C505ACE1110589F875 /* MediaUploadingPreprocessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadingPreprocessor.swift; sourceTree = ""; }; 66F2402D738694F98729A441 /* RoomTimelineProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineProvider.swift; sourceTree = ""; }; + 68356CB936A8814A3FEA66A8 /* EncryptionSyncListenerProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionSyncListenerProxy.swift; sourceTree = ""; }; 6861FE915C7B5466E6962BBA /* StartChatScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreen.swift; sourceTree = ""; }; 686BCFA37AC6C67FF973CE67 /* OnboardingBackgroundImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingBackgroundImage.swift; sourceTree = ""; }; 69219A908D7C22E6EE6689AE /* UserNotificationCenterSpy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationCenterSpy.swift; sourceTree = ""; }; @@ -2519,6 +2522,7 @@ 832FC81F760220239E285294 /* Proxy */ = { isa = PBXGroup; children = ( + 68356CB936A8814A3FEA66A8 /* EncryptionSyncListenerProxy.swift */, 25F7FE40EF7490A7E09D7BE6 /* NotificationItemProxy.swift */, ); path = Proxy; @@ -3766,6 +3770,7 @@ 9A3B0CDF097E3838FB1B9595 /* Bundle.swift in Sources */, DFCA89C4EC2A5332ED6B441F /* DataProtectionManager.swift in Sources */, 24A75F72EEB7561B82D726FD /* Date.swift in Sources */, + F91B4629E4AF51A4FE8E7608 /* EncryptionSyncListenerProxy.swift in Sources */, A33784831AD880A670CAA9F9 /* FileManager.swift in Sources */, 59F940FCBE6BC343AECEF75E /* ImageCache.swift in Sources */, A3E390675E9730C176B59E1B /* ImageProviderProtocol.swift in Sources */, @@ -4006,6 +4011,7 @@ 8B1D5CE017EEC734CF5FE130 /* Encodable.swift in Sources */, 4C5A638DAA8AF64565BA4866 /* EncryptedRoomTimelineItem.swift in Sources */, B5903E48CF43259836BF2DBF /* EncryptedRoomTimelineView.swift in Sources */, + 69ABFBAF05D7EF11E7C88CEA /* EncryptionSyncListenerProxy.swift in Sources */, F78BAD28482A467287A9A5A3 /* EventBasedMessageTimelineItemProtocol.swift in Sources */, 02D8DF8EB7537EB4E9019DDB /* EventBasedTimelineItemProtocol.swift in Sources */, 63E46D18B91D08E15FC04125 /* ExpiringTaskRunner.swift in Sources */, diff --git a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 1f43542730..b312bf32c3 100644 --- a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -11,7 +11,7 @@ { "identity" : "compound-ios", "kind" : "remoteSourceControl", - "location" : "https://github.com/vector-im/compound-ios.git", + "location" : "https://github.com/vector-im/compound-ios", "state" : { "revision" : "d1a28b8a311e33ddb517d10391037f1547a3c7b6" } diff --git a/ElementX/Sources/Application/AppCoordinator.swift b/ElementX/Sources/Application/AppCoordinator.swift index 3c4f05c6d9..06e56c8869 100644 --- a/ElementX/Sources/Application/AppCoordinator.swift +++ b/ElementX/Sources/Application/AppCoordinator.swift @@ -563,6 +563,16 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate, selector: #selector(applicationDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil) + + NotificationCenter.default.addObserver(self, + selector: #selector(applicationWillTerminate), + name: UIApplication.willTerminateNotification, + object: nil) + } + + @objc + private func applicationWillTerminate() { + userSession?.clientProxy.stopSync() } @objc diff --git a/ElementX/Sources/Application/AppSettings.swift b/ElementX/Sources/Application/AppSettings.swift index cf65cff18c..1f25c96687 100644 --- a/ElementX/Sources/Application/AppSettings.swift +++ b/ElementX/Sources/Application/AppSettings.swift @@ -163,9 +163,9 @@ final class AppSettings { @UserPreference(key: UserDefaultsKeys.pusherProfileTag, storageType: .userDefaults(store)) var pusherProfileTag: String? - /// A set of all the notification identifiers that have been served so far, it's reset every time the app is launched - @UserPreference(key: SharedUserDefaultsKeys.servedNotificationIdentifiers, initialValue: [], storageType: .userDefaults(store)) - var servedNotificationIdentifiers: Set + /// Tag describing if the app and the NSE should use the encryption sync + @UserPreference(key: SharedUserDefaultsKeys.isEncryptionSyncEnabled, defaultValue: false, storageType: .userDefaults(store)) + var isEncryptionSyncEnabled // MARK: - Other diff --git a/ElementX/Sources/Other/Extensions/UNNotificationContent.swift b/ElementX/Sources/Other/Extensions/UNNotificationContent.swift index 54b1659cf5..5aebcea8cf 100644 --- a/ElementX/Sources/Other/Extensions/UNNotificationContent.swift +++ b/ElementX/Sources/Other/Extensions/UNNotificationContent.swift @@ -33,10 +33,6 @@ extension UNNotificationContent { userInfo[NotificationConstants.UserInfoKey.receiverIdentifier] as? String } - @objc var notificationID: String? { - userInfo[NotificationConstants.UserInfoKey.notificationIdentifier] as? String - } - @objc var roomID: String? { userInfo[NotificationConstants.UserInfoKey.roomIdentifier] as? String } @@ -74,15 +70,6 @@ extension UNMutableNotificationContent { } } - override var notificationID: String? { - get { - userInfo[NotificationConstants.UserInfoKey.notificationIdentifier] as? String - } - set { - userInfo[NotificationConstants.UserInfoKey.notificationIdentifier] = newValue - } - } - func addMediaAttachment(using mediaProvider: MediaProviderProtocol?, mediaSource: MediaSourceProxy) async -> UNMutableNotificationContent { guard let mediaProvider else { diff --git a/ElementX/Sources/Other/SharedUserDefaultsKeys.swift b/ElementX/Sources/Other/SharedUserDefaultsKeys.swift index 8af58921bc..164118e554 100644 --- a/ElementX/Sources/Other/SharedUserDefaultsKeys.swift +++ b/ElementX/Sources/Other/SharedUserDefaultsKeys.swift @@ -15,5 +15,5 @@ // enum SharedUserDefaultsKeys: String { - case servedNotificationIdentifiers + case isEncryptionSyncEnabled } diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift index c1d60d68bd..8365ccafb5 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift @@ -28,11 +28,13 @@ struct DeveloperOptionsScreenViewStateBindings { var shouldCollapseRoomStateEvents: Bool var userSuggestionsEnabled: Bool var readReceiptsEnabled: Bool + var isEncryptionSyncEnabled: Bool } enum DeveloperOptionsScreenViewAction { case changedShouldCollapseRoomStateEvents case changedUserSuggestionsEnabled case changedReadReceiptsEnabled + case changedIsEncryptionSyncEnabled case clearCache } diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenViewModel.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenViewModel.swift index db92456848..ad8d109eb2 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenViewModel.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenViewModel.swift @@ -27,7 +27,8 @@ class DeveloperOptionsScreenViewModel: DeveloperOptionsScreenViewModelType, Deve appSettings = ServiceLocator.shared.settings let bindings = DeveloperOptionsScreenViewStateBindings(shouldCollapseRoomStateEvents: appSettings.shouldCollapseRoomStateEvents, userSuggestionsEnabled: appSettings.userSuggestionsEnabled, - readReceiptsEnabled: appSettings.readReceiptsEnabled) + readReceiptsEnabled: appSettings.readReceiptsEnabled, + isEncryptionSyncEnabled: appSettings.isEncryptionSyncEnabled) let state = DeveloperOptionsScreenViewState(bindings: bindings) super.init(initialViewState: state) @@ -45,6 +46,8 @@ class DeveloperOptionsScreenViewModel: DeveloperOptionsScreenViewModelType, Deve appSettings.userSuggestionsEnabled = state.bindings.userSuggestionsEnabled case .changedReadReceiptsEnabled: appSettings.readReceiptsEnabled = state.bindings.readReceiptsEnabled + case .changedIsEncryptionSyncEnabled: + appSettings.isEncryptionSyncEnabled = state.bindings.isEncryptionSyncEnabled case .clearCache: callback?(.clearCache) } diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift index 36899a56f0..b90a3a94e3 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift @@ -44,6 +44,14 @@ struct DeveloperOptionsScreen: View { .onChange(of: context.readReceiptsEnabled) { _ in context.send(viewAction: .changedReadReceiptsEnabled) } + + Toggle(isOn: $context.isEncryptionSyncEnabled) { + Text("Use notification encryption sync") + Text("requires app reboot") + } + .onChange(of: context.isEncryptionSyncEnabled) { _ in + context.send(viewAction: .changedIsEncryptionSyncEnabled) + } } Section { diff --git a/ElementX/Sources/Services/Client/ClientProxy.swift b/ElementX/Sources/Services/Client/ClientProxy.swift index 06c5b1d825..cc628eadcb 100644 --- a/ElementX/Sources/Services/Client/ClientProxy.swift +++ b/ElementX/Sources/Services/Client/ClientProxy.swift @@ -28,7 +28,10 @@ class ClientProxy: ClientProxyProtocol { private var roomListService: RoomListService? private var roomListStateUpdateTaskHandle: TaskHandle? - + + private var encryptionSyncService: EncryptionSync? + private var isEncryptionSyncing = false + var roomSummaryProvider: RoomSummaryProviderProtocol? var inviteSummaryProvider: RoomSummaryProviderProtocol? @@ -103,7 +106,7 @@ class ClientProxy: ClientProxyProtocol { } var isSyncing: Bool { - roomListService?.isSyncing() ?? false + roomListService?.isSyncing() ?? false && isEncryptionSyncing } func startSync() { @@ -111,18 +114,30 @@ class ClientProxy: ClientProxyProtocol { guard !isSyncing else { return } - + + startEncryptionSyncService() roomListService?.sync() } func stopSync() { MXLog.info("Stopping sync") + stopEncryptionSyncService() + do { try roomListService?.stopSync() } catch { MXLog.error("Failed stopping room list service with error: \(error)") } } + + private func stopEncryptionSyncService() { + guard isEncryptionSyncing else { + return + } + isEncryptionSyncing = false + encryptionSyncService?.stop() + MXLog.info("Stopping Encryption Sync service") + } func directRoomForUserID(_ userID: String) async -> Result { await Task.dispatch(on: clientQueue) { @@ -359,14 +374,47 @@ class ClientProxy: ClientProxyProtocol { self.avatarURLSubject.value = urlString.flatMap(URL.init) } } - + + private func startEncryptionSyncService() { + guard ServiceLocator.shared.settings.isEncryptionSyncEnabled else { + return + } + configureEncryptionSyncService() + } + + private func configureEncryptionSyncService() { + do { + let listener = EncryptionSyncListenerProxy { [weak self] reason in + switch reason { + case .done: + MXLog.info("Encryption Sync has finished for user: \(self?.userID ?? "unknown")") + case .error(let msg): + MXLog.error("Encryption Sync has terminated for user: \(self?.userID ?? "unknown") for reason: \(msg)") + guard let self else { + return + } + Task { + self.configureEncryptionSyncService() + } + } + } + let encryptionSync = try client.mainEncryptionSync(id: "Main App", listener: listener) + encryptionSync.reloadCaches() + isEncryptionSyncing = true + encryptionSyncService = encryptionSync + MXLog.info("Encryption sync started for user: \(userID)") + } catch { + MXLog.error("Configure encryption sync failed with error: \(error)") + } + } + private func configureRoomListService() async { guard roomListService == nil else { fatalError("This shouldn't be called more than once") } do { - let roomListService = try client.roomListServiceWithEncryption() + let roomListService = try ServiceLocator.shared.settings.isEncryptionSyncEnabled ? client.roomListService() : client.roomListServiceWithEncryption() roomListStateUpdateTaskHandle = roomListService.state(listener: RoomListStateListenerProxy { [weak self] state in guard let self else { return } MXLog.info("Received room list update: \(state)") diff --git a/ElementX/Sources/Services/Notification/Proxy/EncryptionSyncListenerProxy.swift b/ElementX/Sources/Services/Notification/Proxy/EncryptionSyncListenerProxy.swift new file mode 100644 index 0000000000..e01c6bbafd --- /dev/null +++ b/ElementX/Sources/Services/Notification/Proxy/EncryptionSyncListenerProxy.swift @@ -0,0 +1,31 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +import MatrixRustSDK + +final class EncryptionSyncListenerProxy: EncryptionSyncListener { + private let didTerminateClosure: (EncryptionSyncTerminationReason) -> Void + + init(_ didTerminateClosure: @escaping (EncryptionSyncTerminationReason) -> Void) { + self.didTerminateClosure = didTerminateClosure + } + + func didTerminate(reason: EncryptionSyncTerminationReason) { + didTerminateClosure(reason) + } +} diff --git a/ElementX/Sources/Services/Notification/Proxy/NotificationItemProxy.swift b/ElementX/Sources/Services/Notification/Proxy/NotificationItemProxy.swift index 7bf994fa32..8d2a57be5b 100644 --- a/ElementX/Sources/Services/Notification/Proxy/NotificationItemProxy.swift +++ b/ElementX/Sources/Services/Notification/Proxy/NotificationItemProxy.swift @@ -48,17 +48,6 @@ protocol NotificationItemProxyProtocol { var isEncrypted: Bool { get } } -extension NotificationItemProxyProtocol { - var id: String? { - let identifiers = receiverID + roomID + event.eventID - guard let data = identifiers.data(using: .utf8) else { - return nil - } - let digest = SHA256.hash(data: data) - return digest.compactMap { String(format: "%02x", $0) }.joined() - } -} - struct NotificationItemProxy: NotificationItemProxyProtocol { let notificationItem: NotificationItem let receiverID: String @@ -168,7 +157,6 @@ extension NotificationItemProxyProtocol { notification.receiverID = receiverID notification.roomID = roomID notification.eventID = event.eventID - notification.notificationID = id notification.sound = isNoisy ? UNNotificationSound(named: UNNotificationSoundName(rawValue: "message.caf")) : nil // So that the UI groups notification that are received for the same room but also for the same user notification.threadIdentifier = "\(receiverID)\(roomID)" diff --git a/NSE/Sources/NotificationServiceExtension.swift b/NSE/Sources/NotificationServiceExtension.swift index e111e90683..5b31ab7845 100644 --- a/NSE/Sources/NotificationServiceExtension.swift +++ b/NSE/Sources/NotificationServiceExtension.swift @@ -24,6 +24,7 @@ class NotificationServiceExtension: UNNotificationServiceExtension { accessGroup: InfoPlistReader.main.keychainAccessGroupIdentifier) private var handler: ((UNNotificationContent) -> Void)? private var modifiedContent: UNMutableNotificationContent? + private var userSession: NSEUserSession? override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { @@ -71,9 +72,26 @@ class NotificationServiceExtension: UNNotificationServiceExtension { do { let userSession = try NSEUserSession(credentials: credentials) - - guard let itemProxy = try await userSession.notificationItemProxy(roomID: roomId, eventID: eventId) else { - MXLog.info("\(tag) no notification for this event") + self.userSession = userSession + var itemProxy = await userSession.notificationItemProxy(roomID: roomId, eventID: eventId) + if settings.isEncryptionSyncEnabled, + itemProxy?.isEncrypted == true, + let _ = try? userSession.startEncryptionSync() { + // TODO: The following wait with a timeout should be handled by the SDK + // We try to decrypt the notification for 10 seconds at most + let date = Date() + repeat { + // if the sync terminated we try one last time then we break from the loop + guard userSession.isSyncing else { + itemProxy = await userSession.notificationItemProxy(roomID: roomId, eventID: eventId) + break + } + itemProxy = await userSession.notificationItemProxy(roomID: roomId, eventID: eventId) + } while itemProxy?.isEncrypted == true && date.timeIntervalSinceNow > -10 + } + + guard let itemProxy else { + MXLog.info("\(tag) no notification for the event, discard") return discard() } @@ -110,31 +128,29 @@ class NotificationServiceExtension: UNNotificationServiceExtension { return discard() } - guard let identifier = modifiedContent.notificationID, - !settings.servedNotificationIdentifiers.contains(identifier) else { - MXLog.info("\(tag) notify: notification already served") - return discard() - } - - settings.servedNotificationIdentifiers.insert(identifier) handler?(modifiedContent) - handler = nil - self.modifiedContent = nil + cleanUp() } private func discard() { MXLog.info("\(tag) discard") handler?(UNMutableNotificationContent()) - handler = nil - modifiedContent = nil + cleanUp() } private var tag: String { "[NSE][\(Unmanaged.passUnretained(self).toOpaque())][\(Unmanaged.passUnretained(Thread.current).toOpaque())]" } + private func cleanUp() { + handler = nil + modifiedContent = nil + userSession?.stopEncryptionSync() + } + deinit { + cleanUp() NSELogger.logMemory(with: tag) MXLog.info("\(tag) deinit") } diff --git a/NSE/Sources/Other/NSESettings.swift b/NSE/Sources/Other/NSESettings.swift index 4205e208b3..e8a63e98cf 100644 --- a/NSE/Sources/Other/NSESettings.swift +++ b/NSE/Sources/Other/NSESettings.swift @@ -22,7 +22,7 @@ final class NSESettings { /// UserDefaults to be used on reads and writes. private static var store: UserDefaults! = UserDefaults(suiteName: suiteName) - /// A set of all the notification identifiers that have been served so far, it's reset every time the app is launched - @UserPreference(key: SharedUserDefaultsKeys.servedNotificationIdentifiers, defaultValue: [], storageType: .userDefaults(store)) - var servedNotificationIdentifiers: Set + /// Tag describing if the app and the NSE should use the encryption sync + @UserPreference(key: SharedUserDefaultsKeys.isEncryptionSyncEnabled, defaultValue: false, storageType: .userDefaults(store)) + var isEncryptionSyncEnabled } diff --git a/NSE/Sources/Other/NSEUserSession.swift b/NSE/Sources/Other/NSEUserSession.swift index 711aad8e4a..ecdea63000 100644 --- a/NSE/Sources/Other/NSEUserSession.swift +++ b/NSE/Sources/Other/NSEUserSession.swift @@ -19,11 +19,18 @@ import MatrixRustSDK final class NSEUserSession { private let client: ClientProtocol + private let userID: String + private var encryptionSyncService: EncryptionSync? private(set) lazy var mediaProvider: MediaProviderProtocol = MediaProvider(mediaLoader: MediaLoader(client: client), imageCache: .onlyOnDisk, backgroundTaskService: nil) + var isSyncing: Bool { + encryptionSyncService != nil + } + init(credentials: KeychainCredentials) throws { + userID = credentials.userID let builder = ClientBuilder() .basePath(path: URL.sessionsBaseDirectory.path) .username(username: credentials.userID) @@ -32,18 +39,35 @@ final class NSEUserSession { try client.restoreSession(session: credentials.restorationToken.session) } - func notificationItemProxy(roomID: String, eventID: String) async throws -> NotificationItemProxyProtocol? { - let userID = try client.userId() - return await Task.dispatch(on: .global()) { + func startEncryptionSync() throws { + let listener = EncryptionSyncListenerProxy { [weak self] reason in + MXLog.info("NSE: Encryption sync terminated for user: \(self?.userID ?? "unknown") with reason: \(reason)") + self?.encryptionSyncService = nil + } + encryptionSyncService = try client.notificationEncryptionSync(id: "NSE", listener: listener, numIters: 2) + MXLog.info("NSE: Encryption sync started for user: \(userID)") + } + + func notificationItemProxy(roomID: String, eventID: String) async -> NotificationItemProxyProtocol? { + await Task.dispatch(on: .global()) { do { guard let notification = try self.client.getNotificationItem(roomId: roomID, eventId: eventID) else { return nil } - return NotificationItemProxy(notificationItem: notification, receiverID: userID) + return NotificationItemProxy(notificationItem: notification, receiverID: self.userID) } catch { MXLog.error("NSE: Could not get notification's content creating an empty notification instead, error: \(error)") - return EmptyNotificationItemProxy(eventID: eventID, roomID: roomID, receiverID: userID) + return EmptyNotificationItemProxy(eventID: eventID, roomID: roomID, receiverID: self.userID) } } } + + func stopEncryptionSync() { + encryptionSyncService?.stop() + } + + deinit { + MXLog.info("NSE: NSEUserSession deinit called for user: \(userID)") + stopEncryptionSync() + } } diff --git a/NSE/SupportingFiles/target.yml b/NSE/SupportingFiles/target.yml index 3ab7f92968..c5e973afde 100644 --- a/NSE/SupportingFiles/target.yml +++ b/NSE/SupportingFiles/target.yml @@ -73,7 +73,7 @@ targets: - path: ../../ElementX/Sources/Services/Keychain/KeychainControllerProtocol.swift - path: ../../ElementX/Sources/Services/Keychain/KeychainController.swift - path: ../../ElementX/Sources/Services/UserSession/RestorationToken.swift - - path: ../../ElementX/Sources/Services/Notification/Proxy/NotificationItemProxy.swift + - path: ../../ElementX/Sources/Services/Notification/Proxy - path: ../../ElementX/Sources/Services/Notification/NotificationConstants.swift - path: ../../ElementX/Sources/Services/Media/Provider - path: ../../ElementX/Sources/Services/Background/BackgroundTaskServiceProtocol.swift diff --git a/changelog.d/1083.feature b/changelog.d/1083.feature new file mode 100644 index 0000000000..00159c7c64 --- /dev/null +++ b/changelog.d/1083.feature @@ -0,0 +1 @@ +Two sync loop implementation to allow to fetch and update decryption keys also from the NSE. \ No newline at end of file diff --git a/project.yml b/project.yml index 0c9baabf4a..8379009c20 100644 --- a/project.yml +++ b/project.yml @@ -11,7 +11,7 @@ options: deploymentTarget: iOS: "16.4" macOS: "13.3" - groupOrdering: + groupOrdering: - order: [ElementX, UnitTests, UITests, IntegrationTests, Tools] - pattern: ElementX order: [Sources, Resources, SupportingFiles]