Merge pull request #687 from RyanRory/badcall-fix

Voice & video call fix
pull/692/head
RyanZhao 2 years ago committed by GitHub
commit 570883e28a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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

@ -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()
}
}
}
}

@ -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),

@ -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
)
}

@ -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

@ -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)

@ -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
)
}

@ -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 ]

@ -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<Void> 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)
}

Loading…
Cancel
Save