Skip to content

Commit

Permalink
Merge pull request #687 from RyanRory/badcall-fix
Browse files Browse the repository at this point in the history
Voice & video call fix
  • Loading branch information
RyanRory committed Sep 15, 2022
2 parents 31b62e2 + 87c2bc3 commit 570883e
Show file tree
Hide file tree
Showing 9 changed files with 189 additions and 131 deletions.
107 changes: 59 additions & 48 deletions Session/Calls/Call Management/SessionCall.swift
Expand Up @@ -71,6 +71,7 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate {
var connectingDate: Date? {
didSet {
stateDidChange?()
resetTimeoutTimerIfNeeded()
hasStartedConnectingDidChange?()
}
}
Expand Down Expand Up @@ -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 }
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
112 changes: 62 additions & 50 deletions Session/Calls/Call Management/SessionCallManager.swift
Expand Up @@ -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) {
Expand All @@ -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)
}
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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
}
Expand All @@ -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()
}
}
}
}
Expand Down
20 changes: 16 additions & 4 deletions Session/Meta/AppDelegate.swift
Expand Up @@ -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 }

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

Expand Down Expand Up @@ -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),
Expand Down

0 comments on commit 570883e

Please sign in to comment.