This is preparation for putting group calls in CallKit as well, but for now there's no functionality change here, just moving things around.
323 lines
11 KiB
Swift
323 lines
11 KiB
Swift
//
|
|
// Copyright (c) 2022 Open Whisper Systems. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
import CallKit
|
|
import SignalServiceKit
|
|
import SignalMessaging
|
|
import WebRTC
|
|
|
|
protocol CallUIAdaptee: AnyObject {
|
|
var notificationPresenter: NotificationPresenter { get }
|
|
var callService: CallService { get }
|
|
var hasManualRinger: Bool { get }
|
|
|
|
func startOutgoingCall(call: SignalCall)
|
|
func reportIncomingCall(_ call: SignalCall, completion: @escaping (Error?) -> Void)
|
|
func reportMissedCall(_ call: SignalCall)
|
|
func answerCall(localId: UUID)
|
|
func answerCall(_ call: SignalCall)
|
|
func recipientAcceptedCall(_ call: SignalCall)
|
|
func localHangupCall(localId: UUID)
|
|
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: SignalCall.CallError)
|
|
func setIsMuted(call: SignalCall, isMuted: Bool)
|
|
func setHasLocalVideo(call: SignalCall, hasLocalVideo: Bool)
|
|
func startAndShowOutgoingCall(thread: TSContactThread, hasLocalVideo: Bool)
|
|
}
|
|
|
|
// Shared default implementations
|
|
extension CallUIAdaptee {
|
|
|
|
internal func showCall(_ call: SignalCall) {
|
|
AssertIsOnMainThread()
|
|
if !call.isTerminatedIndividualCall {
|
|
Logger.info("showCall")
|
|
|
|
let callViewController = IndividualCallViewController(call: call)
|
|
callViewController.modalTransitionStyle = .crossDissolve
|
|
|
|
OWSWindowManager.shared.startCall(callViewController)
|
|
} else {
|
|
Logger.info("Not showing window for terminated individual call")
|
|
}
|
|
}
|
|
|
|
internal func reportMissedCall(_ call: SignalCall) {
|
|
AssertIsOnMainThread()
|
|
|
|
let sentAtTimestamp = Date(millisecondsSince1970: call.individualCall.sentAtTimestamp)
|
|
notificationPresenter.presentMissedCall(call,
|
|
caller: call.individualCall.remoteAddress,
|
|
sentAt: sentAtTimestamp)
|
|
}
|
|
|
|
internal func startAndShowOutgoingCall(thread: TSContactThread, hasLocalVideo: Bool) {
|
|
AssertIsOnMainThread()
|
|
|
|
guard let call = 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
|
|
}
|
|
|
|
Logger.debug("")
|
|
|
|
startOutgoingCall(call: call)
|
|
call.individualCall.hasLocalVideo = hasLocalVideo
|
|
self.showCall(call)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Notify the user of call related activities.
|
|
* Driven by either a CallKit or System notifications adaptee
|
|
*/
|
|
@objc
|
|
public class CallUIAdapter: NSObject, CallServiceObserver {
|
|
|
|
lazy var nonCallKitAdaptee = NonCallKitCallUIAdaptee()
|
|
|
|
lazy var callKitAdaptee: CallKitCallUIAdaptee? = {
|
|
if Platform.isSimulator {
|
|
// CallKit doesn't seem entirely supported in simulator.
|
|
// e.g. you can't receive calls in the call screen.
|
|
// So we use the non-CallKit call UI.
|
|
Logger.info("not using callkit adaptee for simulator.")
|
|
return nil
|
|
} else if CallUIAdapter.isCallkitDisabledForLocale {
|
|
Logger.info("not using callkit adaptee due to locale.")
|
|
return nil
|
|
} else {
|
|
Logger.info("using callkit adaptee for iOS11+")
|
|
let showNames = preferences.notificationPreviewType() != .noNameNoPreview
|
|
let useSystemCallLog = preferences.isSystemCallLogEnabled()
|
|
|
|
return CallKitCallUIAdaptee(showNamesOnCallScreen: showNames,
|
|
useSystemCallLog: useSystemCallLog)
|
|
}
|
|
}()
|
|
|
|
var defaultAdaptee: CallUIAdaptee { callKitAdaptee ?? nonCallKitAdaptee }
|
|
|
|
func adaptee(for call: SignalCall) -> CallUIAdaptee {
|
|
switch call.individualCall.callAdapterType {
|
|
case .nonCallKit: return nonCallKitAdaptee
|
|
case .default: return defaultAdaptee
|
|
}
|
|
}
|
|
|
|
public required override init() {
|
|
AssertIsOnMainThread()
|
|
|
|
super.init()
|
|
|
|
// We cannot assert singleton here, because this class gets rebuilt when the user changes relevant call settings
|
|
AppReadiness.runNowOrWhenAppDidBecomeReadySync {
|
|
self.callService.addObserverAndSyncState(observer: self)
|
|
}
|
|
}
|
|
|
|
@objc
|
|
public static var isCallkitDisabledForLocale: Bool {
|
|
let locale = Locale.current
|
|
guard let regionCode = locale.regionCode else {
|
|
if !Platform.isSimulator { owsFailDebug("Missing region code.") }
|
|
return false
|
|
}
|
|
|
|
// Apple has stopped approving apps that use CallKit functionality in mainland China.
|
|
// When the "CN" region is enabled, this check simply switches to the same pre-CallKit
|
|
// interface that is still used by everyone on iOS 9.
|
|
//
|
|
// For further reference: https://forums.developer.apple.com/thread/103083
|
|
return regionCode == "CN"
|
|
}
|
|
|
|
// MARK:
|
|
|
|
internal func reportIncomingCall(_ call: SignalCall) {
|
|
AssertIsOnMainThread()
|
|
|
|
Logger.info("remoteAddress: \(call.individualCall.remoteAddress)")
|
|
|
|
// make sure we don't terminate audio session during call
|
|
_ = audioSession.startAudioActivity(call.audioActivity)
|
|
|
|
adaptee(for: call).reportIncomingCall(call) { error in
|
|
AssertIsOnMainThread()
|
|
|
|
guard var error = error else { return }
|
|
|
|
let nsError: NSError = error as NSError
|
|
Logger.warn("Error: \(nsError.domain), \(nsError.code), error: \(error)")
|
|
if nsError.domain == CXErrorCodeIncomingCallError.errorDomain {
|
|
switch nsError.code {
|
|
case CXErrorCodeIncomingCallError.unknown.rawValue:
|
|
Logger.warn("unknown")
|
|
case CXErrorCodeIncomingCallError.unentitled.rawValue:
|
|
Logger.warn("unentitled")
|
|
case CXErrorCodeIncomingCallError.callUUIDAlreadyExists.rawValue:
|
|
Logger.warn("callUUIDAlreadyExists")
|
|
case CXErrorCodeIncomingCallError.filteredByDoNotDisturb.rawValue:
|
|
Logger.warn("filteredByDoNotDisturb")
|
|
error = SignalCall.CallError.doNotDisturbEnabled
|
|
case CXErrorCodeIncomingCallError.filteredByBlockList.rawValue:
|
|
Logger.warn("filteredByBlockList")
|
|
error = SignalCall.CallError.contactIsBlocked
|
|
default:
|
|
Logger.warn("Unknown CXErrorCodeIncomingCallError")
|
|
}
|
|
}
|
|
|
|
self.callService.handleFailedCall(failedCall: call, error: error)
|
|
}
|
|
}
|
|
|
|
internal func reportMissedCall(_ call: SignalCall) {
|
|
AssertIsOnMainThread()
|
|
|
|
adaptee(for: call).reportMissedCall(call)
|
|
}
|
|
|
|
internal func startOutgoingCall(call: SignalCall) {
|
|
AssertIsOnMainThread()
|
|
|
|
adaptee(for: call).startOutgoingCall(call: call)
|
|
}
|
|
|
|
@objc
|
|
public func answerCall(localId: UUID) {
|
|
AssertIsOnMainThread()
|
|
|
|
guard let call = self.callService.currentCall else {
|
|
owsFailDebug("No current call.")
|
|
return
|
|
}
|
|
|
|
guard call.localId == localId else {
|
|
owsFailDebug("localId does not match current call")
|
|
return
|
|
}
|
|
|
|
adaptee(for: call).answerCall(localId: localId)
|
|
}
|
|
|
|
internal func answerCall(_ call: SignalCall) {
|
|
AssertIsOnMainThread()
|
|
|
|
adaptee(for: call).answerCall(call)
|
|
}
|
|
|
|
@objc
|
|
public func startAndShowOutgoingCall(thread: TSContactThread, hasLocalVideo: Bool) {
|
|
AssertIsOnMainThread()
|
|
|
|
defaultAdaptee.startAndShowOutgoingCall(thread: thread, hasLocalVideo: hasLocalVideo)
|
|
}
|
|
|
|
internal func recipientAcceptedCall(_ call: SignalCall) {
|
|
AssertIsOnMainThread()
|
|
|
|
adaptee(for: call).recipientAcceptedCall(call)
|
|
}
|
|
|
|
internal func remoteDidHangupCall(_ call: SignalCall) {
|
|
AssertIsOnMainThread()
|
|
|
|
adaptee(for: call).remoteDidHangupCall(call)
|
|
}
|
|
|
|
internal func remoteBusy(_ call: SignalCall) {
|
|
AssertIsOnMainThread()
|
|
|
|
adaptee(for: call).remoteBusy(call)
|
|
}
|
|
|
|
internal func didAnswerElsewhere(call: SignalCall) {
|
|
adaptee(for: call).didAnswerElsewhere(call: call)
|
|
}
|
|
|
|
internal func didDeclineElsewhere(call: SignalCall) {
|
|
adaptee(for: call).didDeclineElsewhere(call: call)
|
|
}
|
|
|
|
internal func wasBusyElsewhere(call: SignalCall) {
|
|
adaptee(for: call).wasBusyElsewhere(call: call)
|
|
}
|
|
|
|
internal func localHangupCall(localId: UUID) {
|
|
AssertIsOnMainThread()
|
|
|
|
guard let call = self.callService.currentCall else {
|
|
owsFailDebug("No current call.")
|
|
return
|
|
}
|
|
|
|
guard call.localId == localId else {
|
|
owsFailDebug("localId does not match current call")
|
|
return
|
|
}
|
|
|
|
adaptee(for: call).localHangupCall(localId: localId)
|
|
}
|
|
|
|
internal func localHangupCall(_ call: SignalCall) {
|
|
AssertIsOnMainThread()
|
|
|
|
adaptee(for: call).localHangupCall(call)
|
|
}
|
|
|
|
internal func failCall(_ call: SignalCall, error: SignalCall.CallError) {
|
|
AssertIsOnMainThread()
|
|
|
|
adaptee(for: call).failCall(call, error: error)
|
|
}
|
|
|
|
internal func showCall(_ call: SignalCall) {
|
|
AssertIsOnMainThread()
|
|
|
|
adaptee(for: call).showCall(call)
|
|
}
|
|
|
|
internal func setIsMuted(call: SignalCall, isMuted: Bool) {
|
|
AssertIsOnMainThread()
|
|
|
|
// With CallKit, muting is handled by a CXAction, so it must go through the adaptee
|
|
adaptee(for: call).setIsMuted(call: call, isMuted: isMuted)
|
|
}
|
|
|
|
internal func setHasLocalVideo(call: SignalCall, hasLocalVideo: Bool) {
|
|
AssertIsOnMainThread()
|
|
|
|
adaptee(for: call).setHasLocalVideo(call: call, hasLocalVideo: hasLocalVideo)
|
|
}
|
|
|
|
internal func setCameraSource(call: SignalCall, isUsingFrontCamera: Bool) {
|
|
AssertIsOnMainThread()
|
|
|
|
callService.updateCameraSource(call: call, isUsingFrontCamera: isUsingFrontCamera)
|
|
}
|
|
|
|
// MARK: - CallServiceObserver
|
|
|
|
internal func didUpdateCall(from oldValue: SignalCall?, to newValue: SignalCall?) {
|
|
AssertIsOnMainThread()
|
|
|
|
guard let call = newValue, call.isIndividualCall else { return }
|
|
|
|
callService.audioService.handleRinging = adaptee(for: call).hasManualRinger
|
|
}
|
|
}
|