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? { var connectingDate: Date? {
didSet { didSet {
stateDidChange?() stateDidChange?()
resetTimeoutTimerIfNeeded()
hasStartedConnectingDidChange?() hasStartedConnectingDidChange?()
} }
} }
@ -113,12 +114,12 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate {
set { connectingDate = newValue ? Date() : nil } set { connectingDate = newValue ? Date() : nil }
} }
var hasConnected: Bool { public var hasConnected: Bool {
get { return connectedDate != nil } get { return connectedDate != nil }
set { connectedDate = newValue ? Date() : nil } set { connectedDate = newValue ? Date() : nil }
} }
var hasEnded: Bool { public var hasEnded: Bool {
get { return endDate != nil } get { return endDate != nil }
set { endDate = newValue ? Date() : nil } set { endDate = newValue ? Date() : nil }
} }
@ -277,55 +278,60 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate {
let duration: TimeInterval = self.duration let duration: TimeInterval = self.duration
let hasStartedConnecting: Bool = self.hasStartedConnecting let hasStartedConnecting: Bool = self.hasStartedConnecting
Storage.shared.writeAsync { db in Storage.shared.writeAsync(
guard let interaction: Interaction = try? Interaction.fetchOne(db, id: callInteractionId) else { updates: { db in
return guard let interaction: Interaction = try? Interaction.fetchOne(db, id: callInteractionId) else {
} return
}
let updateToMissedIfNeeded: () throws -> () = {
let missedCallInfo: CallMessage.MessageInfo = CallMessage.MessageInfo(state: .missed)
guard let updateToMissedIfNeeded: () throws -> () = {
let infoMessageData: Data = (interaction.body ?? "").data(using: .utf8), let missedCallInfo: CallMessage.MessageInfo = CallMessage.MessageInfo(state: .missed)
let messageInfo: CallMessage.MessageInfo = try? JSONDecoder().decode(
CallMessage.MessageInfo.self, guard
from: infoMessageData let infoMessageData: Data = (interaction.body ?? "").data(using: .utf8),
), let messageInfo: CallMessage.MessageInfo = try? JSONDecoder().decode(
messageInfo.state == .incoming, CallMessage.MessageInfo.self,
let missedCallInfoData: Data = try? JSONEncoder().encode(missedCallInfo) from: infoMessageData
else { return } ),
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 guard shouldMarkAsRead else { return }
.with(body: String(data: missedCallInfoData, encoding: .utf8))
.saved(db)
}
let shouldMarkAsRead: Bool = try {
if duration > 0 { return true }
if hasStartedConnecting { return true }
switch mode { try Interaction.markAsRead(
case .local: db,
try updateToMissedIfNeeded() interactionId: interaction.id,
return true threadId: interaction.threadId,
includingOlder: false,
case .remote, .unanswered: trySendReadReceipt: false
try updateToMissedIfNeeded() )
return false },
completion: { _, _ in
case .answeredElsewhere: return true SessionCallManager.suspendDatabaseIfCallEndedInBackground()
} }
}() )
guard shouldMarkAsRead else { return }
try Interaction.markAsRead(
db,
interactionId: interaction.id,
threadId: interaction.threadId,
includingOlder: false,
trySendReadReceipt: false
)
}
} }
// MARK: - Renderer // MARK: - Renderer
@ -421,6 +427,11 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate {
} }
} }
public func resetTimeoutTimerIfNeeded() {
if self.timeOutTimer == nil { return }
setupTimeoutTimer()
}
public func invalidateTimeoutTimer() { public func invalidateTimeoutTimer() {
timeOutTimer?.invalidate() timeOutTimer?.invalidate()
timeOutTimer = nil timeOutTimer = nil

@ -73,13 +73,19 @@ public final class SessionCallManager: NSObject, CallManagerProtocol {
// MARK: - Report calls // MARK: - Report calls
public static func reportFakeCall(info: String) { public static func reportFakeCall(info: String) {
SessionCallManager.sharedProvider(useSystemCallLog: false) let callId = UUID()
.reportNewIncomingCall( let provider = SessionCallManager.sharedProvider(useSystemCallLog: false)
with: UUID(), provider.reportNewIncomingCall(
update: CXCallUpdate() with: callId,
) { _ in update: CXCallUpdate()
SNLog("[Calls] Reported fake incoming call to CallKit due to: \(info)") ) { _ 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) { 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) { public func reportIncomingCall(_ call: SessionCall, callerName: String, completion: @escaping (Error?) -> Void) {
AssertIsOnMainThread() let provider = provider ?? Self.sharedProvider(useSystemCallLog: false)
// Construct a CXCallUpdate describing the incoming call, including the caller.
if let provider = provider { let update = CXCallUpdate()
// Construct a CXCallUpdate describing the incoming call, including the caller. update.localizedCallerName = callerName
let update = CXCallUpdate() update.remoteHandle = CXHandle(type: .generic, value: call.callId.uuidString)
update.localizedCallerName = callerName update.hasVideo = false
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 // Report the incoming call to the system
provider.reportNewIncomingCall(with: call.callId, update: update) { error in provider.reportNewIncomingCall(with: call.callId, update: update) { error in
guard error == nil else { guard error == nil else {
self.reportCurrentCallEnded(reason: .failed) self.reportCurrentCallEnded(reason: .failed)
completion(error) completion(error)
return return
}
UserDefaults.sharedLokiProject?.set(true, forKey: "isCallOngoing")
completion(nil)
} }
}
else {
SessionCallManager.reportFakeCall(info: "No CXProvider instance")
UserDefaults.sharedLokiProject?.set(true, forKey: "isCallOngoing") UserDefaults.sharedLokiProject?.set(true, forKey: "isCallOngoing")
completion(nil) completion(nil)
} }
@ -135,7 +133,16 @@ public final class SessionCallManager: NSObject, CallManagerProtocol {
return 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 { if let reason = reason {
self.provider?.reportCall(with: call.callId, endedAt: nil, 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() call.webRTCSession.dropConnection()
self.currentCall = nil self.currentCall = nil
WebRTCSession.current = nil handleCallEnded()
UserDefaults.sharedLokiProject?.set(false, forKey: "isCallOngoing")
} }
// MARK: - Util // MARK: - Util
@ -172,15 +178,18 @@ public final class SessionCallManager: NSObject, CallManagerProtocol {
callUpdate.supportsDTMF = false 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 // MARK: - UI
public func showCallUIForCall(caller: String, uuid: String, mode: CallMode, interactionId: Int64?) { 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 { guard let call: SessionCall = Storage.shared.read({ db in SessionCall(db, for: caller, uuid: uuid, mode: mode) }) else {
return return
} }
@ -193,20 +202,23 @@ public final class SessionCallManager: NSObject, CallManagerProtocol {
} }
guard CurrentAppContext().isMainAppAndActive else { return } 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 { DispatchQueue.main.async {
let callVC = CallVC(for: call) guard let presentingVC = CurrentAppContext().frontmostViewController() else {
callVC.conversationVC = conversationVC preconditionFailure() // FIXME: Handle more gracefully
conversationVC.inputAccessoryView?.isHidden = true }
conversationVC.inputAccessoryView?.alpha = 0
presentingVC.present(callVC, animated: true, completion: nil) if let conversationVC: ConversationVC = presentingVC as? ConversationVC, conversationVC.viewModel.threadData.threadId == call.sessionId {
} let callVC = CallVC(for: call)
else if !Preferences.isCallKitSupported { callVC.conversationVC = conversationVC
let incomingCallBanner = IncomingCallBanner(for: call) conversationVC.inputAccessoryView?.isHidden = true
incomingCallBanner.show() 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() SNAppearance.switchToSessionAppearance()
if Environment.shared?.callManager.wrappedValue?.currentCall == nil {
UserDefaults.sharedLokiProject?.set(false, forKey: "isCallOngoing")
}
// No point continuing if we are running tests // No point continuing if we are running tests
guard !CurrentAppContext().isRunningTests else { return true } 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 // NOTE: Fix an edge case where user taps on the callkit notification
// but answers the call on another device // 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 // Stop all jobs except for message sending and when completed suspend the database
JobRunner.stopAndClearPendingJobs(exceptForVariant: .messageSend) { 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) { func applicationDidReceiveMemoryWarning(_ application: UIApplication) {
Logger.info("applicationDidReceiveMemoryWarning") Logger.info("applicationDidReceiveMemoryWarning")
} }
func applicationWillTerminate(_ application: UIApplication) { func applicationWillTerminate(_ application: UIApplication) {
DDLog.flushLog() DDLog.flushLog()
stopPollers() stopPollers()
} }
@ -634,6 +640,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
return !call.hasStartedConnecting return !call.hasStartedConnecting
} }
func hasCallOngoing() -> Bool {
guard let call = AppEnvironment.shared.callManager.currentCall else { return false }
return !call.hasEnded
}
func handleAppActivatedWithOngoingCallIfNeeded() { func handleAppActivatedWithOngoingCallIfNeeded() {
guard guard
let call: SessionCall = (AppEnvironment.shared.callManager.currentCall as? SessionCall), let call: SessionCall = (AppEnvironment.shared.callManager.currentCall as? SessionCall),

@ -313,21 +313,25 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
AppNotificationUserInfoKey.threadId: thread.id AppNotificationUserInfoKey.threadId: thread.id
] ]
let notificationTitle: String = interaction.previewText(db) let notificationTitle: String = "Session"
let threadName: String = SessionThread.displayName( let senderName: String = Profile.displayName(db, id: interaction.authorId, threadVariant: thread.variant)
threadId: thread.id, let notificationBody: String? = {
variant: thread.variant, switch messageInfo.state {
closedGroupName: nil, // Not supported case .permissionDenied:
openGroupName: nil // Not supported return String(
) format: "modal_call_missed_tips_explanation".localized(),
var notificationBody: String? 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] let fallbackSound: Preferences.Sound = db[.defaultNotificationSound]
.defaulting(to: Preferences.Sound.defaultNotificationSound) .defaulting(to: Preferences.Sound.defaultNotificationSound)
@ -345,7 +349,7 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
previewType: previewType, previewType: previewType,
sound: sound, sound: sound,
threadVariant: thread.variant, threadVariant: thread.variant,
threadName: threadName, threadName: senderName,
replacingIdentifier: UUID().uuidString replacingIdentifier: UUID().uuidString
) )
} }

@ -6,7 +6,7 @@ import Foundation
import PromiseKit import PromiseKit
import PushKit import PushKit
import SignalUtilitiesKit import SignalUtilitiesKit
import SignalUtilitiesKit import GRDB
public enum PushRegistrationError: Error { public enum PushRegistrationError: Error {
case assertionError(description: String) case assertionError(description: String)
@ -251,6 +251,9 @@ public enum PushRegistrationError: Error {
return return
} }
// Resume database
NotificationCenter.default.post(name: Database.resumeNotification, object: self)
let maybeCall: SessionCall? = Storage.shared.write { db in let maybeCall: SessionCall? = Storage.shared.write { db in
let messageInfo: CallMessage.MessageInfo = CallMessage.MessageInfo( let messageInfo: CallMessage.MessageInfo = CallMessage.MessageInfo(
state: (caller == getUserHexEncodedPublicKey(db) ? 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 call: SessionCall = SessionCall(db, for: caller, uuid: uuid, mode: .answer)
let thread: SessionThread = try SessionThread.fetchOrCreate(db, id: caller, variant: .contact) let thread: SessionThread = try SessionThread.fetchOrCreate(db, id: caller, variant: .contact)
@ -269,7 +278,7 @@ public enum PushRegistrationError: Error {
threadId: thread.id, threadId: thread.id,
authorId: caller, authorId: caller,
variant: .infoCall, variant: .infoCall,
body: String(data: messageInfoData, encoding: .utf8), body: messageInfoString,
timestampMs: timestampMs timestampMs: timestampMs
).inserted(db) ).inserted(db)
call.callInteractionId = interaction.id call.callInteractionId = interaction.id

@ -9,6 +9,7 @@ public protocol CurrentCallProtocol {
var callId: UUID { get } var callId: UUID { get }
var webRTCSession: WebRTCSession { get } var webRTCSession: WebRTCSession { get }
var hasStartedConnecting: Bool { get set } var hasStartedConnecting: Bool { get set }
var hasEnded: Bool { get set }
func updateCallMessage(mode: EndCallMode) func updateCallMessage(mode: EndCallMode)
func didReceiveRemoteSDP(sdp: RTCSessionDescription) func didReceiveRemoteSDP(sdp: RTCSessionDescription)

@ -157,18 +157,15 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol {
notificationContent.badge = NSNumber(value: newBadgeNumber) notificationContent.badge = NSNumber(value: newBadgeNumber)
CurrentAppContext().appUserDefaults().set(newBadgeNumber, forKey: "currentBadgeNumber") CurrentAppContext().appUserDefaults().set(newBadgeNumber, forKey: "currentBadgeNumber")
notificationContent.title = interaction.previewText(db) notificationContent.title = "Session"
notificationContent.body = "" notificationContent.body = ""
let senderName: String = Profile.displayName(db, id: interaction.authorId, threadVariant: thread.variant)
if messageInfo.state == .permissionDenied { if messageInfo.state == .permissionDenied {
notificationContent.body = String( notificationContent.body = String(
format: "modal_call_missed_tips_explanation".localized(), format: "modal_call_missed_tips_explanation".localized(),
SessionThread.displayName( senderName
threadId: thread.id,
variant: thread.variant,
closedGroupName: nil, // Not supported
openGroupName: nil // Not supported
)
) )
} }

@ -25,6 +25,9 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
self.contentHandler = contentHandler self.contentHandler = contentHandler
self.request = request self.request = request
// Resume database
NotificationCenter.default.post(name: Database.resumeNotification, object: self)
guard let notificationContent = request.content.mutableCopy() as? UNMutableNotificationContent else { guard let notificationContent = request.content.mutableCopy() as? UNMutableNotificationContent else {
return self.completeSilenty() return self.completeSilenty()
} }
@ -237,6 +240,10 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
private func completeSilenty() { private func completeSilenty() {
SNLog("Complete silenty") SNLog("Complete silenty")
// Suspend the database
NotificationCenter.default.post(name: Database.suspendNotification, object: self)
self.contentHandler!(.init()) self.contentHandler!(.init())
} }
@ -298,11 +305,10 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
SNLog("Add remote notification request") SNLog("Add remote notification request")
} }
private func handleSuccess(for content: UNMutableNotificationContent) {
contentHandler!(content)
}
private func handleFailure(for content: UNMutableNotificationContent) { 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.body = "You've got a new message"
content.title = "Session" content.title = "Session"
let userInfo: [String:Any] = [ NotificationServiceExtension.isFromRemoteKey : true ] let userInfo: [String:Any] = [ NotificationServiceExtension.isFromRemoteKey : true ]

@ -220,6 +220,8 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
shareVC?.dismiss(animated: true, completion: nil) shareVC?.dismiss(animated: true, completion: nil)
ModalActivityIndicatorViewController.present(fromViewController: shareVC!, canCancel: false, message: "vc_share_sending_message".localized()) { activityIndicator in 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 Storage.shared
.writeAsync { [weak self] db -> Promise<Void> in .writeAsync { [weak self] db -> Promise<Void> in
guard let thread: SessionThread = try SessionThread.fetchOne(db, id: threadId) else { 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 .done { [weak self] _ in
// Suspend the database
NotificationCenter.default.post(name: Database.suspendNotification, object: self)
activityIndicator.dismiss { } activityIndicator.dismiss { }
self?.shareVC?.shareViewWasCompleted() self?.shareVC?.shareViewWasCompleted()
} }
.catch { [weak self] error in .catch { [weak self] error in
// Suspend the database
NotificationCenter.default.post(name: Database.suspendNotification, object: self)
activityIndicator.dismiss { } activityIndicator.dismiss { }
self?.shareVC?.shareViewFailed(error: error) self?.shareVC?.shareViewFailed(error: error)
} }

Loading…
Cancel
Save