Merge pull request #2001 from simplex-chat/callkit

iOS: native calls using WebRTC library and CallKit
This commit is contained in:
Evgeny Poberezkin
2023-03-19 12:25:44 +00:00
committed by GitHub
23 changed files with 695 additions and 242 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "60.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "120.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "180.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
+101 -28
View File
@@ -6,14 +6,18 @@
//
import SwiftUI
import Intents
import SimpleXChat
struct ContentView: View {
@EnvironmentObject var chatModel: ChatModel
@ObservedObject var alertManager = AlertManager.shared
@ObservedObject var callController = CallController.shared
@Environment(\.colorScheme) var colorScheme
@Binding var doAuthenticate: Bool
@Binding var userAuthorized: Bool?
@Binding var canConnectCall: Bool
@Binding var lastSuccessfulUnlock: TimeInterval?
@AppStorage(DEFAULT_SHOW_LA_NOTICE) private var prefShowLANotice = false
@AppStorage(DEFAULT_LA_NOTICE_SHOWN) private var prefLANoticeShown = false
@AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false
@@ -23,41 +27,64 @@ struct ContentView: View {
var body: some View {
ZStack {
if prefPerformLA && userAuthorized != true {
Button(action: runAuthenticate) { Label("Unlock", systemImage: "lock") }
} else if let status = chatModel.chatDbStatus, status != .ok {
DatabaseErrorView(status: status)
} else if !chatModel.v3DBMigration.startChat {
MigrateToAppGroupView()
} else if let step = chatModel.onboardingStage {
if case .onboardingComplete = step,
chatModel.currentUser != nil {
mainView().privacySensitive(protectScreen)
} else {
OnboardingView(onboarding: step)
}
contentView()
if chatModel.showCallView, let call = chatModel.activeCall {
callView(call)
}
}
.onAppear {
if doAuthenticate { runAuthenticate() }
if prefPerformLA { requestNtfAuthorization() }
initAuthenticate()
}
.onChange(of: doAuthenticate) { _ in
initAuthenticate()
}
.onChange(of: doAuthenticate) { _ in if doAuthenticate { runAuthenticate() } }
.alert(isPresented: $alertManager.presentAlert) { alertManager.alertView! }
}
@ViewBuilder private func contentView() -> some View {
if prefPerformLA && userAuthorized != true {
lockButton()
} else if let status = chatModel.chatDbStatus, status != .ok {
DatabaseErrorView(status: status)
} else if !chatModel.v3DBMigration.startChat {
MigrateToAppGroupView()
} else if let step = chatModel.onboardingStage {
if case .onboardingComplete = step,
chatModel.currentUser != nil {
mainView()
} else {
OnboardingView(onboarding: step)
}
}
}
@ViewBuilder private func callView(_ call: Call) -> some View {
if CallController.useCallKit() {
ActiveCallView(call: call, canConnectCall: Binding.constant(true))
.onDisappear {
if userAuthorized == false && doAuthenticate { runAuthenticate() }
}
} else {
ActiveCallView(call: call, canConnectCall: $canConnectCall)
if prefPerformLA && userAuthorized != true {
Rectangle()
.fill(colorScheme == .dark ? .black : .white)
.frame(maxWidth: .infinity, maxHeight: .infinity)
lockButton()
}
}
}
private func lockButton() -> some View {
Button(action: runAuthenticate) { Label("Unlock", systemImage: "lock") }
}
private func mainView() -> some View {
ZStack(alignment: .top) {
ChatListView()
ChatListView().privacySensitive(protectScreen)
.onAppear {
NtfManager.shared.requestAuthorization(
onDeny: {
if (!notificationAlertShown) {
notificationAlertShown = true
alertManager.showAlert(notificationAlert())
}
},
onAuthorized: { notificationAlertShown = false }
)
if !prefPerformLA { requestNtfAuthorization() }
// Local Authentication notice is to be shown on next start after onboarding is complete
if (!prefLANoticeShown && prefShowLANotice && !chatModel.chats.isEmpty) {
prefLANoticeShown = true
@@ -74,11 +101,42 @@ struct ContentView: View {
.sheet(isPresented: $showWhatsNew) {
WhatsNewView()
}
if chatModel.showCallView, let call = chatModel.activeCall {
ActiveCallView(call: call)
}
IncomingCallView()
}
.onContinueUserActivity("INStartCallIntent", perform: processUserActivity)
.onContinueUserActivity("INStartAudioCallIntent", perform: processUserActivity)
.onContinueUserActivity("INStartVideoCallIntent", perform: processUserActivity)
}
private func processUserActivity(_ activity: NSUserActivity) {
let intent = activity.interaction?.intent
if let intent = intent as? INStartCallIntent {
callToRecentContact(intent.contacts, intent.callCapability == .videoCall ? .video : .audio)
} else if let intent = intent as? INStartAudioCallIntent {
callToRecentContact(intent.contacts, .audio)
} else if let intent = intent as? INStartVideoCallIntent {
callToRecentContact(intent.contacts, .video)
}
}
private func callToRecentContact(_ contacts: [INPerson]?, _ mediaType: CallMediaType) {
logger.debug("callToRecentContact")
if let contactId = contacts?.first?.personHandle?.value,
let chat = chatModel.getChat(contactId),
case let .direct(contact) = chat.chatInfo {
logger.debug("callToRecentContact: schedule call")
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
CallController.shared.startCall(contact, mediaType)
}
}
}
private func initAuthenticate() {
if CallController.useCallKit() && chatModel.showCallView && chatModel.activeCall != nil {
userAuthorized = false
} else if doAuthenticate {
runAuthenticate()
}
}
private func runAuthenticate() {
@@ -98,16 +156,31 @@ struct ContentView: View {
switch (laResult) {
case .success:
userAuthorized = true
canConnectCall = true
lastSuccessfulUnlock = ProcessInfo.processInfo.systemUptime
case .failed:
break
case .unavailable:
userAuthorized = true
prefPerformLA = false
canConnectCall = true
AlertManager.shared.showAlert(laUnavailableTurningOffAlert())
}
}
}
func requestNtfAuthorization() {
NtfManager.shared.requestAuthorization(
onDeny: {
if (!notificationAlertShown) {
notificationAlertShown = true
alertManager.showAlert(notificationAlert())
}
},
onAuthorized: { notificationAlertShown = false }
)
}
func laNoticeAlert() -> Alert {
Alert(
title: Text("SimpleX Lock"),
-2
View File
@@ -9,7 +9,6 @@
import Foundation
import Combine
import SwiftUI
import WebKit
import SimpleXChat
final class ChatModel: ObservableObject {
@@ -59,7 +58,6 @@ final class ChatModel: ObservableObject {
@Published var stopPreviousRecPlay: Bool = false // value is not taken into account, only the fact it switches
@Published var draft: ComposeState?
@Published var draftChatId: String?
var callWebView: WKWebView?
var messageDelivery: Dictionary<Int64, () -> Void> = [:]
+15 -20
View File
@@ -909,7 +909,7 @@ func apiGetVersion() throws -> CoreVersionInfo {
throw r
}
func initializeChat(start: Bool, dbKey: String? = nil) throws {
func initializeChat(start: Bool, dbKey: String? = nil, refreshInvitations: Bool = true) throws {
logger.debug("initializeChat")
let m = ChatModel.shared
(m.chatDbEncrypted, m.chatDbStatus) = chatMigrateInit(dbKey)
@@ -925,13 +925,13 @@ func initializeChat(start: Bool, dbKey: String? = nil) throws {
if m.currentUser == nil {
m.onboardingStage = .step1_SimpleXInfo
} else if start {
try startChat()
try startChat(refreshInvitations: refreshInvitations)
} else {
m.chatRunning = false
}
}
func startChat() throws {
func startChat(refreshInvitations: Bool = true) throws {
logger.debug("startChat")
let m = ChatModel.shared
try setNetworkConfig(getNetCfg())
@@ -940,7 +940,9 @@ func startChat() throws {
if justStarted {
try getUserChatData()
NtfManager.shared.setNtfBadgeCount(m.totalUnreadCountForAllUsers())
try refreshCallInvitations()
if (refreshInvitations) {
try refreshCallInvitations()
}
(m.savedToken, m.tokenStatus, m.notificationMode) = apiGetNtfToken()
if let token = m.deviceToken {
registerToken(token: token)
@@ -1214,19 +1216,6 @@ func processReceivedMsg(_ res: ChatResponse) async {
case let .callInvitation(invitation):
m.callInvitations[invitation.contact.id] = invitation
activateCall(invitation)
// This will be called from notification service extension
// CXProvider.reportNewIncomingVoIPPushPayload([
// "displayName": contact.displayName,
// "contactId": contact.id,
// "uuid": invitation.callkitUUID
// ]) { error in
// if let error = error {
// logger.error("reportNewIncomingVoIPPushPayload error \(error.localizedDescription)")
// } else {
// logger.debug("reportNewIncomingVoIPPushPayload success for \(contact.id)")
// }
// }
case let .callOffer(_, contact, callType, offer, sharedKey, _):
withCall(contact) { call in
call.callState = .offerReceived
@@ -1259,7 +1248,7 @@ func processReceivedMsg(_ res: ChatResponse) async {
}
withCall(contact) { call in
m.callCommand = .end
// CallController.shared.reportCallRemoteEnded(call: call)
CallController.shared.reportCallRemoteEnded(call: call)
}
case .chatSuspended:
chatSuspended()
@@ -1310,8 +1299,7 @@ func processContactSubError(_ contact: Contact, _ chatError: ChatError) {
func refreshCallInvitations() throws {
let m = ChatModel.shared
let callInvitations = try apiGetCallInvitations()
m.callInvitations = callInvitations.reduce(into: [ChatId: RcvCallInvitation]()) { result, inv in result[inv.contact.id] = inv }
let callInvitations = try justRefreshCallInvitations()
if let (chatId, ntfAction) = m.ntfCallInvitationAction,
let invitation = m.callInvitations.removeValue(forKey: chatId) {
m.ntfCallInvitationAction = nil
@@ -1321,6 +1309,13 @@ func refreshCallInvitations() throws {
}
}
func justRefreshCallInvitations() throws -> [RcvCallInvitation] {
let m = ChatModel.shared
let callInvitations = try apiGetCallInvitations()
m.callInvitations = callInvitations.reduce(into: [ChatId: RcvCallInvitation]()) { result, inv in result[inv.contact.id] = inv }
return callInvitations
}
func activateCall(_ callInvitation: RcvCallInvitation) {
let m = ChatModel.shared
CallController.shared.reportNewIncomingCall(invitation: callInvitation) { error in
+21
View File
@@ -81,3 +81,24 @@ func activateChat(appState: AppState = .active) {
if ChatModel.ok { apiActivateChat() }
}
}
func initChatAndMigrate(refreshInvitations: Bool = true) {
let m = ChatModel.shared
if (!m.chatInitialized) {
do {
m.v3DBMigration = v3DBMigrationDefault.get()
try initializeChat(start: m.v3DBMigration.startChat, refreshInvitations: refreshInvitations)
} catch let error {
fatalError("Failed to start or load chats: \(responseError(error))")
}
}
}
func startChatAndActivate() {
if ChatModel.shared.chatRunning == true {
ChatReceiver.shared.start()
}
if .active != appStateGroupDefault.get() {
activateChat()
}
}
+26 -17
View File
@@ -21,6 +21,8 @@ struct SimpleXApp: App {
@State private var userAuthorized: Bool?
@State private var doAuthenticate = false
@State private var enteredBackground: TimeInterval? = nil
@State private var canConnectCall = false
@State private var lastSuccessfulUnlock: TimeInterval? = nil
init() {
hs_init(0, nil)
@@ -34,44 +36,43 @@ struct SimpleXApp: App {
var body: some Scene {
return WindowGroup {
ContentView(doAuthenticate: $doAuthenticate, userAuthorized: $userAuthorized)
ContentView(doAuthenticate: $doAuthenticate, userAuthorized: $userAuthorized, canConnectCall: $canConnectCall, lastSuccessfulUnlock: $lastSuccessfulUnlock)
.environmentObject(chatModel)
.onOpenURL { url in
logger.debug("ContentView.onOpenURL: \(url)")
chatModel.appOpenUrl = url
}
.onAppear() {
if (!chatModel.chatInitialized) {
do {
chatModel.v3DBMigration = v3DBMigrationDefault.get()
try initializeChat(start: chatModel.v3DBMigration.startChat)
} catch let error {
fatalError("Failed to start or load chats: \(responseError(error))")
}
}
initChatAndMigrate()
}
.onChange(of: scenePhase) { phase in
logger.debug("scenePhase \(String(describing: scenePhase))")
logger.debug("scenePhase was \(String(describing: scenePhase)), now \(String(describing: phase))")
switch (phase) {
case .background:
suspendChat()
BGManager.shared.schedule()
if CallController.useCallKit() && chatModel.activeCall != nil {
CallController.shared.shouldSuspendChat = true
} else {
suspendChat()
BGManager.shared.schedule()
}
if userAuthorized == true {
enteredBackground = ProcessInfo.processInfo.systemUptime
}
doAuthenticate = false
canConnectCall = false
NtfManager.shared.setNtfBadgeCount(chatModel.totalUnreadCountForAllUsers())
case .active:
if chatModel.chatRunning == true {
ChatReceiver.shared.start()
}
CallController.shared.shouldSuspendChat = false
let appState = appStateGroupDefault.get()
activateChat()
startChatAndActivate()
if appState.inactive && chatModel.chatRunning == true {
updateChats()
updateCallInvitations()
if !chatModel.showCallView && !CallController.shared.hasActiveCalls() {
updateCallInvitations()
}
}
doAuthenticate = authenticationExpired()
canConnectCall = !(doAuthenticate && prefPerformLA) || unlockedRecently()
default:
break
}
@@ -111,6 +112,14 @@ struct SimpleXApp: App {
}
}
private func unlockedRecently() -> Bool {
if let lastSuccessfulUnlock = lastSuccessfulUnlock {
return ProcessInfo.processInfo.systemUptime - lastSuccessfulUnlock < 2
} else {
return false
}
}
private func updateChats() {
do {
let chats = try apiGetChats()
+26 -13
View File
@@ -13,9 +13,11 @@ import SimpleXChat
struct ActiveCallView: View {
@EnvironmentObject var m: ChatModel
@ObservedObject var call: Call
@Environment(\.scenePhase) var scenePhase
@State private var client: WebRTCClient? = nil
@State private var activeCall: WebRTCClient.Call? = nil
@State private var localRendererAspectRatio: CGFloat? = nil
@Binding var canConnectCall: Bool
var body: some View {
ZStack(alignment: .bottom) {
@@ -36,12 +38,16 @@ struct ActiveCallView: View {
}
}
.onAppear {
if client == nil {
client = WebRTCClient($activeCall, { msg in await MainActor.run { processRtcMessage(msg: msg) } }, $localRendererAspectRatio)
sendCommandToClient()
}
logger.debug("ActiveCallView: appear client is nil \(client == nil), scenePhase \(String(describing: scenePhase), privacy: .public), canConnectCall \(canConnectCall)")
createWebRTCClient()
dismissAllSheets()
}
.onChange(of: canConnectCall) { _ in
logger.debug("ActiveCallView: canConnectCall changed to \(canConnectCall, privacy: .public)")
createWebRTCClient()
}
.onDisappear {
logger.debug("ActiveCallView: disappear")
client?.endCall()
}
.onChange(of: m.callCommand) { _ in sendCommandToClient()}
@@ -49,6 +55,13 @@ struct ActiveCallView: View {
.preferredColorScheme(.dark)
}
private func createWebRTCClient() {
if client == nil && canConnectCall {
client = WebRTCClient($activeCall, { msg in await MainActor.run { processRtcMessage(msg: msg) } }, $localRendererAspectRatio)
sendCommandToClient()
}
}
private func sendCommandToClient() {
if call == m.activeCall,
m.activeCall != nil,
@@ -117,9 +130,9 @@ struct ActiveCallView: View {
case let .connection(state):
if let callStatus = WebRTCCallStatus.init(rawValue: state.connectionState),
case .connected = callStatus {
// if case .outgoing = call.direction {
// CallController.shared.reportOutgoingCall(call: call, connectedAt: nil)
// }
call.direction == .outgoing
? CallController.shared.reportOutgoingCall(call: call, connectedAt: nil)
: CallController.shared.reportIncomingCall(call: call, connectedAt: nil)
call.callState = .connected
}
if state.connectionState == "closed" {
@@ -252,7 +265,7 @@ struct ActiveCallOverlay: View {
private func endCallButton() -> some View {
let cc = CallController.shared
return callButton("phone.down.fill", size: 60) {
return callButton("phone.down.fill", width: 60, height: 60) {
if let uuid = call.callkitUUID {
cc.endCall(callUUID: uuid)
} else {
@@ -274,7 +287,7 @@ struct ActiveCallOverlay: View {
}
private func toggleSpeakerButton() -> some View {
controlButton(call, call.speakerEnabled ? "speaker.fill" : "speaker.slash") {
controlButton(call, call.speakerEnabled ? "speaker.wave.2.fill" : "speaker.wave.1.fill") {
Task {
client.setSpeakerEnabledAndConfigureSession(!call.speakerEnabled)
DispatchQueue.main.async {
@@ -305,22 +318,22 @@ struct ActiveCallOverlay: View {
@ViewBuilder private func controlButton(_ call: Call, _ imageName: String, _ perform: @escaping () -> Void) -> some View {
if call.hasMedia {
callButton(imageName, size: 40, perform)
callButton(imageName, width: 50, height: 38, perform)
.foregroundColor(.white)
.opacity(0.85)
} else {
Color.clear.frame(width: 40, height: 40)
Color.clear.frame(width: 50, height: 38)
}
}
private func callButton(_ imageName: String, size: CGFloat, _ perform: @escaping () -> Void) -> some View {
private func callButton(_ imageName: String, width: CGFloat, height: CGFloat, _ perform: @escaping () -> Void) -> some View {
Button {
perform()
} label: {
Image(systemName: imageName)
.resizable()
.scaledToFit()
.frame(maxWidth: size, maxHeight: size)
.frame(maxWidth: width, maxHeight: height)
}
}
}
+274 -141
View File
@@ -7,192 +7,315 @@
//
import Foundation
//import CallKit
import CallKit
import StoreKit
import PushKit
import AVFoundation
import SimpleXChat
import WebRTC
//class CallController: NSObject, CXProviderDelegate, ObservableObject {
class CallController: NSObject, ObservableObject {
static let useCallKit = false
class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, ObservableObject {
static let shared = CallController()
// private let provider = CXProvider(configuration: CallController.configuration)
// private let controller = CXCallController()
static let isInChina = SKStorefront().countryCode == "CHN"
static func useCallKit() -> Bool { !isInChina && callKitEnabledGroupDefault.get() }
private let provider = CXProvider(configuration: {
let configuration = CXProviderConfiguration()
configuration.supportsVideo = true
configuration.supportedHandleTypes = [.generic]
configuration.includesCallsInRecents = UserDefaults.standard.bool(forKey: DEFAULT_CALL_KIT_CALLS_IN_RECENTS)
configuration.maximumCallGroups = 1
configuration.maximumCallsPerCallGroup = 1
configuration.iconTemplateImageData = UIImage(named: "icon-transparent")?.pngData()
return configuration
}())
private let controller = CXCallController()
private let callManager = CallManager()
@Published var activeCallInvitation: RcvCallInvitation?
var shouldSuspendChat: Bool = false
var fulfillOnConnect: CXAnswerCallAction? = nil
// PKPushRegistry will be used from notification service extension
// let registry = PKPushRegistry(queue: nil)
// static let configuration: CXProviderConfiguration = {
// let configuration = CXProviderConfiguration()
// configuration.supportsVideo = true
// configuration.supportedHandleTypes = [.generic]
// configuration.includesCallsInRecents = true // TODO disable or add option
// configuration.maximumCallsPerCallGroup = 1
// return configuration
// }()
// PKPushRegistry is used from notification service extension
private let registry = PKPushRegistry(queue: nil)
override init() {
super.init()
// self.provider.setDelegate(self, queue: nil)
// self.registry.delegate = self
// self.registry.desiredPushTypes = [.voIP]
provider.setDelegate(self, queue: nil)
registry.delegate = self
registry.desiredPushTypes = [.voIP]
}
// func providerDidReset(_ provider: CXProvider) {
// }
func providerDidReset(_ provider: CXProvider) {
logger.debug("CallController.providerDidReset")
}
// func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
// logger.debug("CallController.provider CXStartCallAction")
// if callManager.startOutgoingCall(callUUID: action.callUUID) {
// action.fulfill()
// provider.reportOutgoingCall(with: action.callUUID, startedConnectingAt: nil)
// } else {
// action.fail()
// }
// }
func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
logger.debug("CallController.provider CXStartCallAction")
if callManager.startOutgoingCall(callUUID: action.callUUID) {
action.fulfill()
provider.reportOutgoingCall(with: action.callUUID, startedConnectingAt: nil)
} else {
action.fail()
}
}
// func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
// logger.debug("CallController.provider CXAnswerCallAction")
// if callManager.answerIncomingCall(callUUID: action.callUUID) {
// action.fulfill()
// } else {
// action.fail()
// }
// }
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
logger.debug("CallController.provider CXAnswerCallAction")
if callManager.answerIncomingCall(callUUID: action.callUUID) {
// WebRTC call should be in connected state to fulfill.
// Otherwise no audio and mic working on lockscreen
fulfillOnConnect = action
} else {
action.fail()
}
}
// func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
// logger.debug("CallController.provider CXEndCallAction")
// callManager.endCall(callUUID: action.callUUID) { ok in
// if ok {
// action.fulfill()
// } else {
// action.fail()
// }
// }
// }
func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
logger.debug("CallController.provider CXEndCallAction")
// Should be nil here if connection was in connected state
fulfillOnConnect?.fail()
fulfillOnConnect = nil
callManager.endCall(callUUID: action.callUUID) { ok in
if ok {
action.fulfill()
} else {
action.fail()
}
self.suspendOnEndCall()
}
}
// func provider(_ provider: CXProvider, timedOutPerforming action: CXAction) {
// print("timed out", #function)
// action.fulfill()
// }
func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) {
if callManager.enableMedia(media: .audio, enable: !action.isMuted, callUUID: action.callUUID) {
action.fulfill()
} else {
action.fail()
}
}
// func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
// print("received", #function)
//// do {
//// try audioSession.setCategory(.playAndRecord, mode: .voiceChat, options: .mixWithOthers)
//// logger.debug("audioSession category set")
//// try audioSession.setActive(true)
//// logger.debug("audioSession activated")
//// } catch {
//// print(error)
//// logger.error("failed activating audio session")
//// }
// }
func provider(_ provider: CXProvider, timedOutPerforming action: CXAction) {
logger.debug("timed out: \(String(describing: action))")
action.fulfill()
}
// func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
// print("received", #function)
// }
func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
logger.debug("CallController: activating audioSession and audio in WebRTCClient")
RTCAudioSession.sharedInstance().audioSessionDidActivate(audioSession)
RTCAudioSession.sharedInstance().isAudioEnabled = true
do {
try audioSession.setCategory(.playAndRecord, mode: .voiceChat, options: .mixWithOthers)
logger.debug("audioSession category set")
try audioSession.setActive(true)
logger.debug("audioSession activated")
} catch {
print(error)
logger.error("failed activating audio session")
}
}
// func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) {
//
// }
func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
logger.debug("CallController: deactivating audioSession and audio in WebRTCClient")
RTCAudioSession.sharedInstance().audioSessionDidDeactivate(audioSession)
RTCAudioSession.sharedInstance().isAudioEnabled = false
do {
try audioSession.setActive(false)
logger.debug("audioSession deactivated")
} catch {
print(error)
logger.error("failed deactivating audio session")
}
suspendOnEndCall()
}
// This will be needed when we have notification service extension
// func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
// if type == .voIP {
// // Extract the call information from the push notification payload
// if let displayName = payload.dictionaryPayload["displayName"] as? String,
// let contactId = payload.dictionaryPayload["contactId"] as? String,
// let uuidStr = payload.dictionaryPayload["uuid"] as? String,
// let uuid = UUID(uuidString: uuidStr) {
// let callUpdate = CXCallUpdate()
// callUpdate.remoteHandle = CXHandle(type: .phoneNumber, value: displayName)
// provider.reportNewIncomingCall(with: uuid, update: callUpdate, completion: { error in
// if error != nil {
// let m = ChatModel.shared
// m.callInvitations.removeValue(forKey: contactId)
// }
// // Tell PushKit that the notification is handled.
// completion()
// })
// }
// }
// }
func suspendOnEndCall() {
if shouldSuspendChat {
// The delay allows to accept the second call before suspending a chat
// see `.onChange(of: scenePhase)` in SimpleXApp
DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in
logger.debug("CallController: shouldSuspendChat \(String(describing: self?.shouldSuspendChat), privacy: .public)")
if ChatModel.shared.activeCall == nil && self?.shouldSuspendChat == true {
self?.shouldSuspendChat = false
suspendChat()
BGManager.shared.schedule()
}
}
}
}
@objc(pushRegistry:didUpdatePushCredentials:forType:)
func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) {
logger.debug("CallController: didUpdate push credentials for type \(type.rawValue, privacy: .public)")
}
func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
logger.debug("CallController: did receive push with type \(type.rawValue, privacy: .public)")
if type != .voIP {
completion()
return
}
logger.debug("CallController: initializing chat")
if (!ChatModel.shared.chatInitialized) {
initChatAndMigrate(refreshInvitations: false)
}
startChatAndActivate()
shouldSuspendChat = true
// There are no invitations in the model, as it was processed by NSE
_ = try? justRefreshCallInvitations()
// logger.debug("CallController justRefreshCallInvitations: \(String(describing: m.callInvitations))")
// Extract the call information from the push notification payload
let m = ChatModel.shared
if let contactId = payload.dictionaryPayload["contactId"] as? String,
let invitation = m.callInvitations[contactId] {
let update = cxCallUpdate(invitation: invitation)
if let uuid = invitation.callkitUUID {
logger.debug("CallController: report pushkit call via CallKit")
let update = cxCallUpdate(invitation: invitation)
provider.reportNewIncomingCall(with: uuid, update: update) { error in
if error != nil {
m.callInvitations.removeValue(forKey: contactId)
}
// Tell PushKit that the notification is handled.
completion()
}
} else {
reportExpiredCall(update: update, completion)
}
} else {
reportExpiredCall(payload: payload, completion)
}
}
// This function fulfils the requirement to always report a call when PushKit notification is received,
// even when there is no more active calls by the time PushKit payload is processed.
// See the note in the bottom of this article:
// https://developer.apple.com/documentation/pushkit/pkpushregistrydelegate/2875784-pushregistry
private func reportExpiredCall(update: CXCallUpdate, _ completion: @escaping () -> Void) {
logger.debug("CallController: report expired pushkit call via CallKit")
let uuid = UUID()
provider.reportNewIncomingCall(with: uuid, update: update) { error in
if error == nil {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.provider.reportCall(with: uuid, endedAt: nil, reason: .remoteEnded)
}
}
completion()
}
}
private func reportExpiredCall(payload: PKPushPayload, _ completion: @escaping () -> Void) {
let update = CXCallUpdate()
let displayName = payload.dictionaryPayload["displayName"] as? String
let media = payload.dictionaryPayload["media"] as? String
update.localizedCallerName = displayName ?? NSLocalizedString("Unknown caller", comment: "callkit banner")
update.hasVideo = media == CallMediaType.video.rawValue
reportExpiredCall(update: update, completion)
}
func reportNewIncomingCall(invitation: RcvCallInvitation, completion: @escaping (Error?) -> Void) {
logger.debug("CallController.reportNewIncomingCall")
// if CallController.useCallKit, let uuid = invitation.callkitUUID {
// let update = CXCallUpdate()
// update.remoteHandle = CXHandle(type: .generic, value: invitation.contact.displayName)
// update.hasVideo = invitation.peerMedia == .video
// provider.reportNewIncomingCall(with: uuid, update: update, completion: completion)
// } else {
logger.debug("CallController.reportNewIncomingCall, UUID=\(String(describing: invitation.callkitUUID), privacy: .public)")
if CallController.useCallKit(), let uuid = invitation.callkitUUID {
let update = cxCallUpdate(invitation: invitation)
provider.reportNewIncomingCall(with: uuid, update: update, completion: completion)
} else {
NtfManager.shared.notifyCallInvitation(invitation)
if invitation.callTs.timeIntervalSinceNow >= -180 {
activeCallInvitation = invitation
}
// }
}
}
// func reportOutgoingCall(call: Call, connectedAt dateConnected: Date?) {
// if CallController.useCallKit, let uuid = call.callkitUUID {
// provider.reportOutgoingCall(with: uuid, connectedAt: dateConnected)
// }
// }
private func cxCallUpdate(invitation: RcvCallInvitation) -> CXCallUpdate {
let update = CXCallUpdate()
update.remoteHandle = CXHandle(type: .generic, value: invitation.contact.id)
update.hasVideo = invitation.callType.media == .video
update.localizedCallerName = invitation.contact.displayName
return update
}
func reportIncomingCall(call: Call, connectedAt dateConnected: Date?) {
logger.debug("CallController: reporting incoming call connected")
if CallController.useCallKit() {
// Fulfilling this action only after connect, otherwise there are no audio and mic on lockscreen
fulfillOnConnect?.fulfill()
fulfillOnConnect = nil
}
}
func reportOutgoingCall(call: Call, connectedAt dateConnected: Date?) {
logger.debug("CallController: reporting outgoing call connected")
if CallController.useCallKit(), let uuid = call.callkitUUID {
provider.reportOutgoingCall(with: uuid, connectedAt: dateConnected)
}
}
func reportCallRemoteEnded(invitation: RcvCallInvitation) {
// if CallController.useCallKit, let uuid = invitation.callkitUUID {
// provider.reportCall(with: uuid, endedAt: nil, reason: .remoteEnded)
// } else if invitation.contact.id == activeCallInvitation?.contact.id {
logger.debug("CallController: reporting remote ended")
if CallController.useCallKit(), let uuid = invitation.callkitUUID {
provider.reportCall(with: uuid, endedAt: nil, reason: .remoteEnded)
} else if invitation.contact.id == activeCallInvitation?.contact.id {
activeCallInvitation = nil
// }
}
}
// func reportCallRemoteEnded(call: Call) {
// if CallController.useCallKit, let uuid = call.callkitUUID {
// provider.reportCall(with: uuid, endedAt: nil, reason: .remoteEnded)
// }
// }
func reportCallRemoteEnded(call: Call) {
logger.debug("CallController: reporting remote ended")
if CallController.useCallKit(), let uuid = call.callkitUUID {
provider.reportCall(with: uuid, endedAt: nil, reason: .remoteEnded)
}
}
func startCall(_ contact: Contact, _ media: CallMediaType) {
logger.debug("CallController.startCall")
let uuid = callManager.newOutgoingCall(contact, media)
// if CallController.useCallKit {
// let handle = CXHandle(type: .generic, value: contact.displayName)
// let action = CXStartCallAction(call: uuid, handle: handle)
// action.isVideo = media == .video
// requestTransaction(with: action)
// } else if callManager.startOutgoingCall(callUUID: uuid) {
if callManager.startOutgoingCall(callUUID: uuid) {
logger.debug("CallController.startCall: call started")
} else {
logger.error("CallController.startCall: no active call")
if CallController.useCallKit() {
let handle = CXHandle(type: .generic, value: contact.id)
let action = CXStartCallAction(call: uuid, handle: handle)
action.isVideo = media == .video
requestTransaction(with: action) {
let update = CXCallUpdate()
update.remoteHandle = CXHandle(type: .generic, value: contact.id)
update.hasVideo = media == .video
update.localizedCallerName = contact.displayName
self.provider.reportCall(with: uuid, updated: update)
}
} else if callManager.startOutgoingCall(callUUID: uuid) {
if callManager.startOutgoingCall(callUUID: uuid) {
logger.debug("CallController.startCall: call started")
} else {
logger.error("CallController.startCall: no active call")
}
}
}
func answerCall(invitation: RcvCallInvitation) {
callManager.answerIncomingCall(invitation: invitation)
logger.debug("CallController: answering a call")
if CallController.useCallKit(), let callUUID = invitation.callkitUUID {
requestTransaction(with: CXAnswerCallAction(call: callUUID))
} else {
callManager.answerIncomingCall(invitation: invitation)
}
if invitation.contact.id == self.activeCallInvitation?.contact.id {
self.activeCallInvitation = nil
}
}
func endCall(callUUID: UUID) {
// if CallController.useCallKit {
// requestTransaction(with: CXEndCallAction(call: callUUID))
// } else {
logger.debug("CallController: ending the call with UUID \(callUUID.uuidString)")
if CallController.useCallKit() {
requestTransaction(with: CXEndCallAction(call: callUUID))
} else {
callManager.endCall(callUUID: callUUID) { ok in
if ok {
logger.debug("CallController.endCall: call ended")
} else {
logger.error("CallController.endCall: no actove call pr call invitation to end")
logger.error("CallController.endCall: no active call pr call invitation to end")
}
}
// }
}
}
func endCall(invitation: RcvCallInvitation) {
logger.debug("CallController: ending the call with invitation")
callManager.endCall(invitation: invitation) {
if invitation.contact.id == self.activeCallInvitation?.contact.id {
DispatchQueue.main.async {
@@ -203,6 +326,7 @@ class CallController: NSObject, ObservableObject {
}
func endCall(call: Call, completed: @escaping () -> Void) {
logger.debug("CallController: ending the call with call instance")
callManager.endCall(call: call, completed: completed)
}
@@ -213,15 +337,24 @@ class CallController: NSObject, ObservableObject {
}
}
// private func requestTransaction(with action: CXAction) {
// let t = CXTransaction()
// t.addAction(action)
// controller.request(t) { error in
// if let error = error {
// logger.error("CallController.requestTransaction error requesting transaction: \(error.localizedDescription)")
// } else {
// logger.debug("CallController.requestTransaction requested transaction successfully")
// }
// }
// }
func showInRecents(_ show: Bool) {
let conf = provider.configuration
conf.includesCallsInRecents = show
provider.configuration = conf
}
func hasActiveCalls() -> Bool {
controller.callObserver.calls.count > 0
}
private func requestTransaction(with action: CXAction, onSuccess: @escaping () -> Void = {}) {
controller.request(CXTransaction(action: action)) { error in
if let error = error {
logger.error("CallController.requestTransaction error requesting transaction: \(error.localizedDescription, privacy: .public)")
} else {
logger.debug("CallController.requestTransaction requested transaction successfully")
onSuccess()
}
}
}
}
+23 -12
View File
@@ -48,18 +48,31 @@ class CallManager {
sharedKey: invitation.sharedKey
)
call.speakerEnabled = invitation.callType.media == .video
m.activeCall = call
m.showCallView = true
let useRelay = UserDefaults.standard.bool(forKey: DEFAULT_WEBRTC_POLICY_RELAY)
let iceServers = getIceServers()
logger.debug("answerIncomingCall useRelay: \(useRelay)")
logger.debug("answerIncomingCall iceServers: \(String(describing: iceServers))")
m.callCommand = .start(
media: invitation.callType.media,
aesKey: invitation.sharedKey,
iceServers: iceServers,
relay: useRelay
)
// When in active call user wants to accept another call, this can only work after delay (to hide and show activeCallView)
DispatchQueue.main.asyncAfter(deadline: .now() + (m.activeCall == nil ? 0 : 1)) {
m.activeCall = call
m.showCallView = true
m.callCommand = .start(
media: invitation.callType.media,
aesKey: invitation.sharedKey,
iceServers: iceServers,
relay: useRelay
)
}
}
func enableMedia(media: CallMediaType, enable: Bool, callUUID: UUID) -> Bool {
if let call = ChatModel.shared.activeCall, call.callkitUUID == callUUID {
let m = ChatModel.shared
m.callCommand = .media(media: media, enable: enable)
return true
}
return false
}
func endCall(callUUID: UUID, completed: @escaping (Bool) -> Void) {
@@ -82,17 +95,15 @@ class CallManager {
} else {
logger.debug("CallManager.endCall: ending call...")
m.callCommand = .end
m.activeCall = nil
m.showCallView = false
completed()
Task {
do {
try await apiEndCall(call.contact)
} catch {
logger.error("CallController.provider apiEndCall error: \(responseError(error))")
}
DispatchQueue.main.async {
m.activeCall = nil
completed()
}
}
}
}
@@ -65,6 +65,7 @@ struct IncomingCallView: View {
.padding(.vertical, 12)
.frame(maxWidth: .infinity)
.background(Color(uiColor: .tertiarySystemGroupedBackground))
.onAppear { dismissAllSheets() }
}
private func callButton(_ text: LocalizedStringKey, _ image: String, _ color: Color, action: @escaping () -> Void) -> some View {
+10 -1
View File
@@ -47,6 +47,9 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
self.sendCallResponse = sendCallResponse
self.activeCall = activeCall
self.localRendererAspectRatio = localRendererAspectRatio
rtcAudioSession.useManualAudio = CallController.useCallKit()
rtcAudioSession.isAudioEnabled = !CallController.useCallKit()
logger.debug("WebRTCClient: rtcAudioSession has manual audio \(self.rtcAudioSession.useManualAudio) and audio enabled \(self.rtcAudioSession.isAudioEnabled)}")
super.init()
}
@@ -239,6 +242,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
}
func enableMedia(_ media: CallMediaType, _ enable: Bool) {
logger.debug("WebRTCClient: enabling media \(media.rawValue) \(enable)")
media == .video ? setVideoEnabled(enable) : setAudioEnabled(enable)
}
@@ -361,6 +365,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
func endCall() {
guard let call = activeCall.wrappedValue else { return }
logger.debug("WebRTCClient: ending the call")
activeCall.wrappedValue = nil
call.connection.close()
call.connection.delegate = nil
@@ -532,6 +537,7 @@ extension WebRTCClient {
}
func setSpeakerEnabledAndConfigureSession( _ enabled: Bool) {
logger.debug("WebRTCClient: configuring session with speaker enabled \(enabled)")
audioQueue.async { [weak self] in
guard let self = self else { return }
self.rtcAudioSession.lockForConfiguration()
@@ -543,6 +549,7 @@ extension WebRTCClient {
try self.rtcAudioSession.setMode(AVAudioSession.Mode.voiceChat.rawValue)
try self.rtcAudioSession.overrideOutputAudioPort(enabled ? .speaker : .none)
try self.rtcAudioSession.setActive(true)
logger.debug("WebRTCClient: configuring session with speaker enabled \(enabled) success")
} catch let error {
logger.debug("Error configuring AVAudioSession: \(error)")
}
@@ -550,6 +557,7 @@ extension WebRTCClient {
}
func audioSessionToDefaults() {
logger.debug("WebRTCClient: audioSession to defaults")
audioQueue.async { [weak self] in
guard let self = self else { return }
self.rtcAudioSession.lockForConfiguration()
@@ -561,8 +569,9 @@ extension WebRTCClient {
try self.rtcAudioSession.setMode(AVAudioSession.Mode.default.rawValue)
try self.rtcAudioSession.overrideOutputAudioPort(.none)
try self.rtcAudioSession.setActive(false)
logger.debug("WebRTCClient: audioSession to defaults success")
} catch let error {
logger.debug("Error configuring AVAudioSession: \(error)")
logger.debug("Error configuring AVAudioSession with defaults: \(error)")
}
}
}
@@ -7,22 +7,26 @@
//
import SwiftUI
import SimpleXChat
struct CallSettings: View {
@AppStorage(DEFAULT_WEBRTC_POLICY_RELAY) private var webrtcPolicyRelay = true
@AppStorage(GROUP_DEFAULT_CALL_KIT_ENABLED, store: UserDefaults(suiteName: APP_GROUP_NAME)!) private var callKitEnabled = true
@AppStorage(DEFAULT_CALL_KIT_CALLS_IN_RECENTS) private var callKitCallsInRecents = false
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
private let allowChangingCallsHistory = false
var body: some View {
VStack {
List {
Section {
Toggle("Connect via relay", isOn: $webrtcPolicyRelay)
NavigationLink {
RTCServers()
.navigationTitle("Your ICE servers")
} label: {
Text("WebRTC ICE servers")
}
Toggle("Always use relay", isOn: $webrtcPolicyRelay)
} header: {
Text("Settings")
} footer: {
@@ -33,12 +37,29 @@ struct CallSettings: View {
}
}
if !CallController.isInChina {
Section {
Toggle("Use iOS call interface", isOn: $callKitEnabled)
Toggle("Show calls in phone history", isOn: $callKitCallsInRecents)
.disabled(!callKitEnabled)
.onChange(of: callKitCallsInRecents) { value in
CallController.shared.showInRecents(value)
}
} header: {
Text("Interface")
} footer: {
if callKitEnabled {
Text("You can accept calls from lock screen, without device and app authentication.")
} else {
Text("Authentication is required before the call is connected, but you may miss calls.")
}
}
}
Section("Limitations") {
VStack(alignment: .leading, spacing: 8) {
textListItem("1.", "Do NOT use SimpleX for emergency calls.")
textListItem("2.", "The microphone does not work when the app is in the background.")
textListItem("3.", "To prevent the call interruption, enable Do Not Disturb mode.")
textListItem("4.", "If the video fails to connect, flip the camera to resolve it.")
textListItem("2.", "Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions.")
}
.font(.callout)
.padding(.vertical, 8)
@@ -22,6 +22,7 @@ let DEFAULT_PERFORM_LA = "performLocalAuthentication"
let DEFAULT_NOTIFICATION_ALERT_SHOWN = "notificationAlertShown"
let DEFAULT_WEBRTC_POLICY_RELAY = "webrtcPolicyRelay"
let DEFAULT_WEBRTC_ICE_SERVERS = "webrtcICEServers"
let DEFAULT_CALL_KIT_CALLS_IN_RECENTS = "callKitCallsInRecents"
let DEFAULT_PRIVACY_ACCEPT_IMAGES = "privacyAcceptImages"
let DEFAULT_PRIVACY_LINK_PREVIEWS = "privacyLinkPreviews"
let DEFAULT_PRIVACY_SIMPLEX_LINK_MODE = "privacySimplexLinkMode"
@@ -47,6 +48,7 @@ let appDefaults: [String: Any] = [
DEFAULT_PERFORM_LA: false,
DEFAULT_NOTIFICATION_ALERT_SHOWN: false,
DEFAULT_WEBRTC_POLICY_RELAY: true,
DEFAULT_CALL_KIT_CALLS_IN_RECENTS: false,
DEFAULT_PRIVACY_ACCEPT_IMAGES: true,
DEFAULT_PRIVACY_LINK_PREVIEWS: true,
DEFAULT_PRIVACY_SIMPLEX_LINK_MODE: "description",
@@ -8,6 +8,8 @@
import UserNotifications
import OSLog
import StoreKit
import CallKit
import SimpleXChat
let logger = Logger()
@@ -206,6 +208,9 @@ func chatRecvMsg() async -> ChatResponse? {
}
}
private let isInChina = SKStorefront().countryCode == "CHN"
private func useCallKit() -> Bool { !isInChina && callKitEnabledGroupDefault.get() }
func receivedMsgNtf(_ res: ChatResponse) async -> (String, UNMutableNotificationContent)? {
logger.debug("NotificationService processReceivedMsg: \(res.responseType)")
switch res {
@@ -237,6 +242,20 @@ func receivedMsgNtf(_ res: ChatResponse) async -> (String, UNMutableNotification
}
return cItem.showMutableNotification ? (aChatItem.chatId, createMessageReceivedNtf(user, cInfo, cItem)) : nil
case let .callInvitation(invitation):
// Do not post it without CallKit support, iOS will stop launching the app without showing CallKit
if useCallKit() {
do {
try await CXProvider.reportNewIncomingVoIPPushPayload([
"displayName": invitation.contact.displayName,
"contactId": invitation.contact.id,
"media": invitation.callType.media.rawValue
])
logger.debug("reportNewIncomingVoIPPushPayload success to CallController for \(invitation.contact.id)")
return (invitation.contact.id, (UNNotificationContent().mutableCopy() as! UNMutableNotificationContent))
} catch let error {
logger.error("reportNewIncomingVoIPPushPayload error \(String(describing: error), privacy: .public)")
}
}
return (invitation.contact.id, createCallInvitationNtf(invitation))
default:
logger.debug("NotificationService processReceivedMsg ignored event: \(res.responseType)")
@@ -10,5 +10,7 @@
<array>
<string>$(AppIdentifierPrefix)chat.simplex.app</string>
</array>
<key>com.apple.developer.usernotifications.filtering</key>
<true/>
</dict>
</plist>
+5
View File
@@ -45,9 +45,14 @@
</array>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>NSUserActivityTypes</key>
<array>
<string>INStartCallIntent</string>
</array>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
<string>bluetooth-central</string>
<string>fetch</string>
<string>remote-notification</string>
<string>voip</string>
@@ -167,6 +167,8 @@
64F1CC3B28B39D8600CD1FB1 /* IncognitoHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */; };
D7197A1829AE89660055C05A /* WebRTC in Frameworks */ = {isa = PBXBuildFile; productRef = D7197A1729AE89660055C05A /* WebRTC */; };
D72A9088294BD7A70047C86D /* NativeTextEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A9087294BD7A70047C86D /* NativeTextEditor.swift */; };
D741547829AF89AF0022400A /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D741547729AF89AF0022400A /* StoreKit.framework */; };
D741547A29AF90B00022400A /* PushKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D741547929AF90B00022400A /* PushKit.framework */; };
D77B92DC2952372200A5A1CC /* SwiftyGif in Frameworks */ = {isa = PBXBuildFile; productRef = D77B92DB2952372200A5A1CC /* SwiftyGif */; };
D7F0E33929964E7E0068AF69 /* LZString in Frameworks */ = {isa = PBXBuildFile; productRef = D7F0E33829964E7E0068AF69 /* LZString */; };
/* End PBXBuildFile section */
@@ -411,6 +413,8 @@
64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIGroupInvitationView.swift; sourceTree = "<group>"; };
64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncognitoHelp.swift; sourceTree = "<group>"; };
D72A9087294BD7A70047C86D /* NativeTextEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeTextEditor.swift; sourceTree = "<group>"; };
D741547729AF89AF0022400A /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/StoreKit.framework; sourceTree = DEVELOPER_DIR; };
D741547929AF90B00022400A /* PushKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PushKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/PushKit.framework; sourceTree = DEVELOPER_DIR; };
D7AA2C3429A936B400737B40 /* MediaEncryption.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; name = MediaEncryption.playground; path = Shared/MediaEncryption.playground; sourceTree = SOURCE_ROOT; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
/* End PBXFileReference section */
@@ -420,9 +424,11 @@
buildActionMask = 2147483647;
files = (
5CE2BA702845308900EC33A6 /* SimpleXChat.framework in Frameworks */,
D741547829AF89AF0022400A /* StoreKit.framework in Frameworks */,
D7197A1829AE89660055C05A /* WebRTC in Frameworks */,
D77B92DC2952372200A5A1CC /* SwiftyGif in Frameworks */,
D7F0E33929964E7E0068AF69 /* LZString in Frameworks */,
D741547A29AF90B00022400A /* PushKit.framework in Frameworks */,
646BB38C283BEEB9001CE359 /* LocalAuthentication.framework in Frameworks */,
5C8F01CD27A6F0D8007D2C8D /* CodeScanner in Frameworks */,
);
@@ -524,6 +530,8 @@
5C764E7A279C71D4000C6508 /* Frameworks */ = {
isa = PBXGroup;
children = (
D741547929AF90B00022400A /* PushKit.framework */,
D741547729AF89AF0022400A /* StoreKit.framework */,
646BB38B283BEEB9001CE359 /* LocalAuthentication.framework */,
5CDCAD6028187D7900503DA2 /* libz.tbd */,
5CDCAD5E28187D4A00503DA2 /* libiconv.tbd */,
@@ -1482,6 +1490,7 @@
buildSettings = {
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 128;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
@@ -1499,6 +1508,7 @@
MARKETING_VERSION = 4.5.4;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
@@ -1512,6 +1522,7 @@
buildSettings = {
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 128;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
@@ -1529,6 +1540,7 @@
MARKETING_VERSION = 4.5.4;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
@@ -0,0 +1,97 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1400"
wasCreatedForAppExtension = "YES"
version = "2.0">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "5CDCAD442818589900503DA2"
BuildableName = "SimpleX NSE.appex"
BlueprintName = "SimpleX NSE"
ReferencedContainer = "container:SimpleX.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "5CA059C9279559F40002BEB4"
BuildableName = "SimpleX.app"
BlueprintName = "SimpleX (iOS)"
ReferencedContainer = "container:SimpleX.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = ""
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
launchStyle = "0"
askForAppToLaunch = "Yes"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "5CA059C9279559F40002BEB4"
BuildableName = "SimpleX.app"
BlueprintName = "SimpleX (iOS)"
ReferencedContainer = "container:SimpleX.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES"
askForAppToLaunch = "Yes"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "5CA059C9279559F40002BEB4"
BuildableName = "SimpleX.app"
BlueprintName = "SimpleX (iOS)"
ReferencedContainer = "container:SimpleX.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
+6 -2
View File
@@ -29,8 +29,9 @@ let GROUP_DEFAULT_NETWORK_TCP_KEEP_CNT = "networkTCPKeepCnt"
let GROUP_DEFAULT_INCOGNITO = "incognito"
let GROUP_DEFAULT_STORE_DB_PASSPHRASE = "storeDBPassphrase"
let GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE = "initialRandomDBPassphrase"
public let GROUP_DEFAULT_CALL_KIT_ENABLED = "callKitEnabled"
let APP_GROUP_NAME = "group.chat.simplex.app"
public let APP_GROUP_NAME = "group.chat.simplex.app"
public let groupDefaults = UserDefaults(suiteName: APP_GROUP_NAME)!
@@ -50,7 +51,8 @@ public func registerGroupDefaults() {
GROUP_DEFAULT_STORE_DB_PASSPHRASE: true,
GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE: false,
GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES: true,
GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE: false
GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE: false,
GROUP_DEFAULT_CALL_KIT_ENABLED: true
])
}
@@ -119,6 +121,8 @@ public let storeDBPassphraseGroupDefault = BoolDefault(defaults: groupDefaults,
public let initialRandomDBPassphraseGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE)
public let callKitEnabledGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_CALL_KIT_ENABLED)
public class DateDefault {
var defaults: UserDefaults
var key: String
+6 -1
View File
@@ -40,7 +40,6 @@ public struct WebRTCExtraInfo: Codable {
public struct RcvCallInvitation: Decodable {
public var user: User
public var contact: Contact
public var callkitUUID: UUID? = UUID()
public var callType: CallType
public var sharedKey: String?
public var callTs: Date
@@ -53,6 +52,12 @@ public struct RcvCallInvitation: Decodable {
}
}
public var callkitUUID: UUID? = UUID()
private enum CodingKeys: String, CodingKey {
case user, contact, callType, sharedKey, callTs
}
public static let sampleData = RcvCallInvitation(
user: User.sampleData,
contact: Contact.sampleData,