From 8ee3e46dbc2a05655f29c437b9477b3f0479a2ed Mon Sep 17 00:00:00 2001
From: Christophe Deschamps <christophe.deschamps.work@gmail.com>
Date: Wed, 13 Sep 2023 14:04:51 +0200
Subject: [PATCH] Suggestions from CD for Async Call backs and Core objects
 associated with publishers

---
 .../CallKitTutorial.xcodeproj/project.pbxproj |   4 +
 .../CallKitProviderDelegate.swift             |   4 +-
 .../CallKitTutorial/CallKitTutorial.swift     | 163 ++++++++++--------
 .../CallKitTutorial/LinphoneAsyncHelper.swift | 121 +++++++++++++
 4 files changed, 218 insertions(+), 74 deletions(-)
 create mode 100644 ios/swift/4-CallKitTutorial/CallKitTutorial/LinphoneAsyncHelper.swift

diff --git a/ios/swift/4-CallKitTutorial/CallKitTutorial.xcodeproj/project.pbxproj b/ios/swift/4-CallKitTutorial/CallKitTutorial.xcodeproj/project.pbxproj
index cc25697..f07abb4 100644
--- a/ios/swift/4-CallKitTutorial/CallKitTutorial.xcodeproj/project.pbxproj
+++ b/ios/swift/4-CallKitTutorial/CallKitTutorial.xcodeproj/project.pbxproj
@@ -16,6 +16,7 @@
 		6608A97224E197D5006E6C68 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6608A97024E197D5006E6C68 /* LaunchScreen.storyboard */; };
 		6608A97A24E19817006E6C68 /* CallKitTutorial.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6608A97924E19817006E6C68 /* CallKitTutorial.swift */; };
 		6608A97C24E1981E006E6C68 /* CallKitProviderDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6608A97B24E1981E006E6C68 /* CallKitProviderDelegate.swift */; };
+		C6FFD82E2AB199FA00DB168D /* LinphoneAsyncHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6FFD82D2AB199FA00DB168D /* LinphoneAsyncHelper.swift */; };
 /* End PBXBuildFile section */
 
 /* Begin PBXFileReference section */
@@ -32,6 +33,7 @@
 		6608A97D24E19852006E6C68 /* CallKitTutorial.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = CallKitTutorial.entitlements; sourceTree = "<group>"; };
 		9D767CD0AA573757AD042365 /* Pods-CallKitTutorial.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CallKitTutorial.release.xcconfig"; path = "Target Support Files/Pods-CallKitTutorial/Pods-CallKitTutorial.release.xcconfig"; sourceTree = "<group>"; };
 		C6E14A45B7F8F19111223509 /* Pods_CallKitTutorial.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_CallKitTutorial.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		C6FFD82D2AB199FA00DB168D /* LinphoneAsyncHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinphoneAsyncHelper.swift; sourceTree = "<group>"; };
 		D7FA91A3C73CAEE3300CB14C /* Pods-CallKitTutorial.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CallKitTutorial.debug.xcconfig"; path = "Target Support Files/Pods-CallKitTutorial/Pods-CallKitTutorial.debug.xcconfig"; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
@@ -78,6 +80,7 @@
 			isa = PBXGroup;
 			children = (
 				6608A97D24E19852006E6C68 /* CallKitTutorial.entitlements */,
+				C6FFD82D2AB199FA00DB168D /* LinphoneAsyncHelper.swift */,
 				6608A96524E197D5006E6C68 /* AppDelegate.swift */,
 				6608A96724E197D5006E6C68 /* SceneDelegate.swift */,
 				6608A97B24E1981E006E6C68 /* CallKitProviderDelegate.swift */,
@@ -225,6 +228,7 @@
 				6608A97A24E19817006E6C68 /* CallKitTutorial.swift in Sources */,
 				6608A96624E197D5006E6C68 /* AppDelegate.swift in Sources */,
 				6608A96824E197D5006E6C68 /* SceneDelegate.swift in Sources */,
+				C6FFD82E2AB199FA00DB168D /* LinphoneAsyncHelper.swift in Sources */,
 				6608A96A24E197D5006E6C68 /* ContentView.swift in Sources */,
 				6608A97C24E1981E006E6C68 /* CallKitProviderDelegate.swift in Sources */,
 			);
diff --git a/ios/swift/4-CallKitTutorial/CallKitTutorial/CallKitProviderDelegate.swift b/ios/swift/4-CallKitTutorial/CallKitTutorial/CallKitProviderDelegate.swift
index fb9101b..97fffc8 100644
--- a/ios/swift/4-CallKitTutorial/CallKitTutorial/CallKitProviderDelegate.swift
+++ b/ios/swift/4-CallKitTutorial/CallKitTutorial/CallKitProviderDelegate.swift
@@ -109,14 +109,14 @@ extension CallKitProviderDelegate: CXProviderDelegate {
 		NSLog("Callkit  didActivateaudiosession -- Is main thread ? \(Thread.isMainThread ? "yes" : "no") ")
 		// The linphone Core must be notified that CallKit has activated the AVAudioSession
 		// in order to start streaming audio.
-		tutorialContext.postOnCoreQueue {
+		tutorialContext.linphoneAsyncHelper.postOnCoreQueue {
 			self.tutorialContext.mCore.activateAudioSession(actived: true)
 		}
 	}
 	
 	func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
 		// The linphone Core must be notified that CallKit has deactivated the AVAudioSession.
-		tutorialContext.postOnCoreQueue {
+		tutorialContext.linphoneAsyncHelper.postOnCoreQueue {
 			self.tutorialContext.mCore.activateAudioSession(actived: false)
 		}
 	}
diff --git a/ios/swift/4-CallKitTutorial/CallKitTutorial/CallKitTutorial.swift b/ios/swift/4-CallKitTutorial/CallKitTutorial/CallKitTutorial.swift
index cd6394b..a0a144c 100644
--- a/ios/swift/4-CallKitTutorial/CallKitTutorial/CallKitTutorial.swift
+++ b/ios/swift/4-CallKitTutorial/CallKitTutorial/CallKitTutorial.swift
@@ -8,17 +8,15 @@
 
 import linphonesw
 import AVFoundation
-
+import Combine
 
 
 class CallKitExampleContext : ObservableObject
 {
-	private let queue = DispatchQueue(label:"core.queue")
 	var mCore: Core!
 	var mAccount: Account?
-	var mCoreDelegate : CoreDelegate!
 	var mIterateTimer : Timer!
-	
+
 	@Published var coreVersion: String = Core.getVersion
 	@Published var username : String = "quentindev"
 	@Published var passwd : String = "dev"
@@ -32,101 +30,122 @@ class CallKitExampleContext : ObservableObject
 	@Published var isSpeakerEnabled : Bool = false
 	@Published var isMicrophoneEnabled : Bool = false
 	
+	/* Async */
+	let linphoneAsyncHelper = LinphoneAsyncHelper()
+
+	
 	/*------------ Callkit tutorial related variables ---------------*/
 	let incomingCallName = "Incoming call example"
 	var mCall : Call?
 	var mProviderDelegate : CallKitProviderDelegate!
 	var mCallAlreadyStopped : Bool = false;
+
 	
-	func postOnCoreQueue(lambda : @escaping ()->()) {
-		queue.async {
-			lambda()
-		}
+	func addRegistrationStateCallBack(core:Core) {
+		core.createAccountRegistrationStateChangedPublisher()
+			.postOnMainQueue { result in
+				NSLog("New registration state is \(result.state) for user id \( String(describing: result.account.params?.identityAddress?.asString()))\n")
+				if (result.state == .Ok) {
+					self.loggedIn = true
+					// Since core has "Push Enabled", the reception and setting of the push notification token is done automatically
+					// It should have been set and used when we log in, you can check here or in the liblinphone logs
+					NSLog("Account registered Push voip token: \(result.account.params?.pushNotificationConfig?.voipToken)")
+				} else if (result.state == .Cleared) {
+					self.loggedIn = false
+				}
+			}
+			.postOnCoreQueue{ result in
+				// optional something on core queue if needed
+ 			}
 	}
 	
-	func postOnMainQueue(lambda : @escaping()->()) {
-		DispatchQueue.main.async {
-			lambda()
-		}
+	
+	func addCallStateChangedCallBack(core:Core) {
+		core.createOnCallStateChangedPublisher()
+			.postOnMainQueue { result in
+				self.callMsg = result.message
+				if (result.state == .PushIncomingReceived){
+					// We're being called by someone (and app is in background)
+					self.mCall = result.call
+					self.mProviderDelegate.incomingCall()
+					self.isCallIncoming = true
+					self.callMsg = result.message
+				} else if (result.state == .IncomingReceived) {
+					// If app is in foreground, it's likely that we will receive the SIP invite before the Push notification
+					if (!self.isCallIncoming) {
+						self.mCall = result.call
+						self.mProviderDelegate.incomingCall()
+						
+						self.isCallIncoming = true
+						self.callMsg = result.message
+					}
+					self.remoteAddress = result.call.remoteAddress!.asStringUriOnly()
+				} else if (result.state == .Connected) {
+					self.isCallIncoming = false
+					self.isCallRunning = true
+				} else if (result.state == .Released || result.state == .End || result.state == .Error) {
+					// Call has been terminated by any side
+					
+					// Report to CallKit that the call is over, if the terminate action was initiated by other end of the call
+					if (self.isCallRunning) {
+						self.mProviderDelegate.stopCall()
+					}
+					self.remoteAddress = "Nobody yet"
+				}
+			}
+			.postOnCoreQueue{ result in
+				// optional something on core queue if needed
+			}
 	}
 	
 	init()
 	{
 		LoggingService.Instance.logLevel = LogLevel.Debug
 		
-		let factory = Factory.Instance
 		// IMPORTANT : In this tutorial, we require the use of a core configuration file.
 		// This way, once the registration is done, and until it is cleared, it will return to the LoggedIn state on launch.
 		// This allows us to have a functional call when the app was closed and is started by a VOIP push notification (incoming call
 		// We also need to enable "Push Notitifications" and "Background Mode - Voice Over IP"
-		let configDir = factory.getConfigDir(context: nil)
-		try? mCore = factory.createCore(configPath: "\(configDir)/MyConfig", factoryConfigPath: "", systemContext: nil)
-		// enabling push notifications management in the core
-		mCore.callkitEnabled = true
-		mCore.pushNotificationEnabled = true
-		mCore.autoIterateEnabled = false
-		
 		
-		mCoreDelegate = CoreDelegateStub( onCallStateChanged: { (core: Core, call: Call, state: Call.State, message: String) in
-			self.callMsg = message
-			if (state == .PushIncomingReceived){
-				// We're being called by someone (and app is in background)
-				self.mCall = call
-				self.mProviderDelegate.incomingCall()
-				self.isCallIncoming = true
-				self.callMsg = message
-			} else if (state == .IncomingReceived) {
-				// If app is in foreground, it's likely that we will receive the SIP invite before the Push notification
-				if (!self.isCallIncoming) {
-					self.mCall = call
-					self.mProviderDelegate.incomingCall()
-					
-					self.isCallIncoming = true
-					self.callMsg = message
+		linphoneAsyncHelper.postOnCoreQueue {
+			let factory = Factory.Instance
+			let configDir = factory.getConfigDir(context: nil)
+			let corePublisher = self.linphoneAsyncHelper.createLinphoneObjectWithPublisher(createAction: {
+				try factory.createCore(configPath: "\(configDir)/MyConfig", factoryConfigPath: "", systemContext: nil)
+			})
+			corePublisher
+				.postOnCoreQueue (
+				onError: { error in
+					NSLog("failed creating core \(error)")
+				},
+				receiveValue: { core in
+					self.mCore = core
+					// enabling push notifications management in the core
+					self.mCore.callkitEnabled = true
+					self.mCore.pushNotificationEnabled = true
+					self.mCore.autoIterateEnabled = false
+					self.addRegistrationStateCallBack(core: core)
+					self.addCallStateChangedCallBack(core: core)
+					try? core.start()
+				})
+				.postOnMainQueue { core in
+				self.mIterateTimer = Timer.scheduledTimer(withTimeInterval: 0.02, repeats: true) { [weak self] timer in
+					self?.linphoneAsyncHelper.postOnCoreQueue {
+						core.iterate()
+					}
 				}
-				self.remoteAddress = call.remoteAddress!.asStringUriOnly()
-			} else if (state == .Connected) {
-				self.isCallIncoming = false
-				self.isCallRunning = true
-			} else if (state == .Released || state == .End || state == .Error) {
-				// Call has been terminated by any side
-				
-				// Report to CallKit that the call is over, if the terminate action was initiated by other end of the call
-				if (self.isCallRunning) {
-					self.mProviderDelegate.stopCall()
-				}
-				self.remoteAddress = "Nobody yet"
-			}
-		}, onAccountRegistrationStateChanged: { (core: Core, account: Account, state: RegistrationState, message: String) in
-			NSLog("New registration state is \(state) for user id \( String(describing: account.params?.identityAddress?.asString()))\n")
-			if (state == .Ok) {
-				self.loggedIn = true
-				// Since core has "Push Enabled", the reception and setting of the push notification token is done automatically
-				// It should have been set and used when we log in, you can check here or in the liblinphone logs
-				NSLog("Account registered Push voip token: \(account.params?.pushNotificationConfig?.voipToken)")
-			} else if (state == .Cleared) {
-				self.loggedIn = false
-			}
-		})
-		
-		mIterateTimer = Timer.scheduledTimer(withTimeInterval: 0.02, repeats: true) { [weak self] timer in
-			self?.postOnCoreQueue {
-				self?.mCore.iterate()
 			}
+			
 		}
-		
+						
 		mProviderDelegate = CallKitProviderDelegate(context: self)
-		mCore.addDelegate(delegate: mCoreDelegate)
 		
-		postOnCoreQueue {
-			try? self.mCore.start()
-		}
 		
 	}
 	
 	func login() {
 
-		postOnCoreQueue {
+		linphoneAsyncHelper.postOnCoreQueue {
 			do {
 				var transport : TransportType
 				if (self.transportType == "TLS") { transport = TransportType.Tls }
@@ -156,7 +175,7 @@ class CallKitExampleContext : ObservableObject
 	
 	func unregister()
 	{
-		postOnCoreQueue {
+		linphoneAsyncHelper.postOnCoreQueue {
 			if let account = self.mCore.defaultAccount {
 				let params = account.params
 				let clonedParams = params?.clone()
@@ -166,7 +185,7 @@ class CallKitExampleContext : ObservableObject
 		}
 	}
 	func delete() {
-		postOnCoreQueue {
+		linphoneAsyncHelper.postOnCoreQueue {
 			if let account = self.mCore.defaultAccount {
 				self.mCore.removeAccount(account: account)
 				self.mCore.clearAccounts()
diff --git a/ios/swift/4-CallKitTutorial/CallKitTutorial/LinphoneAsyncHelper.swift b/ios/swift/4-CallKitTutorial/CallKitTutorial/LinphoneAsyncHelper.swift
new file mode 100644
index 0000000..f957f21
--- /dev/null
+++ b/ios/swift/4-CallKitTutorial/CallKitTutorial/LinphoneAsyncHelper.swift
@@ -0,0 +1,121 @@
+//
+//  LinphoneAsyncWrapper.swift
+//  CallKitTutorial
+//
+//  Created by CD on 13/09/2023.
+//  Copyright © 2023 BelledonneCommunications. All rights reserved.
+//
+
+import Foundation
+import Combine
+import linphonesw
+
+var coreQueue : DispatchQueue = DispatchQueue(label:"core.queue")
+var cancellables = Set<AnyCancellable>()
+
+// A publisher object that can old one or many LinphoneObject objects.
+
+public class LinphoneObjectsPublisher <T> : Publisher {
+	public typealias Output = T
+	public typealias Failure = Error
+	let passThroughSubject = PassthroughSubject<Output, Failure>()
+	public func receive<S>(subscriber: S) where S : Subscriber, S.Failure == Failure, S.Input == Output {
+			passThroughSubject.receive(subscriber: subscriber)
+	}
+	
+	public func send(completion: Subscribers.Completion<Failure>) {
+		passThroughSubject.send(completion: completion)
+	}
+	
+	@discardableResult
+	public func postOnMainQueue(onError :@escaping ((Error) -> Void) = {_ in }, receiveValue:@escaping ((Output) -> Void)) ->  LinphoneObjectsPublisher <T>{
+		doOnQueue(onError,receiveValue,queue: DispatchQueue.main)
+		return self
+	}
+	
+	@discardableResult
+	public func postOnCoreQueue(onError :@escaping ((Error) -> Void) = {_ in }, receiveValue:@escaping ((Output) -> Void)) -> LinphoneObjectsPublisher <T>{
+		doOnQueue(onError,receiveValue,queue: coreQueue)
+		return self
+	}
+	
+	
+	private func doOnQueue(_ onError :@escaping ((Error) -> Void) = {_ in }, _ receiveValue:@escaping ((Output) -> Void), queue:DispatchQueue) {
+		passThroughSubject.receive(on:queue)
+		.sink { error in
+			onError(error as! Error)
+		} receiveValue: { result in
+			receiveValue(result)
+		}.store(in: &cancellables)
+	}
+		
+	var savedReference : Any? = nil // Used when a reference is needed to avoid object from beeing GCd (example delegate stubs)
+	convenience init (reference: Any) {
+		self.init()
+		savedReference = reference
+	}
+}
+
+public class LinphoneAsyncHelper {
+	
+	func postOnCoreQueue(lambda : @escaping ()->()) {
+		coreQueue.async {
+			lambda()
+		}
+	}
+	
+	func postOnMainQueue(lambda : @escaping()->()) {
+		DispatchQueue.main.async {
+			lambda()
+		}
+	}
+	
+	// Creates a publisher from the object created by the action passed as parameter
+	// For example if passed a create core call this function will create the LinphoneObject Core on core queue, and created object will be published through the built publisher
+	func createLinphoneObjectWithPublisher<LinphoneObject>(createAction:@escaping()throws -> LinphoneObject ) -> LinphoneObjectsPublisher<LinphoneObject> {
+		let publisher = LinphoneObjectsPublisher<LinphoneObject>()
+		coreQueue.async {
+			do {
+				publisher.passThroughSubject.send(try createAction())
+			} catch {
+				publisher.send(completion: .failure(error))
+			}
+		}
+		return publisher
+	}
+
+}
+
+extension Core {
+
+	// Methods below would generated by a script similar to the one that creates LinphoneWrapper
+
+	public func createAccountRegistrationStateChangedPublisher() -> LinphoneObjectsPublisher<(core:Core, account:Account, state:RegistrationState, message:String)> {
+		let publisher = LinphoneObjectsPublisher<(core:Core, account:Account, state:RegistrationState, message:String)>()
+		let coreDelegate = CoreDelegateStub (
+		onAccountRegistrationStateChanged: { (core: Core, account: Account, state: RegistrationState, message: String) in
+			publisher.passThroughSubject.send((core,account,state,message))
+		})
+		publisher.savedReference = coreDelegate
+		addDelegate(delegate: coreDelegate)
+		return publisher
+	}
+	
+	public func createOnCallStateChangedPublisher() -> LinphoneObjectsPublisher<(core: Core, call: Call, state: Call.State, message: String)> {
+		let publisher = LinphoneObjectsPublisher<(core: Core, call: Call, state: Call.State, message: String)>()
+		let coreDelegate = CoreDelegateStub (
+			onCallStateChanged: { (core: Core, call: Call, state: Call.State, message: String) in
+				publisher.passThroughSubject.send((core,call,state,message))
+		})
+		publisher.savedReference = coreDelegate
+		addDelegate(delegate: coreDelegate)
+		return publisher
+	}
+	
+	
+	
+	// ...
+}
+
+
+
-- 
GitLab