diff --git a/Session/Calls/Call Management/SessionCall.swift b/Session/Calls/Call Management/SessionCall.swift index 1aef15a69..87a3aed2a 100644 --- a/Session/Calls/Call Management/SessionCall.swift +++ b/Session/Calls/Call Management/SessionCall.swift @@ -71,6 +71,7 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { var connectingDate: Date? { didSet { stateDidChange?() + resetTimeoutTimerIfNeeded() hasStartedConnectingDidChange?() } } @@ -113,12 +114,12 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { set { connectingDate = newValue ? Date() : nil } } - var hasConnected: Bool { + public var hasConnected: Bool { get { return connectedDate != nil } set { connectedDate = newValue ? Date() : nil } } - var hasEnded: Bool { + public var hasEnded: Bool { get { return endDate != nil } set { endDate = newValue ? Date() : nil } } @@ -277,55 +278,60 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { let duration: TimeInterval = self.duration let hasStartedConnecting: Bool = self.hasStartedConnecting - Storage.shared.writeAsync { db in - guard let interaction: Interaction = try? Interaction.fetchOne(db, id: callInteractionId) else { - return - } - - let updateToMissedIfNeeded: () throws -> () = { - let missedCallInfo: CallMessage.MessageInfo = CallMessage.MessageInfo(state: .missed) + Storage.shared.writeAsync( + updates: { db in + guard let interaction: Interaction = try? Interaction.fetchOne(db, id: callInteractionId) else { + return + } - guard - let infoMessageData: Data = (interaction.body ?? "").data(using: .utf8), - let messageInfo: CallMessage.MessageInfo = try? JSONDecoder().decode( - CallMessage.MessageInfo.self, - from: infoMessageData - ), - messageInfo.state == .incoming, - let missedCallInfoData: Data = try? JSONEncoder().encode(missedCallInfo) - else { return } + let updateToMissedIfNeeded: () throws -> () = { + let missedCallInfo: CallMessage.MessageInfo = CallMessage.MessageInfo(state: .missed) + + guard + let infoMessageData: Data = (interaction.body ?? "").data(using: .utf8), + let messageInfo: CallMessage.MessageInfo = try? JSONDecoder().decode( + CallMessage.MessageInfo.self, + from: infoMessageData + ), + messageInfo.state == .incoming, + let missedCallInfoData: Data = try? JSONEncoder().encode(missedCallInfo) + else { return } + + _ = try interaction + .with(body: String(data: missedCallInfoData, encoding: .utf8)) + .saved(db) + } + let shouldMarkAsRead: Bool = try { + if duration > 0 { return true } + if hasStartedConnecting { return true } + + switch mode { + case .local: + try updateToMissedIfNeeded() + return true + + case .remote, .unanswered: + try updateToMissedIfNeeded() + return false + + case .answeredElsewhere: return true + } + }() - _ = try interaction - .with(body: String(data: missedCallInfoData, encoding: .utf8)) - .saved(db) - } - let shouldMarkAsRead: Bool = try { - if duration > 0 { return true } - if hasStartedConnecting { return true } + guard shouldMarkAsRead else { return } - switch mode { - case .local: - try updateToMissedIfNeeded() - return true - - case .remote, .unanswered: - try updateToMissedIfNeeded() - return false - - case .answeredElsewhere: return true - } - }() - - guard shouldMarkAsRead else { return } - - try Interaction.markAsRead( - db, - interactionId: interaction.id, - threadId: interaction.threadId, - includingOlder: false, - trySendReadReceipt: false - ) - } + try Interaction.markAsRead( + db, + interactionId: interaction.id, + threadId: interaction.threadId, + includingOlder: false, + trySendReadReceipt: false + ) + }, + completion: { _, _ in + SessionCallManager.suspendDatabaseIfCallEndedInBackground() + } + ) } // MARK: - Renderer @@ -421,6 +427,11 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { } } + public func resetTimeoutTimerIfNeeded() { + if self.timeOutTimer == nil { return } + setupTimeoutTimer() + } + public func invalidateTimeoutTimer() { timeOutTimer?.invalidate() timeOutTimer = nil diff --git a/Session/Calls/Call Management/SessionCallManager.swift b/Session/Calls/Call Management/SessionCallManager.swift index 643268bc1..38ecd7b75 100644 --- a/Session/Calls/Call Management/SessionCallManager.swift +++ b/Session/Calls/Call Management/SessionCallManager.swift @@ -73,13 +73,19 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { // MARK: - Report calls public static func reportFakeCall(info: String) { - SessionCallManager.sharedProvider(useSystemCallLog: false) - .reportNewIncomingCall( - with: UUID(), - update: CXCallUpdate() - ) { _ in - SNLog("[Calls] Reported fake incoming call to CallKit due to: \(info)") - } + let callId = UUID() + let provider = SessionCallManager.sharedProvider(useSystemCallLog: false) + provider.reportNewIncomingCall( + with: callId, + update: CXCallUpdate() + ) { _ in + SNLog("[Calls] Reported fake incoming call to CallKit due to: \(info)") + } + provider.reportCall( + with: callId, + endedAt: nil, + reason: .failed + ) } public func reportOutgoingCall(_ call: SessionCall) { @@ -98,30 +104,22 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { } public func reportIncomingCall(_ call: SessionCall, callerName: String, completion: @escaping (Error?) -> Void) { - AssertIsOnMainThread() - - if let provider = provider { - // Construct a CXCallUpdate describing the incoming call, including the caller. - let update = CXCallUpdate() - update.localizedCallerName = callerName - update.remoteHandle = CXHandle(type: .generic, value: call.callId.uuidString) - update.hasVideo = false + let provider = provider ?? Self.sharedProvider(useSystemCallLog: false) + // Construct a CXCallUpdate describing the incoming call, including the caller. + let update = CXCallUpdate() + update.localizedCallerName = callerName + update.remoteHandle = CXHandle(type: .generic, value: call.callId.uuidString) + update.hasVideo = false - disableUnsupportedFeatures(callUpdate: update) + disableUnsupportedFeatures(callUpdate: update) - // Report the incoming call to the system - provider.reportNewIncomingCall(with: call.callId, update: update) { error in - guard error == nil else { - self.reportCurrentCallEnded(reason: .failed) - completion(error) - return - } - UserDefaults.sharedLokiProject?.set(true, forKey: "isCallOngoing") - completion(nil) + // Report the incoming call to the system + provider.reportNewIncomingCall(with: call.callId, update: update) { error in + guard error == nil else { + self.reportCurrentCallEnded(reason: .failed) + completion(error) + return } - } - else { - SessionCallManager.reportFakeCall(info: "No CXProvider instance") UserDefaults.sharedLokiProject?.set(true, forKey: "isCallOngoing") completion(nil) } @@ -135,7 +133,16 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { return } - guard let call = currentCall else { return } + func handleCallEnded() { + WebRTCSession.current = nil + UserDefaults.sharedLokiProject?.set(false, forKey: "isCallOngoing") + } + + guard let call = currentCall else { + handleCallEnded() + Self.suspendDatabaseIfCallEndedInBackground() + return + } if let reason = reason { self.provider?.reportCall(with: call.callId, endedAt: nil, reason: reason) @@ -153,8 +160,7 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { call.webRTCSession.dropConnection() self.currentCall = nil - WebRTCSession.current = nil - UserDefaults.sharedLokiProject?.set(false, forKey: "isCallOngoing") + handleCallEnded() } // MARK: - Util @@ -172,15 +178,18 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { callUpdate.supportsDTMF = false } + public static func suspendDatabaseIfCallEndedInBackground() { + if CurrentAppContext().isInBackground() { + // Stop all jobs except for message sending and when completed suspend the database + JobRunner.stopAndClearPendingJobs(exceptForVariant: .messageSend) { + NotificationCenter.default.post(name: Database.suspendNotification, object: self) + } + } + } + // MARK: - UI public func showCallUIForCall(caller: String, uuid: String, mode: CallMode, interactionId: Int64?) { - guard Thread.isMainThread else { - DispatchQueue.main.async { - self.showCallUIForCall(caller: caller, uuid: uuid, mode: mode, interactionId: interactionId) - } - return - } guard let call: SessionCall = Storage.shared.read({ db in SessionCall(db, for: caller, uuid: uuid, mode: mode) }) else { return } @@ -193,20 +202,23 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { } guard CurrentAppContext().isMainAppAndActive else { return } - guard let presentingVC = CurrentAppContext().frontmostViewController() else { - preconditionFailure() // FIXME: Handle more gracefully - } - if let conversationVC: ConversationVC = presentingVC as? ConversationVC, conversationVC.viewModel.threadData.threadId == call.sessionId { - let callVC = CallVC(for: call) - callVC.conversationVC = conversationVC - conversationVC.inputAccessoryView?.isHidden = true - conversationVC.inputAccessoryView?.alpha = 0 - presentingVC.present(callVC, animated: true, completion: nil) - } - else if !Preferences.isCallKitSupported { - let incomingCallBanner = IncomingCallBanner(for: call) - incomingCallBanner.show() + DispatchQueue.main.async { + guard let presentingVC = CurrentAppContext().frontmostViewController() else { + preconditionFailure() // FIXME: Handle more gracefully + } + + if let conversationVC: ConversationVC = presentingVC as? ConversationVC, conversationVC.viewModel.threadData.threadId == call.sessionId { + let callVC = CallVC(for: call) + callVC.conversationVC = conversationVC + conversationVC.inputAccessoryView?.isHidden = true + conversationVC.inputAccessoryView?.alpha = 0 + presentingVC.present(callVC, animated: true, completion: nil) + } + else if !Preferences.isCallKitSupported { + let incomingCallBanner = IncomingCallBanner(for: call) + incomingCallBanner.show() + } } } } diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index 79db24db8..0255c03e0 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -78,6 +78,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD SNAppearance.switchToSessionAppearance() + if Environment.shared?.callManager.wrappedValue?.currentCall == nil { + UserDefaults.sharedLokiProject?.set(false, forKey: "isCallOngoing") + } + // No point continuing if we are running tests guard !CurrentAppContext().isRunningTests else { return true } @@ -132,21 +136,23 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // NOTE: Fix an edge case where user taps on the callkit notification // but answers the call on another device - stopPollers(shouldStopUserPoller: !self.hasIncomingCallWaiting()) + stopPollers(shouldStopUserPoller: !self.hasCallOngoing()) // Stop all jobs except for message sending and when completed suspend the database JobRunner.stopAndClearPendingJobs(exceptForVariant: .messageSend) { - NotificationCenter.default.post(name: Database.suspendNotification, object: self) + if !self.hasCallOngoing() { + NotificationCenter.default.post(name: Database.suspendNotification, object: self) + } } } func applicationDidReceiveMemoryWarning(_ application: UIApplication) { Logger.info("applicationDidReceiveMemoryWarning") } - + func applicationWillTerminate(_ application: UIApplication) { DDLog.flushLog() - + stopPollers() } @@ -634,6 +640,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD return !call.hasStartedConnecting } + func hasCallOngoing() -> Bool { + guard let call = AppEnvironment.shared.callManager.currentCall else { return false } + + return !call.hasEnded + } + func handleAppActivatedWithOngoingCallIfNeeded() { guard let call: SessionCall = (AppEnvironment.shared.callManager.currentCall as? SessionCall), diff --git a/Session/Notifications/AppNotifications.swift b/Session/Notifications/AppNotifications.swift index e0c25749b..d2e5a0829 100644 --- a/Session/Notifications/AppNotifications.swift +++ b/Session/Notifications/AppNotifications.swift @@ -313,21 +313,25 @@ public class NotificationPresenter: NSObject, NotificationsProtocol { AppNotificationUserInfoKey.threadId: thread.id ] - let notificationTitle: String = interaction.previewText(db) - let threadName: String = SessionThread.displayName( - threadId: thread.id, - variant: thread.variant, - closedGroupName: nil, // Not supported - openGroupName: nil // Not supported - ) - var notificationBody: String? + let notificationTitle: String = "Session" + let senderName: String = Profile.displayName(db, id: interaction.authorId, threadVariant: thread.variant) + let notificationBody: String? = { + switch messageInfo.state { + case .permissionDenied: + return String( + format: "modal_call_missed_tips_explanation".localized(), + senderName + ) + case .missed: + return String( + format: "call_missed".localized(), + senderName + ) + default: + return nil + } + }() - if messageInfo.state == .permissionDenied { - notificationBody = String( - format: "modal_call_missed_tips_explanation".localized(), - threadName - ) - } let fallbackSound: Preferences.Sound = db[.defaultNotificationSound] .defaulting(to: Preferences.Sound.defaultNotificationSound) @@ -345,7 +349,7 @@ public class NotificationPresenter: NSObject, NotificationsProtocol { previewType: previewType, sound: sound, threadVariant: thread.variant, - threadName: threadName, + threadName: senderName, replacingIdentifier: UUID().uuidString ) } diff --git a/Session/Notifications/PushRegistrationManager.swift b/Session/Notifications/PushRegistrationManager.swift index 1cd8d71cf..c2df1a4f9 100644 --- a/Session/Notifications/PushRegistrationManager.swift +++ b/Session/Notifications/PushRegistrationManager.swift @@ -6,7 +6,7 @@ import Foundation import PromiseKit import PushKit import SignalUtilitiesKit -import SignalUtilitiesKit +import GRDB public enum PushRegistrationError: Error { case assertionError(description: String) @@ -251,6 +251,9 @@ public enum PushRegistrationError: Error { return } + // Resume database + NotificationCenter.default.post(name: Database.resumeNotification, object: self) + let maybeCall: SessionCall? = Storage.shared.write { db in let messageInfo: CallMessage.MessageInfo = CallMessage.MessageInfo( state: (caller == getUserHexEncodedPublicKey(db) ? @@ -259,7 +262,13 @@ public enum PushRegistrationError: Error { ) ) - guard let messageInfoData: Data = try? JSONEncoder().encode(messageInfo) else { return nil } + let messageInfoString: String? = { + if let messageInfoData: Data = try? JSONEncoder().encode(messageInfo) { + return String(data: messageInfoData, encoding: .utf8) + } else { + return "Incoming call." // TODO: We can do better here. + } + }() let call: SessionCall = SessionCall(db, for: caller, uuid: uuid, mode: .answer) let thread: SessionThread = try SessionThread.fetchOrCreate(db, id: caller, variant: .contact) @@ -269,7 +278,7 @@ public enum PushRegistrationError: Error { threadId: thread.id, authorId: caller, variant: .infoCall, - body: String(data: messageInfoData, encoding: .utf8), + body: messageInfoString, timestampMs: timestampMs ).inserted(db) call.callInteractionId = interaction.id diff --git a/SessionMessagingKit/Calls/CurrentCallProtocol.swift b/SessionMessagingKit/Calls/CurrentCallProtocol.swift index 6968116db..8cdaf7ff7 100644 --- a/SessionMessagingKit/Calls/CurrentCallProtocol.swift +++ b/SessionMessagingKit/Calls/CurrentCallProtocol.swift @@ -9,6 +9,7 @@ public protocol CurrentCallProtocol { var callId: UUID { get } var webRTCSession: WebRTCSession { get } var hasStartedConnecting: Bool { get set } + var hasEnded: Bool { get set } func updateCallMessage(mode: EndCallMode) func didReceiveRemoteSDP(sdp: RTCSessionDescription) diff --git a/SessionNotificationServiceExtension/NSENotificationPresenter.swift b/SessionNotificationServiceExtension/NSENotificationPresenter.swift index 62956ff80..686e6109f 100644 --- a/SessionNotificationServiceExtension/NSENotificationPresenter.swift +++ b/SessionNotificationServiceExtension/NSENotificationPresenter.swift @@ -157,18 +157,15 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol { notificationContent.badge = NSNumber(value: newBadgeNumber) CurrentAppContext().appUserDefaults().set(newBadgeNumber, forKey: "currentBadgeNumber") - notificationContent.title = interaction.previewText(db) + notificationContent.title = "Session" notificationContent.body = "" + let senderName: String = Profile.displayName(db, id: interaction.authorId, threadVariant: thread.variant) + if messageInfo.state == .permissionDenied { notificationContent.body = String( format: "modal_call_missed_tips_explanation".localized(), - SessionThread.displayName( - threadId: thread.id, - variant: thread.variant, - closedGroupName: nil, // Not supported - openGroupName: nil // Not supported - ) + senderName ) } diff --git a/SessionNotificationServiceExtension/NotificationServiceExtension.swift b/SessionNotificationServiceExtension/NotificationServiceExtension.swift index 5705a4661..7cfeae747 100644 --- a/SessionNotificationServiceExtension/NotificationServiceExtension.swift +++ b/SessionNotificationServiceExtension/NotificationServiceExtension.swift @@ -25,6 +25,9 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension self.contentHandler = contentHandler self.request = request + // Resume database + NotificationCenter.default.post(name: Database.resumeNotification, object: self) + guard let notificationContent = request.content.mutableCopy() as? UNMutableNotificationContent else { return self.completeSilenty() } @@ -237,6 +240,10 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension private func completeSilenty() { SNLog("Complete silenty") + + // Suspend the database + NotificationCenter.default.post(name: Database.suspendNotification, object: self) + self.contentHandler!(.init()) } @@ -298,11 +305,10 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension SNLog("Add remote notification request") } - private func handleSuccess(for content: UNMutableNotificationContent) { - contentHandler!(content) - } - private func handleFailure(for content: UNMutableNotificationContent) { + // Suspend the database + NotificationCenter.default.post(name: Database.suspendNotification, object: self) + content.body = "You've got a new message" content.title = "Session" let userInfo: [String:Any] = [ NotificationServiceExtension.isFromRemoteKey : true ] diff --git a/SessionShareExtension/ThreadPickerVC.swift b/SessionShareExtension/ThreadPickerVC.swift index 4d3352558..f1ea3813d 100644 --- a/SessionShareExtension/ThreadPickerVC.swift +++ b/SessionShareExtension/ThreadPickerVC.swift @@ -220,6 +220,8 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView shareVC?.dismiss(animated: true, completion: nil) ModalActivityIndicatorViewController.present(fromViewController: shareVC!, canCancel: false, message: "vc_share_sending_message".localized()) { activityIndicator in + // Resume database + NotificationCenter.default.post(name: Database.resumeNotification, object: self) Storage.shared .writeAsync { [weak self] db -> Promise in guard let thread: SessionThread = try SessionThread.fetchOne(db, id: threadId) else { @@ -271,10 +273,14 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView ) } .done { [weak self] _ in + // Suspend the database + NotificationCenter.default.post(name: Database.suspendNotification, object: self) activityIndicator.dismiss { } self?.shareVC?.shareViewWasCompleted() } .catch { [weak self] error in + // Suspend the database + NotificationCenter.default.post(name: Database.suspendNotification, object: self) activityIndicator.dismiss { } self?.shareVC?.shareViewFailed(error: error) }