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:
Evgeny Poberezkin
2023-03-16 19:57:43 +00:00
committed by GitHub
co-authored by Avently
parent 063440e735
commit 8145387f77
5 changed files with 86 additions and 51 deletions
+6 -4
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)
+2 -2
View File
@@ -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))")
}
+2 -5
View File
@@ -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 {
+75 -39
View File
@@ -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)")