From a3befe61cf0407cfa2e8d49621e8e220244e5ee7 Mon Sep 17 00:00:00 2001 From: QuentinArguillere Date: Fri, 3 Nov 2023 17:31:59 +0100 Subject: [PATCH] Move all Core related code to another dispatch queue. requires sdk built with feature/swift_wrapper_async_helpers --- Linphone.xcodeproj/project.pbxproj | 2 + Linphone/Contacts/ContactsManager.swift | 94 ++++++----- Linphone/Core/CoreContext.swift | 100 ++++++++---- Linphone/LinphoneApp.swift | 4 +- Linphone/SplashScreen.swift | 2 +- .../Viewmodel/AccountLoginViewModel.swift | 152 ++++++++++-------- .../UI/Assistant/Viewmodel/QRScanner.swift | 11 +- Linphone/UI/Main/ContentView.swift | 10 +- Linphone/Utils/MagicSearchSingleton.swift | 60 ++++--- 9 files changed, 245 insertions(+), 190 deletions(-) diff --git a/Linphone.xcodeproj/project.pbxproj b/Linphone.xcodeproj/project.pbxproj index d22bf1fbe..8cf1f215c 100644 --- a/Linphone.xcodeproj/project.pbxproj +++ b/Linphone.xcodeproj/project.pbxproj @@ -540,6 +540,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOLS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; @@ -600,6 +601,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOLS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; diff --git a/Linphone/Contacts/ContactsManager.swift b/Linphone/Contacts/ContactsManager.swift index 1e31e8716..770c4a72a 100644 --- a/Linphone/Contacts/ContactsManager.swift +++ b/Linphone/Contacts/ContactsManager.swift @@ -38,16 +38,16 @@ final class ContactsManager: ObservableObject { } func fetchContacts() { - DispatchQueue.global().async { - if self.coreContext.mCore.globalState == GlobalState.Shutdown || self.coreContext.mCore.globalState == GlobalState.Off { + coreContext.doOnCoreQueue { core in + if core.globalState == GlobalState.Shutdown || core.globalState == GlobalState.Off { print("$TAG Core is being stopped or already destroyed, abort") } else { print("$TAG ${friends.size} friends created") - self.friendList = self.coreContext.mCore.getFriendListByName(name: self.nativeAddressBookFriendList) + self.friendList = core.getFriendListByName(name: self.nativeAddressBookFriendList) if self.friendList == nil { do { - self.friendList = try self.coreContext.mCore.createFriendList() + self.friendList = try core.createFriendList() } catch let error { print("Failed to enumerate contact", error) } @@ -61,7 +61,7 @@ final class ContactsManager: ObservableObject { self.friendList!.databaseStorageEnabled = false // We don't want to store local address-book in DB self.friendList!.displayName = self.nativeAddressBookFriendList - self.coreContext.mCore.addFriendList(list: self.friendList!) + core.addFriendList(list: self.friendList!) } else { print( "$TAG Friend list [$LINPHONE_ADDRESS_BOOK_FRIEND_LIST] found, removing existing friends if any" @@ -159,52 +159,58 @@ final class ContactsManager: ObservableObject { } awaitDataWrite(data: data, name: name) { _, result in - do { - let friend = try self.coreContext.mCore.createFriend() - friend.edit() - try friend.setName(newValue: contact.firstName + " " + contact.lastName) - friend.organization = contact.organizationName - - var friendAddresses: [Address] = [] - contact.sipAddresses.forEach { sipAddress in - let address = self.coreContext.mCore.interpretUrl(url: sipAddress, applyInternationalPrefix: true) + + self.coreContext.doOnCoreQueue() { core in + do { + var friend = try core.createFriend() - if address != nil && ((friendAddresses.firstIndex(where: {$0.asString() == address?.asString()})) == nil) { - friend.addAddress(address: address!) - friendAddresses.append(address!) - } - } - - var friendPhoneNumbers: [PhoneNumber] = [] - contact.phoneNumbers.forEach { phone in - do { - if (friendPhoneNumbers.firstIndex(where: {$0.numLabel == phone.numLabel})) == nil { - let labelDrop = String(phone.numLabel.dropFirst(4).dropLast(4)) - let phoneNumber = try Factory.Instance.createFriendPhoneNumber(phoneNumber: phone.num, label: labelDrop) - friend.addPhoneNumberWithLabel(phoneNumber: phoneNumber) - friendPhoneNumbers.append(phone) + friend.edit() + try friend.setName(newValue: contact.firstName + " " + contact.lastName) + friend.organization = contact.organizationName + + var friendAddresses: [Address] = [] + contact.sipAddresses.forEach { sipAddress in + let address = core.interpretUrl(url: sipAddress, applyInternationalPrefix: true) + + if address != nil && ((friendAddresses.firstIndex(where: {$0.asString() == address?.asString()})) == nil) { + friend.addAddress(address: address!) + friendAddresses.append(address!) } - } catch let error { - print("Failed to enumerate contact", error) } + + var friendPhoneNumbers: [PhoneNumber] = [] + contact.phoneNumbers.forEach { phone in + do { + if (friendPhoneNumbers.firstIndex(where: {$0.numLabel == phone.numLabel})) == nil { + let labelDrop = String(phone.numLabel.dropFirst(4).dropLast(4)) + let phoneNumber = try Factory.Instance.createFriendPhoneNumber(phoneNumber: phone.num, label: labelDrop) + friend.addPhoneNumberWithLabel(phoneNumber: phoneNumber) + friendPhoneNumbers.append(phone) + } + } catch let error { + print("Failed to enumerate contact", error) + } + } + + let contactImage = result.dropFirst(8) + friend.photo = "file:/" + contactImage + + friend.organization = contact.organizationName + + friend.done() + + DispatchQueue.main.async { + _ = self.friendList!.addLocalFriend(linphoneFriend: friend) + + self.friendList!.updateSubscriptions() + } + } catch let error { + print("Failed to enumerate contact", error) } - let contactImage = result.dropFirst(8) - friend.photo = "file:/" + contactImage - - friend.organization = contact.organizationName - - friend.done() - - _ = self.friendList!.addLocalFriend(linphoneFriend: friend) - - self.friendList!.updateSubscriptions() - - } catch let error { - print("Failed to enumerate contact", error) } } - } + } func awaitDataWrite(data: Data, name: String, completion: @escaping ((), String) -> Void) { let directory = FileManager.default.temporaryDirectory diff --git a/Linphone/Core/CoreContext.swift b/Linphone/Core/CoreContext.swift index efa6b73af..a68b25127 100644 --- a/Linphone/Core/CoreContext.swift +++ b/Linphone/Core/CoreContext.swift @@ -17,74 +17,104 @@ * along with this program. If not, see . */ +// swiftlint:disable large_tuple import linphonesw +import Combine final class CoreContext: ObservableObject { static let shared = CoreContext() - var mCore: Core! - var mRegistrationDelegate: CoreDelegate! - var mConfigurationDelegate: CoreDelegate! - var coreVersion: String = Core.getVersion @Published var loggedIn: Bool = false @Published var loggingInProgress: Bool = false @Published var toastMessage: String = "" + @Published var defaultAccount: Account? + + private var mCore: Core! + private var mIteratePublisher: AnyCancellable? private init() {} - func initialiseCore() async throws { + func doOnCoreQueue(synchronous : Bool = false, lambda: @escaping (Core) -> Void) { + if synchronous { + coreQueue.sync { + lambda(self.mCore) + } + } else { + coreQueue.async { + lambda(self.mCore) + } + } + } + + func initialiseCore() throws { LoggingService.Instance.logLevel = LogLevel.Debug - let factory = Factory.Instance - let configDir = factory.getConfigDir(context: nil) - try? mCore = Factory.Instance.createCore(configPath: "\(configDir)/MyConfig", factoryConfigPath: "", systemContext: nil) - - mCore.friendsDatabasePath = "\(configDir)/friends.db" - - try? mCore.start() - - // Create a Core listener to listen for the callback we need - // In this case, we want to know about the account registration status - - mRegistrationDelegate = - CoreDelegateStub( - onConfiguringStatus: {(_: Core, state: Config.ConfiguringState, message: String) in - NSLog("New configuration state is \(state) = \(message)\n") - if state == .Successful { + coreQueue.async { + let configDir = Factory.Instance.getConfigDir(context: nil) + try? self.mCore = Factory.Instance.createCore(configPath: "\(configDir)/MyConfig", factoryConfigPath: "", systemContext: nil) + self.mCore.autoIterateEnabled = false + self.mCore.friendsDatabasePath = "\(configDir)/friends.db" + + self.mCore.publisher?.onGlobalStateChanged?.postOnMainQueue { (cbVal: (core: Core, state: GlobalState, message: String)) in + if cbVal.state == GlobalState.On { + self.defaultAccount = self.mCore.defaultAccount + } else if cbVal.state == GlobalState.Off { + self.defaultAccount = nil + } + } + try? self.mCore.start() + + // Create a Core listener to listen for the callback we need + // In this case, we want to know about the account registration status + self.mCore.publisher?.onConfiguringStatus?.postOnMainQueue { (cbVal: (core: Core, status: Config.ConfiguringState, message: String)) in + NSLog("New configuration state is \(cbVal.status) = \(cbVal.message)\n") + if cbVal.status == Config.ConfiguringState.Successful { self.toastMessage = "Successful" } else { self.toastMessage = "Failed" } - }, + } - onAccountRegistrationStateChanged: {(_: Core, account: Account, state: RegistrationState, message: String) in + self.mCore.publisher?.onAccountRegistrationStateChanged?.postOnMainQueue { (cbVal: (core: Core, account: Account, state: RegistrationState, message: String)) in // If account has been configured correctly, we will go through Progress and Ok states // Otherwise, we will be Failed. - NSLog("New registration state is \(state) for user id \( String(describing: account.params?.identityAddress?.asString())) = \(message)\n") - if state == .Ok { + NSLog("New registration state is \(cbVal.state) for user id " + + "\( String(describing: cbVal.account.params?.identityAddress?.asString())) = \(cbVal.message)\n") + if cbVal.state == .Ok { self.loggingInProgress = false self.loggedIn = true - } else if state == .Progress { + } else if cbVal.state == .Progress { self.loggingInProgress = true } else { self.toastMessage = "Registration failed" self.loggingInProgress = false self.loggedIn = false - - let params = account.params + } + }.postOnCoreQueue { (cbVal: (core: Core, account: Account, state: RegistrationState, message: String)) in + // If registration failed, remove account from core + if cbVal.state != .Ok && cbVal.state != .Progress { + let params = cbVal.account.params let clonedParams = params?.clone() clonedParams?.registerEnabled = false - account.params = clonedParams + cbVal.account.params = clonedParams - self.mCore!.removeAccount(account: account) - self.mCore!.clearAccounts() - self.mCore!.clearAllAuthInfo() + cbVal.core.removeAccount(account: cbVal.account) + cbVal.core.clearAccounts() + cbVal.core.clearAllAuthInfo() } } - ) - - mCore.addDelegate(delegate: mRegistrationDelegate) + + self.mIteratePublisher = Timer.publish(every: 0.02, on: .main, in: .common) + .autoconnect() + .receive(on: coreQueue) + .sink { _ in + self.mCore.iterate() + } + + } } } + +// swiftlint:enable large_tuple diff --git a/Linphone/LinphoneApp.swift b/Linphone/LinphoneApp.swift index b507a0c87..17257a99f 100644 --- a/Linphone/LinphoneApp.swift +++ b/Linphone/LinphoneApp.swift @@ -32,10 +32,10 @@ struct LinphoneApp: App { if isActive { if !sharedMainViewModel.welcomeViewDisplayed { WelcomeView(sharedMainViewModel: sharedMainViewModel) - } else if coreContext.mCore.defaultAccount == nil || sharedMainViewModel.displayProfileMode { + } else if coreContext.defaultAccount == nil || sharedMainViewModel.displayProfileMode { AssistantView(sharedMainViewModel: sharedMainViewModel) .toast(isShowing: $coreContext.toastMessage) - } else if coreContext.mCore.defaultAccount != nil { + } else if coreContext.defaultAccount != nil { ContentView(contactViewModel: ContactViewModel(), historyViewModel: HistoryViewModel()) .toast(isShowing: $coreContext.toastMessage) } diff --git a/Linphone/SplashScreen.swift b/Linphone/SplashScreen.swift index 64d59e258..350fc93ad 100644 --- a/Linphone/SplashScreen.swift +++ b/Linphone/SplashScreen.swift @@ -40,7 +40,7 @@ struct SplashScreen: View { .ignoresSafeArea(.all) .onAppear { Task { - try await coreContext.initialiseCore() + try coreContext.initialiseCore() withAnimation { self.isActive = true } diff --git a/Linphone/UI/Assistant/Viewmodel/AccountLoginViewModel.swift b/Linphone/UI/Assistant/Viewmodel/AccountLoginViewModel.swift index 6f778145a..d85b4233e 100644 --- a/Linphone/UI/Assistant/Viewmodel/AccountLoginViewModel.swift +++ b/Linphone/UI/Assistant/Viewmodel/AccountLoginViewModel.swift @@ -33,83 +33,95 @@ class AccountLoginViewModel: ObservableObject { init() {} func login() { - do { - // Get the transport protocol to use. - // TLS is strongly recommended - // Only use UDP if you don't have the choice - var transport: TransportType - if transportType == "TLS" { - transport = TransportType.Tls - } else if transportType == "TCP" { - transport = TransportType.Tcp - } else { transport = TransportType.Udp } - - // To configure a SIP account, we need an Account object and an AuthInfo object - // The first one is how to connect to the proxy server, the second one stores the credentials - - // The auth info can be created from the Factory as it's only a data class - // userID is set to null as it's the same as the username in our case - // ha1 is set to null as we are using the clear text password. Upon first register, the hash will be computed automatically. - // The realm will be determined automatically from the first register, as well as the algorithm - let authInfo = try Factory.Instance.createAuthInfo(username: username, userid: "", passwd: passwd, ha1: "", realm: "", domain: domain) - - // Account object replaces deprecated ProxyConfig object - // Account object is configured through an AccountParams object that we can obtain from the Core - - let accountParams = try coreContext.mCore!.createAccountParams() - - // A SIP account is identified by an identity address that we can construct from the username and domain - let identity = try Factory.Instance.createAddress(addr: String("sip:" + username + "@" + domain)) - try accountParams.setIdentityaddress(newValue: identity) - - // We also need to configure where the proxy server is located - let address = try Factory.Instance.createAddress(addr: String("sip:" + domain)) - - // We use the Address object to easily set the transport protocol - try address.setTransport(newValue: transport) - try accountParams.setServeraddress(newValue: address) - // And we ensure the account will start the registration process - accountParams.registerEnabled = true - - // Now that our AccountParams is configured, we can create the Account object - let account = try coreContext.mCore!.createAccount(params: accountParams) - - // Now let's add our objects to the Core - coreContext.mCore!.addAuthInfo(info: authInfo) - try coreContext.mCore!.addAccount(account: account) - - // Also set the newly added account as default - coreContext.mCore!.defaultAccount = account - - } catch { NSLog(error.localizedDescription) } + coreContext.doOnCoreQueue { core in + do { + // Get the transport protocol to use. + // TLS is strongly recommended + // Only use UDP if you don't have the choice + var transport: TransportType + if self.transportType == "TLS" { + transport = TransportType.Tls + } else if self.transportType == "TCP" { + transport = TransportType.Tcp + } else { transport = TransportType.Udp } + + // To configure a SIP account, we need an Account object and an AuthInfo object + // The first one is how to connect to the proxy server, the second one stores the credentials + + // The auth info can be created from the Factory as it's only a data class + // userID is set to null as it's the same as the username in our case + // ha1 is set to null as we are using the clear text password. Upon first register, the hash will be computed automatically. + // The realm will be determined automatically from the first register, as well as the algorithm + let authInfo = try Factory.Instance.createAuthInfo(username: self.username, userid: "", passwd: self.passwd, ha1: "", realm: "", domain: self.domain) + + // Account object replaces deprecated ProxyConfig object + // Account object is configured through an AccountParams object that we can obtain from the Core + + let accountParams = try core.createAccountParams() + + // A SIP account is identified by an identity address that we can construct from the username and domain + let identity = try Factory.Instance.createAddress(addr: String("sip:" + self.username + "@" + self.domain)) + try accountParams.setIdentityaddress(newValue: identity) + + // We also need to configure where the proxy server is located + let address = try Factory.Instance.createAddress(addr: String("sip:" + self.domain)) + + // We use the Address object to easily set the transport protocol + try address.setTransport(newValue: transport) + try accountParams.setServeraddress(newValue: address) + // And we ensure the account will start the registration process + accountParams.registerEnabled = true + + // Now that our AccountParams is configured, we can create the Account object + let account = try core.createAccount(params: accountParams) + + // Now let's add our objects to the Core + core.addAuthInfo(info: authInfo) + try core.addAccount(account: account) + + // Also set the newly added account as default + core.defaultAccount = account + DispatchQueue.main.async { + self.coreContext.defaultAccount = account + } + + } catch { NSLog(error.localizedDescription) } + } } func unregister() { - // Here we will disable the registration of our Account - if let account = coreContext.mCore!.defaultAccount { - - let params = account.params - // Returned params object is const, so to make changes we first need to clone it - let clonedParams = params?.clone() - - // Now let's make our changes - clonedParams?.registerEnabled = false - - // And apply them - account.params = clonedParams + coreContext.doOnCoreQueue { core in + // Here we will disable the registration of our Account + if let account = core.defaultAccount { + + let params = account.params + // Returned params object is const, so to make changes we first need to clone it + let clonedParams = params?.clone() + + // Now let's make our changes + clonedParams?.registerEnabled = false + + // And apply them + account.params = clonedParams + } } } func delete() { - // To completely remove an Account - if let account = coreContext.mCore!.defaultAccount { - coreContext.mCore!.removeAccount(account: account) - - // To remove all accounts use - coreContext.mCore!.clearAccounts() - - // Same for auth info - coreContext.mCore!.clearAllAuthInfo() + coreContext.doOnCoreQueue { core in + // To completely remove an Account + if let account = core.defaultAccount { + core.removeAccount(account: account) + DispatchQueue.main.async { + self.coreContext.defaultAccount = nil + } + + // To remove all accounts use + core.clearAccounts() + + // Same for auth info + core.clearAllAuthInfo() + } } } } diff --git a/Linphone/UI/Assistant/Viewmodel/QRScanner.swift b/Linphone/UI/Assistant/Viewmodel/QRScanner.swift index d5dd78159..5d546ef96 100644 --- a/Linphone/UI/Assistant/Viewmodel/QRScanner.swift +++ b/Linphone/UI/Assistant/Viewmodel/QRScanner.swift @@ -70,14 +70,11 @@ class Coordinator: NSObject, AVCaptureMetadataOutputObjectsDelegate { if let url = NSURL(string: result) { if UIApplication.shared.canOpenURL(url as URL) { lastResult = result - do { - try coreContext.mCore.setProvisioninguri(newValue: result) - coreContext.mCore.stop() - try coreContext.mCore.start() - } catch { - + coreContext.doOnCoreQueue { core in + try? core.setProvisioninguri(newValue: result) + core.stop() + try? core.start() } - } else { coreContext.toastMessage = "Invalide URI" } diff --git a/Linphone/UI/Main/ContentView.swift b/Linphone/UI/Main/ContentView.swift index 2be1c0356..913b29b45 100644 --- a/Linphone/UI/Main/ContentView.swift +++ b/Linphone/UI/Main/ContentView.swift @@ -416,11 +416,11 @@ struct ContentView: View { if isShowDeletePopup { PopupView(sharedMainViewModel: SharedMainViewModel(), isShowPopup: $isShowDeletePopup, title: Text( - contactViewModel.selectedFriend != nil - ? "Delete \(contactViewModel.selectedFriend!.name!)?" - : (contactViewModel.displayedFriend != nil - ? "Delete \(contactViewModel.displayedFriend!.name!)?" - : "Error Name")), + contactViewModel.selectedFriend != nil + ? "Delete \(contactViewModel.selectedFriend!.name!)?" + : (contactViewModel.displayedFriend != nil + ? "Delete \(contactViewModel.displayedFriend!.name!)?" + : "Error Name")), content: Text("This contact will be deleted definitively."), titleFirstButton: Text("Cancel"), actionFirstButton: {self.isShowDeletePopup.toggle()}, diff --git a/Linphone/Utils/MagicSearchSingleton.swift b/Linphone/Utils/MagicSearchSingleton.swift index 2ee38c8cc..50c9f02e9 100644 --- a/Linphone/Utils/MagicSearchSingleton.swift +++ b/Linphone/Utils/MagicSearchSingleton.swift @@ -25,9 +25,8 @@ final class MagicSearchSingleton: ObservableObject { private var coreContext = CoreContext.shared private var magicSearch: MagicSearch! - var magicSearchDelegate: MagicSearchDelegate? - @objc var currentFilter: String = "" + var currentFilter: String = "" var previousFilter: String? var needUpdateLastSearchContacts = false @@ -35,36 +34,45 @@ final class MagicSearchSingleton: ObservableObject { @Published var lastSearch: [SearchResult] = [] private var limitSearchToLinphoneAccounts = true - - @Published var allContact = false - private var domainDefaultAccount = "" + + @Published var allContact = false + private var domainDefaultAccount = "" private init() { - domainDefaultAccount = coreContext.mCore.defaultAccount!.params!.domain! - - magicSearch = try? coreContext.mCore.createMagicSearch() - magicSearch.limitedSearch = false - - magicSearchDelegate = MagicSearchDelegateStub(onSearchResultsReceived: { (magicSearch: MagicSearch) in - self.needUpdateLastSearchContacts = true - self.lastSearch = magicSearch.lastSearch - }) - - magicSearch.addDelegate(delegate: magicSearchDelegate!) + coreContext.doOnCoreQueue{ core in + self.domainDefaultAccount = core.defaultAccount?.params?.domain ?? "" + + self.magicSearch = try? core.createMagicSearch() + self.magicSearch.limitedSearch = false + + self.magicSearch.publisher?.onSearchResultsReceived?.postOnMainQueue { (magicSearch: MagicSearch) in + self.needUpdateLastSearchContacts = true + self.lastSearch = magicSearch.lastSearch + } + } } func searchForContacts(sourceFlags: Int) { - if let oldFilter = previousFilter { - if oldFilter.count > currentFilter.count || oldFilter != currentFilter { - magicSearch.resetSearchCache() + coreContext.doOnCoreQueue{ core in + var needResetCache = false + + DispatchQueue.main.sync { + if let oldFilter = self.previousFilter { + if oldFilter.count > self.currentFilter.count || oldFilter != self.currentFilter { + needResetCache = true + } + } + self.previousFilter = self.currentFilter } + if needResetCache { + self.magicSearch.resetSearchCache() + } + + self.magicSearch.getContactsListAsync( + filter: self.currentFilter, + domain: self.allContact ? "" : self.domainDefaultAccount, + sourceFlags: sourceFlags, + aggregation: MagicSearch.Aggregation.Friend) } - previousFilter = currentFilter - - magicSearch.getContactsListAsync( - filter: currentFilter, - domain: allContact ? "" : domainDefaultAccount, - sourceFlags: sourceFlags, - aggregation: MagicSearch.Aggregation.Friend) } }