// // Copyright 2016 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only // import Foundation import CallKit import SignalServiceKit import WebRTC import UIKit protocol CallUIAdaptee: AnyObject { var notificationPresenterImpl: NotificationPresenterImpl { get } var callService: CallService { get } init(showNamesOnCallScreen: Bool, useSystemCallLog: Bool) func startOutgoingCall(call: SignalCall) // [CallLink] TODO: Only allow individual & group calls. func reportIncomingCall(_ call: SignalCall, completion: @escaping (Error?) -> Void) func answerCall(_ call: SignalCall) func recipientAcceptedCall(_ call: CallMode) func localHangupCall(_ call: SignalCall) func remoteDidHangupCall(_ call: SignalCall) func remoteBusy(_ call: SignalCall) func didAnswerElsewhere(call: SignalCall) func didDeclineElsewhere(call: SignalCall) func wasBusyElsewhere(call: SignalCall) func failCall(_ call: SignalCall, error: CallError) func setIsMuted(call: SignalCall, isMuted: Bool) func setHasLocalVideo(call: SignalCall, hasLocalVideo: Bool) } /** * Notify the user of call related activities. * Driven by either a CallKit or System notifications adaptee */ public class CallUIAdapter: NSObject { private var callService: CallService { AppEnvironment.shared.callService } private lazy var adaptee: any CallUIAdaptee = { () -> any CallUIAdaptee in let callUIAdapteeType: CallUIAdaptee.Type #if targetEnvironment(simulator) callUIAdapteeType = SimulatorCallUIAdaptee.self #else callUIAdapteeType = CallKitCallUIAdaptee.self #endif let (showNames, useSystemCallLog) = databaseStorage.read { tx in return ( preferences.notificationPreviewType(tx: tx) != .noNameNoPreview, preferences.isSystemCallLogEnabled(tx: tx) ) } return callUIAdapteeType.init( showNamesOnCallScreen: showNames, useSystemCallLog: useSystemCallLog ) }() public override init() { AssertIsOnMainThread() super.init() // We cannot assert singleton here, because this class gets rebuilt when the user changes relevant call settings } // MARK: internal func reportIncomingCall(_ call: SignalCall) { AssertIsOnMainThread() guard let caller = call.caller else { return } Logger.info("remoteAddress: \(caller)") // make sure we don't terminate audio session during call _ = audioSession.startAudioActivity(call.commonState.audioActivity) adaptee.reportIncomingCall(call) { error in AssertIsOnMainThread() guard var error = error else { self.showCall(call) return } Logger.warn("error: \(error)") switch error { case CXErrorCodeIncomingCallError.filteredByDoNotDisturb: error = CallError.doNotDisturbEnabled case CXErrorCodeIncomingCallError.filteredByBlockList: error = CallError.contactIsBlocked default: break } self.callService.handleFailedCall(failedCall: call, error: error) } } internal func reportMissedCall(_ call: SignalCall, individualCall: IndividualCall) { AssertIsOnMainThread() guard let callerAci = individualCall.thread.contactAddress.aci else { owsFailDebug("Can't receive a call without an ACI.") return } let sentAtTimestamp = Date(millisecondsSince1970: individualCall.sentAtTimestamp) databaseStorage.read { tx in notificationPresenterImpl.presentMissedCall( notificationInfo: NotificationPresenterImpl.CallNotificationInfo( groupingId: individualCall.commonState.localId, thread: individualCall.thread, caller: callerAci ), offerMediaType: individualCall.offerMediaType, sentAt: sentAtTimestamp, tx: tx ) } } internal func startOutgoingCall(call: SignalCall) { AssertIsOnMainThread() // make sure we don't terminate audio session during call _ = audioSession.startAudioActivity(call.commonState.audioActivity) adaptee.startOutgoingCall(call: call) } internal func answerCall(_ call: SignalCall) { AssertIsOnMainThread() adaptee.answerCall(call) } public func startAndShowOutgoingCall(thread: TSContactThread, hasLocalVideo: Bool) { AssertIsOnMainThread() guard let (call, individualCall) = self.callService.buildOutgoingIndividualCallIfPossible( thread: thread, hasVideo: hasLocalVideo ) else { // @integration This is not unexpected, it could happen if Bob tries // to start an outgoing call at the same moment Alice has already // sent him an Offer that is being processed. Logger.info("found an existing call when trying to start outgoing call: \(thread.contactAddress)") return } startOutgoingCall(call: call) individualCall.hasLocalVideo = hasLocalVideo self.showCall(call) } internal func recipientAcceptedCall(_ call: CallMode) { AssertIsOnMainThread() adaptee.recipientAcceptedCall(call) } internal func remoteDidHangupCall(_ call: SignalCall) { AssertIsOnMainThread() adaptee.remoteDidHangupCall(call) } internal func remoteBusy(_ call: SignalCall) { AssertIsOnMainThread() adaptee.remoteBusy(call) } internal func didAnswerElsewhere(call: SignalCall) { adaptee.didAnswerElsewhere(call: call) } internal func didDeclineElsewhere(call: SignalCall) { adaptee.didDeclineElsewhere(call: call) } internal func wasBusyElsewhere(call: SignalCall) { adaptee.wasBusyElsewhere(call: call) } internal func localHangupCall(_ call: SignalCall) { AssertIsOnMainThread() adaptee.localHangupCall(call) } internal func failCall(_ call: SignalCall, error: CallError) { AssertIsOnMainThread() adaptee.failCall(call, error: error) } private func showCall(_ call: SignalCall) { AssertIsOnMainThread() guard !call.hasTerminated else { Logger.info("Not showing window for terminated call \(call)") return } Logger.info("\(call)") let callViewController: UIViewController & CallViewControllerWindowReference switch call.mode { case .individual(let individualCall): callViewController = IndividualCallViewController(call: call, individualCall: individualCall) case .groupThread(let groupCall as GroupCall), .callLink(let groupCall as GroupCall): callViewController = GroupCallViewController(call: call, groupCall: groupCall) } callViewController.modalTransitionStyle = .crossDissolve WindowManager.shared.startCall(viewController: callViewController) } internal func setIsMuted(call: SignalCall, isMuted: Bool) { AssertIsOnMainThread() // With CallKit, muting is handled by a CXAction, so it must go through the adaptee adaptee.setIsMuted(call: call, isMuted: isMuted) } internal func setHasLocalVideo(call: SignalCall, hasLocalVideo: Bool) { AssertIsOnMainThread() adaptee.setHasLocalVideo(call: call, hasLocalVideo: hasLocalVideo) } internal func setCameraSource(call: SignalCall, isUsingFrontCamera: Bool) { AssertIsOnMainThread() callService.updateCameraSource(call: call, isUsingFrontCamera: isUsingFrontCamera) } }