ios: CallKit integration (#1969)
* ios: CallKit integration * notifying CallKit about outgoing call * changes * switching calls with CallKit * string * add NSE filtering entitlement * add NSE build scheme * remove some call limitations * calls enhancments * fixed calls on lockscreen * don't display useless notification * fix app state * ability to answer on call from chat item via CallKit --------- Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
c4c93f881d
commit
9ec6911005
@@ -6,6 +6,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Intents
|
||||
import SimpleXChat
|
||||
|
||||
struct ContentView: View {
|
||||
@@ -79,6 +80,25 @@ struct ContentView: View {
|
||||
}
|
||||
IncomingCallView()
|
||||
}
|
||||
.onContinueUserActivity("INStartCallIntent", perform: processUserActivity)
|
||||
.onContinueUserActivity("INStartAudioCallIntent", perform: processUserActivity)
|
||||
.onContinueUserActivity("INStartVideoCallIntent", perform: processUserActivity)
|
||||
}
|
||||
|
||||
private func processUserActivity(_ activity: NSUserActivity) {
|
||||
let callToContact = { (contactId: ChatId?, mediaType: CallMediaType) in
|
||||
if let chatInfo = chatModel.chats.first(where: { $0.id == contactId })?.chatInfo,
|
||||
case let .direct(contact) = chatInfo {
|
||||
CallController.shared.startCall(contact, mediaType)
|
||||
}
|
||||
}
|
||||
if let intent = activity.interaction?.intent as? INStartCallIntent {
|
||||
callToContact(intent.contacts?.first?.personHandle?.value, .audio)
|
||||
} else if let intent = activity.interaction?.intent as? INStartAudioCallIntent {
|
||||
callToContact(intent.contacts?.first?.personHandle?.value, .audio)
|
||||
} else if let intent = activity.interaction?.intent as? INStartVideoCallIntent {
|
||||
callToContact(intent.contacts?.first?.personHandle?.value, .video)
|
||||
}
|
||||
}
|
||||
|
||||
private func runAuthenticate() {
|
||||
|
||||
@@ -1214,19 +1214,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 +1246,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 +1297,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 +1307,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
|
||||
|
||||
@@ -81,3 +81,24 @@ func activateChat(appState: AppState = .active) {
|
||||
if ChatModel.ok { apiActivateChat() }
|
||||
}
|
||||
}
|
||||
|
||||
func initChatAndMigrate() {
|
||||
let m = ChatModel.shared
|
||||
if (!m.chatInitialized) {
|
||||
do {
|
||||
m.v3DBMigration = v3DBMigrationDefault.get()
|
||||
try initializeChat(start: m.v3DBMigration.startChat)
|
||||
} 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()
|
||||
}
|
||||
}
|
||||
@@ -41,32 +41,30 @@ struct SimpleXApp: App {
|
||||
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.onEndCall = {
|
||||
suspendChat()
|
||||
BGManager.shared.schedule()
|
||||
}
|
||||
} else {
|
||||
suspendChat()
|
||||
BGManager.shared.schedule()
|
||||
}
|
||||
if userAuthorized == true {
|
||||
enteredBackground = ProcessInfo.processInfo.systemUptime
|
||||
}
|
||||
doAuthenticate = false
|
||||
NtfManager.shared.setNtfBadgeCount(chatModel.totalUnreadCountForAllUsers())
|
||||
case .active:
|
||||
if chatModel.chatRunning == true {
|
||||
ChatReceiver.shared.start()
|
||||
}
|
||||
CallController.shared.onEndCall = nil
|
||||
let appState = appStateGroupDefault.get()
|
||||
activateChat()
|
||||
startChatAndActivate()
|
||||
if appState.inactive && chatModel.chatRunning == true {
|
||||
updateChats()
|
||||
updateCallInvitations()
|
||||
|
||||
@@ -117,9 +117,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" {
|
||||
|
||||
@@ -7,189 +7,260 @@
|
||||
//
|
||||
|
||||
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 = false
|
||||
configuration.supportedHandleTypes = [.generic]
|
||||
configuration.includesCallsInRecents = UserDefaults.standard.bool(forKey: DEFAULT_CALL_KIT_CALLS_IN_RECENTS)
|
||||
configuration.maximumCallGroups = 1
|
||||
configuration.maximumCallsPerCallGroup = 1
|
||||
return configuration
|
||||
}())
|
||||
private let controller = CXCallController()
|
||||
private let callManager = CallManager()
|
||||
@Published var activeCallInvitation: RcvCallInvitation?
|
||||
var onEndCall: (() -> Void)? = nil
|
||||
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) {
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
print("received", #function)
|
||||
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) {
|
||||
print("received", #function)
|
||||
RTCAudioSession.sharedInstance().audioSessionDidDeactivate(audioSession)
|
||||
RTCAudioSession.sharedInstance().isAudioEnabled = false
|
||||
do {
|
||||
try audioSession.setActive(false)
|
||||
} catch {
|
||||
print(error)
|
||||
logger.error("failed deactivating audio session")
|
||||
}
|
||||
// Allows to accept second call while in call with a previous before suspending a chat,
|
||||
// see `.onChange(of: scenePhase)` in SimpleXApp
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in
|
||||
if ChatModel.shared.activeCall == nil {
|
||||
self?.onEndCall?()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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()
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
@objc(pushRegistry:didUpdatePushCredentials:forType:)
|
||||
func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) {
|
||||
|
||||
}
|
||||
|
||||
func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
|
||||
if type == .voIP {
|
||||
if (!ChatModel.shared.chatInitialized) {
|
||||
initChatAndMigrate()
|
||||
CallController.shared.onEndCall = { terminateChat() }
|
||||
// CallKit will be called from different place, see SimpleXAPI.startChat()
|
||||
return
|
||||
} else {
|
||||
startChatAndActivate()
|
||||
CallController.shared.onEndCall = {
|
||||
suspendChat()
|
||||
BGManager.shared.schedule()
|
||||
}
|
||||
}
|
||||
// No actual list of invitations in model before this line
|
||||
let invitations = try? justRefreshCallInvitations()
|
||||
logger.debug("Invitations \(String(describing: invitations))")
|
||||
// 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 uuid = ChatModel.shared.callInvitations.first(where: { (key, value) in value.contact.id == contactId } )?.value.callkitUUID,
|
||||
let media = payload.dictionaryPayload["media"] as? String {
|
||||
let callUpdate = CXCallUpdate()
|
||||
callUpdate.remoteHandle = CXHandle(type: .generic, value: contactId)
|
||||
callUpdate.localizedCallerName = displayName
|
||||
callUpdate.hasVideo = media == CallMediaType.video.rawValue
|
||||
CallController.shared.provider.reportNewIncomingCall(with: uuid, update: callUpdate, completion: { error in
|
||||
if error != nil {
|
||||
ChatModel.shared.callInvitations.removeValue(forKey: contactId)
|
||||
}
|
||||
// Tell PushKit that the notification is handled.
|
||||
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))")
|
||||
if CallController.useCallKit(), let uuid = invitation.callkitUUID {
|
||||
let update = CXCallUpdate()
|
||||
update.remoteHandle = CXHandle(type: .generic, value: invitation.contact.id)
|
||||
update.hasVideo = invitation.callType.media == .video
|
||||
update.localizedCallerName = invitation.contact.displayName
|
||||
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)
|
||||
// }
|
||||
// }
|
||||
func reportIncomingCall(call: Call, connectedAt dateConnected: Date?) {
|
||||
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?) {
|
||||
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 {
|
||||
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) {
|
||||
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)
|
||||
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 {
|
||||
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) {
|
||||
@@ -213,15 +284,20 @@ 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
|
||||
}
|
||||
|
||||
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)")
|
||||
} else {
|
||||
logger.debug("CallController.requestTransaction requested transaction successfully")
|
||||
onSuccess()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,8 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
|
||||
self.sendCallResponse = sendCallResponse
|
||||
self.activeCall = activeCall
|
||||
self.localRendererAspectRatio = localRendererAspectRatio
|
||||
rtcAudioSession.useManualAudio = CallController.useCallKit()
|
||||
rtcAudioSession.isAudioEnabled = !CallController.useCallKit()
|
||||
super.init()
|
||||
}
|
||||
|
||||
@@ -562,7 +564,7 @@ extension WebRTCClient {
|
||||
try self.rtcAudioSession.overrideOutputAudioPort(.none)
|
||||
try self.rtcAudioSession.setActive(false)
|
||||
} catch let error {
|
||||
logger.debug("Error configuring AVAudioSession: \(error)")
|
||||
logger.debug("Error configuring AVAudioSession with defaults: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,14 @@
|
||||
//
|
||||
|
||||
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 {
|
||||
@@ -17,6 +22,18 @@ struct CallSettings: View {
|
||||
Section {
|
||||
Toggle("Connect via relay", isOn: $webrtcPolicyRelay)
|
||||
|
||||
if !CallController.isInChina && developerTools {
|
||||
Toggle("Use CallKit", isOn: $callKitEnabled)
|
||||
|
||||
if allowChangingCallsHistory {
|
||||
Toggle("Show calls in phone history", isOn: $callKitCallsInRecents)
|
||||
.disabled(!callKitEnabled)
|
||||
.onChange(of: callKitCallsInRecents) { value in
|
||||
CallController.shared.showInRecents(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NavigationLink {
|
||||
RTCServers()
|
||||
.navigationTitle("Your ICE servers")
|
||||
@@ -36,9 +53,7 @@ struct CallSettings: View {
|
||||
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.", "To prevent the call interruption, enable Do Not Disturb mode.")
|
||||
}
|
||||
.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,21 @@ 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() {
|
||||
CXProvider.reportNewIncomingVoIPPushPayload([
|
||||
"displayName": invitation.contact.displayName,
|
||||
"contactId": invitation.contact.id,
|
||||
"media": invitation.callType.media.rawValue
|
||||
]) { error in
|
||||
if let error = error {
|
||||
logger.error("reportNewIncomingVoIPPushPayload error \(error.localizedDescription, privacy: .public)")
|
||||
} else {
|
||||
logger.debug("reportNewIncomingVoIPPushPayload success for \(invitation.contact.id)")
|
||||
}
|
||||
}
|
||||
return (invitation.contact.id, (UNNotificationContent().mutableCopy() as! UNMutableNotificationContent))
|
||||
}
|
||||
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>
|
||||
|
||||
@@ -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 = 127;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
@@ -1499,6 +1508,7 @@
|
||||
MARKETING_VERSION = 4.5.3;
|
||||
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 = 127;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
@@ -1529,6 +1540,7 @@
|
||||
MARKETING_VERSION = 4.5.3;
|
||||
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>
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user