Add swift tutorials for basic call, voip push and chat features with SDK 5.0

This commit is contained in:
QuentinArguillere
2020-07-24 15:55:28 +02:00
committed by QuentinArguillere
parent 22df57e83d
commit 4a5d86e9aa
107 changed files with 6976 additions and 170 deletions

View File

@@ -0,0 +1,38 @@
//
// AppDelegate.swift
// CallKitTutorial
//
// Created by QuentinArguillere on 10/08/2020.
// Copyright © 2020 BelledonneCommunications. All rights reserved.
//
import UIKit
import SwiftUI
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
@ObservedObject var tutorialContext = CallKitExampleContext()
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
return true
}
// MARK: UISceneSession Lifecycle
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
// Called when a new scene session is being created.
// Use this method to select a configuration to create the new scene with.
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
// Called when the user discards a scene session.
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
}
}

View File

@@ -0,0 +1,98 @@
{
"images" : [
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "20x20"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "20x20"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "29x29"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "29x29"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "40x40"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "40x40"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "60x60"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "60x60"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "20x20"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "20x20"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "29x29"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "29x29"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "40x40"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "40x40"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "76x76"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "76x76"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "83.5x83.5"
},
{
"idiom" : "ios-marketing",
"scale" : "1x",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" xcode11CocoaTouchSystemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
</document>

View File

@@ -0,0 +1,99 @@
//
// ProviderDelegate.swift
// CallTutorial
//
// Created by QuentinArguillere on 05/08/2020.
// Copyright © 2020 BelledonneCommunications. All rights reserved.
//
import Foundation
import CallKit
import linphonesw
import AVFoundation
class CallKitProviderDelegate : NSObject
{
private let provider: CXProvider
let mCallController = CXCallController()
var tutorialContext : CallKitExampleContext!
var incomingCallUUID : UUID!
init(context: CallKitExampleContext)
{
tutorialContext = context
let providerConfiguration = CXProviderConfiguration(localizedName: Bundle.main.infoDictionary!["CFBundleName"] as! String)
providerConfiguration.supportsVideo = true
providerConfiguration.supportedHandleTypes = [.generic]
providerConfiguration.maximumCallsPerCallGroup = 1
providerConfiguration.maximumCallGroups = 1
provider = CXProvider(configuration: providerConfiguration)
super.init()
provider.setDelegate(self, queue: nil) // The CXProvider delegate will trigger CallKit related callbacks
}
func incomingCall()
{
incomingCallUUID = UUID()
let update = CXCallUpdate()
update.remoteHandle = CXHandle(type:.generic, value: tutorialContext.incomingCallName)
provider.reportNewIncomingCall(with: incomingCallUUID, update: update, completion: { error in }) // Report to CallKit a call is incoming
}
func stopCall()
{
let endCallAction = CXEndCallAction(call: incomingCallUUID)
let transaction = CXTransaction(action: endCallAction)
mCallController.request(transaction, completion: { error in }) // Report to CallKit a call must end
}
}
// In this extension, we implement the action we want to be done when CallKit is notified of something.
// This can happen through the CallKit GUI in the app, or directly in the code (see, incomingCall(), stopCall() functions above)
extension CallKitProviderDelegate: CXProviderDelegate {
func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
do {
if (tutorialContext.mCall?.state != .End && tutorialContext.mCall?.state != .Released) {
try tutorialContext.mCall?.terminate()
}
} catch { NSLog(error.localizedDescription) }
tutorialContext.isCallRunning = false
tutorialContext.isCallIncoming = false
action.fulfill()
}
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
do {
try tutorialContext.mCall?.accept()
tutorialContext.isCallRunning = true
} catch {
print(error)
}
action.fulfill()
}
func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) {}
func provider(_ provider: CXProvider, perform action: CXStartCallAction) {}
func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) {}
func provider(_ provider: CXProvider, perform action: CXPlayDTMFCallAction) {}
func provider(_ provider: CXProvider, timedOutPerforming action: CXAction) {}
func providerDidReset(_ provider: CXProvider) {}
func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
tutorialContext.mCore.activateAudioSession(actived: true)
}
func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
tutorialContext.mCore.activateAudioSession(actived: false)
}
}

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
</dict>
</plist>

View File

@@ -0,0 +1,141 @@
//
// CallExample.swift
// CallTutorial
//
// Created by QuentinArguillere on 31/07/2020.
// Copyright © 2020 BelledonneCommunications. All rights reserved.
//
import linphonesw
import AVFoundation
class CallKitExampleContext : ObservableObject
{
var mCore: Core!
@Published var coreVersion: String = Core.getVersion
var mAccount: Account?
var mCoreDelegate : CoreDelegate!
@Published var username : String = "quentindev"
@Published var passwd : String = "dev"
@Published var domain : String = "sip.linphone.org"
@Published var loggedIn: Bool = false
@Published var transportType : String = "TLS"
@Published var callMsg : String = ""
@Published var isCallIncoming : Bool = false
@Published var isCallRunning : Bool = false
@Published var remoteAddress : String = "Nobody yet"
@Published var isSpeakerEnabled : Bool = false
@Published var isMicrophoneEnabled : Bool = false
/*------------ Callkit tutorial related variables ---------------*/
let incomingCallName = "Incoming call example"
var mCall : Call?
var mProviderDelegate : CallKitProviderDelegate!
var mCallAlreadyStopped : Bool = false;
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)
mProviderDelegate = CallKitProviderDelegate(context: self)
// enabling push notifications management in the core
mCore.callkitEnabled = true
mCore.pushNotificationEnabled = true
try? mCore.start()
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.isCallIncoming = true
self.mProviderDelegate.incomingCall()
} 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.isCallIncoming = true
self.mProviderDelegate.incomingCall()
}
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
}
})
mCore.addDelegate(delegate: mCoreDelegate)
}
func login() {
do {
var transport : TransportType
if (transportType == "TLS") { transport = TransportType.Tls }
else if (transportType == "TCP") { transport = TransportType.Tcp }
else { transport = TransportType.Udp }
let authInfo = try Factory.Instance.createAuthInfo(username: username, userid: "", passwd: passwd, ha1: "", realm: "", domain: domain)
let accountParams = try mCore.createAccountParams()
let identity = try Factory.Instance.createAddress(addr: String("sip:" + username + "@" + domain))
try! accountParams.setIdentityaddress(newValue: identity)
let address = try Factory.Instance.createAddress(addr: String("sip:" + domain))
try address.setTransport(newValue: transport)
try accountParams.setServeraddress(newValue: address)
accountParams.registerEnabled = true
// Enable push notifications on this account
accountParams.pushNotificationAllowed = true
// We're in a sandbox application, so we must set the provider to "apns.dev" since it will be "apns" by default, which is used only for production apps
accountParams.pushNotificationConfig?.provider = "apns.dev"
mAccount = try mCore.createAccount(params: accountParams)
mCore.addAuthInfo(info: authInfo)
try mCore.addAccount(account: mAccount!)
mCore.defaultAccount = mAccount
} catch { NSLog(error.localizedDescription) }
}
func unregister()
{
if let account = mCore.defaultAccount {
let params = account.params
let clonedParams = params?.clone()
clonedParams?.registerEnabled = false
account.params = clonedParams
}
}
func delete() {
if let account = mCore.defaultAccount {
mCore.removeAccount(account: account)
mCore.clearAccounts()
mCore.clearAllAuthInfo()
}
}
}

View File

@@ -0,0 +1,116 @@
//
// ContentView.swift
// CallKit tutorial
//
// Created by QuentinArguillere on 09/09/2021.
// Copyright © 2021 BelledonneCommunications. All rights reserved.
//
import SwiftUI
struct ContentView: View {
@ObservedObject var tutorialContext : CallKitExampleContext
func callStateString() -> String {
if (tutorialContext.isCallRunning) {
return "Call running"
} else if (tutorialContext.isCallIncoming) {
return "Incoming call"
} else {
return "No Call"
}
}
var body: some View {
VStack {
Group {
HStack {
Text("Username:")
.font(.title)
TextField("", text : $tutorialContext.username)
.textFieldStyle(RoundedBorderTextFieldStyle())
.disabled(tutorialContext.loggedIn)
}
HStack {
Text("Password:")
.font(.title)
TextField("", text : $tutorialContext.passwd)
.textFieldStyle(RoundedBorderTextFieldStyle())
.disabled(tutorialContext.loggedIn)
}
HStack {
Text("Domain:")
.font(.title)
TextField("", text : $tutorialContext.domain)
.textFieldStyle(RoundedBorderTextFieldStyle())
.disabled(tutorialContext.loggedIn)
}
Picker(selection: $tutorialContext.transportType, label: Text("Transport:")) {
Text("TLS").tag("TLS")
Text("TCP").tag("TCP")
Text("UDP").tag("UDP")
}.pickerStyle(SegmentedPickerStyle()).padding()
VStack {
HStack {
Button(action: {
if (self.tutorialContext.loggedIn)
{
self.tutorialContext.unregister()
self.tutorialContext.delete()
} else {
self.tutorialContext.login()
}
})
{
Text(tutorialContext.loggedIn ? "Log out & \ndelete account" : "Create & \nlog in account")
.font(.largeTitle)
.foregroundColor(Color.white)
.frame(width: 220.0, height: 90)
.background(Color.gray)
}
}
HStack {
Text("Login State : ")
.font(.footnote)
Text(tutorialContext.loggedIn ? "Looged in" : "Unregistered")
.font(.footnote)
.foregroundColor(tutorialContext.loggedIn ? Color.green : Color.black)
}.padding(.top, 10.0)
}
VStack {
HStack {
Text("Caller:").font(.title).underline()
Text(tutorialContext.remoteAddress)
Spacer()
}.padding(.top, 5)
HStack {
Text("Call msg:").font(.title3).underline()
Text(tutorialContext.callMsg)
Spacer()
}.padding(.top, 5)
}.padding(.top, 30)
Button(action: tutorialContext.mProviderDelegate.stopCall)
{
Text("End call").font(.largeTitle)
.foregroundColor(Color.white)
.frame(width: 120.0, height: 42.0)
.background(Color.gray)
}.disabled(!tutorialContext.isCallRunning)
.padding(.top, 10)
}
Group {
Spacer()
Text("Core Version is \(tutorialContext.coreVersion)")
}
}
.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(tutorialContext: CallKitExampleContext())
}
}

View File

@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSApplicationCategoryType</key>
<string></string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSCameraUsageDescription</key>
<string>Camera access</string>
<key>NSMicrophoneUsageDescription</key>
<string>Microphone access</string>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneConfigurationName</key>
<string>Default Configuration</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
</dict>
</array>
</dict>
</dict>
<key>UIBackgroundModes</key>
<array>
<string>voip</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,66 @@
//
// SceneDelegate.swift
// CallKitTutorial
//
// Created by QuentinArguillere on 10/08/2020.
// Copyright © 2020 BelledonneCommunications. All rights reserved.
//
import UIKit
import SwiftUI
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
// Create the SwiftUI view that provides the window contents.
let delegate = UIApplication.shared.delegate as! AppDelegate
let contentView = ContentView(tutorialContext: delegate.tutorialContext)
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
func sceneDidDisconnect(_ scene: UIScene) {
// Called as the scene is being released by the system.
// This occurs shortly after the scene enters the background, or when its session is discarded.
// Release any resources associated with this scene that can be re-created the next time the scene connects.
// The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead).
}
func sceneDidBecomeActive(_ scene: UIScene) {
// Called when the scene has moved from an inactive state to an active state.
// Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
}
func sceneWillResignActive(_ scene: UIScene) {
// Called when the scene will move from an active state to an inactive state.
// This may occur due to temporary interruptions (ex. an incoming phone call).
}
func sceneWillEnterForeground(_ scene: UIScene) {
// Called as the scene transitions from the background to the foreground.
// Use this method to undo the changes made on entering the background.
}
func sceneDidEnterBackground(_ scene: UIScene) {
// Called as the scene transitions from the foreground to the background.
// Use this method to save data, release shared resources, and store enough scene-specific state information
// to restore the scene back to its current state.
}
}