mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-08-29 11:59:06 +00:00
ios: CallKit changed reporting logic (#2019)
* ios: CallKit changed reporting logic * refactor, suspend chat after call when app is in background --------- Co-authored-by: Avently <7953703+avently@users.noreply.github.com>
This commit is contained in:
co-authored by
Avently
parent
063440e735
commit
8145387f77
@@ -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)
|
||||
|
||||
@@ -82,12 +82,12 @@ func activateChat(appState: AppState = .active) {
|
||||
}
|
||||
}
|
||||
|
||||
func initChatAndMigrate() {
|
||||
func initChatAndMigrate(refreshInvitations: Bool = true) {
|
||||
let m = ChatModel.shared
|
||||
if (!m.chatInitialized) {
|
||||
do {
|
||||
m.v3DBMigration = v3DBMigrationDefault.get()
|
||||
try initializeChat(start: m.v3DBMigration.startChat)
|
||||
try initializeChat(start: m.v3DBMigration.startChat, refreshInvitations: refreshInvitations)
|
||||
} catch let error {
|
||||
fatalError("Failed to start or load chats: \(responseError(error))")
|
||||
}
|
||||
|
||||
@@ -50,10 +50,7 @@ struct SimpleXApp: App {
|
||||
switch (phase) {
|
||||
case .background:
|
||||
if CallController.useCallKit() && chatModel.activeCall != nil {
|
||||
CallController.shared.onEndCall = {
|
||||
suspendChat()
|
||||
BGManager.shared.schedule()
|
||||
}
|
||||
CallController.shared.shouldSuspendChat = true
|
||||
} else {
|
||||
suspendChat()
|
||||
BGManager.shared.schedule()
|
||||
@@ -65,7 +62,7 @@ struct SimpleXApp: App {
|
||||
canConnectCall = false
|
||||
NtfManager.shared.setNtfBadgeCount(chatModel.totalUnreadCountForAllUsers())
|
||||
case .active:
|
||||
CallController.shared.onEndCall = nil
|
||||
CallController.shared.shouldSuspendChat = false
|
||||
let appState = appStateGroupDefault.get()
|
||||
startChatAndActivate()
|
||||
if appState.inactive && chatModel.chatRunning == true {
|
||||
|
||||
@@ -32,7 +32,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
|
||||
private let controller = CXCallController()
|
||||
private let callManager = CallManager()
|
||||
@Published var activeCallInvitation: RcvCallInvitation?
|
||||
var onEndCall: (() -> Void)? = nil
|
||||
var shouldSuspendChat: Bool = false
|
||||
var fulfillOnConnect: CXAnswerCallAction? = nil
|
||||
|
||||
// PKPushRegistry is used from notification service extension
|
||||
@@ -81,6 +81,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
|
||||
} else {
|
||||
action.fail()
|
||||
}
|
||||
self.suspendOnEndCall()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,12 +128,20 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
|
||||
// see `.onChange(of: scenePhase)` in SimpleXApp
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in
|
||||
if ChatModel.shared.activeCall == nil {
|
||||
logger.debug("CallController: calling callback onEndCall which is \(self?.onEndCall == nil ? "nil" : "non-nil", privacy: .public)")
|
||||
self?.onEndCall?()
|
||||
logger.debug("CallController: shouldSuspendChat \(String(describing: self?.shouldSuspendChat), privacy: .public)")
|
||||
self?.suspendOnEndCall()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func suspendOnEndCall() {
|
||||
if shouldSuspendChat {
|
||||
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)")
|
||||
@@ -140,53 +149,72 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
|
||||
|
||||
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 {
|
||||
if (!ChatModel.shared.chatInitialized) {
|
||||
logger.debug("CallController: initializing chat and returning")
|
||||
initChatAndMigrate()
|
||||
startChatAndActivate()
|
||||
CallController.shared.onEndCall = { terminateChat() }
|
||||
// CallKit will be called from different place, see SimpleXAPI.startChat()
|
||||
return
|
||||
} else {
|
||||
logger.debug("CallController: starting chat (already initialized)")
|
||||
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
|
||||
logger.debug("CallController: reporting incoming call directly to CallKit")
|
||||
CallController.shared.provider.reportNewIncomingCall(with: uuid, update: callUpdate, completion: { error in
|
||||
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 {
|
||||
ChatModel.shared.callInvitations.removeValue(forKey: contactId)
|
||||
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, UUID=\(String(describing: invitation.callkitUUID), privacy: .public)")
|
||||
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
|
||||
let update = cxCallUpdate(invitation: invitation)
|
||||
provider.reportNewIncomingCall(with: uuid, update: update, completion: completion)
|
||||
} else {
|
||||
NtfManager.shared.notifyCallInvitation(invitation)
|
||||
@@ -196,6 +224,14 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
|
||||
@@ -250,7 +250,7 @@ func receivedMsgNtf(_ res: ChatResponse) async -> (String, UNMutableNotification
|
||||
"contactId": invitation.contact.id,
|
||||
"media": invitation.callType.media.rawValue
|
||||
])
|
||||
logger.debug("reportNewIncomingVoIPPushPayload success for \(invitation.contact.id)")
|
||||
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)")
|
||||
|
||||
Reference in New Issue
Block a user