session-ios/Session/Calls/UserInterface/Individual/CallUIAdapter.swift

313 lines
10 KiB
Swift

//
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
//
import Foundation
import PromiseKit
import CallKit
import SignalServiceKit
import SignalMessaging
import WebRTC
protocol CallUIAdaptee {
var notificationPresenter: NotificationPresenter { get }
var callService: CallService { get }
var hasManualRinger: Bool { get }
func startOutgoingCall(call: SignalCall)
func reportIncomingCall(_ call: SignalCall, callerName: String, completion: @escaping (Error?) -> Void)
func reportMissedCall(_ call: SignalCall, callerName: String)
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 failCall(_ call: SignalCall, error: SignalCall.CallError)
func setIsMuted(call: SignalCall, isMuted: Bool)
func setHasLocalVideo(call: SignalCall, hasLocalVideo: Bool)
func startAndShowOutgoingCall(address: SignalServiceAddress, hasLocalVideo: Bool)
}
// Shared default implementations
extension CallUIAdaptee {
internal func showCall(_ call: SignalCall) {
AssertIsOnMainThread()
let callViewController = IndividualCallViewController(call: call)
callViewController.modalTransitionStyle = .crossDissolve
OWSWindowManager.shared.startCall(callViewController)
}
internal func reportMissedCall(_ call: SignalCall, callerName: String) {
AssertIsOnMainThread()
notificationPresenter.presentMissedCall(call.individualCall, callerName: callerName)
}
internal func startAndShowOutgoingCall(address: SignalServiceAddress, hasLocalVideo: Bool) {
AssertIsOnMainThread()
guard let call = self.callService.buildOutgoingIndividualCallIfPossible(
address: address,
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: \(address)")
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, thread: TSContactThread) {
AssertIsOnMainThread()
Logger.info("remoteAddress: \(call.individualCall.remoteAddress)")
// make sure we don't terminate audio session during call
_ = audioSession.startAudioActivity(call.audioActivity)
let callerName = self.contactsManager.displayName(for: call.individualCall.remoteAddress)
Logger.verbose("callerName: \(callerName)")
adaptee(for: call).reportIncomingCall(call, callerName: callerName) { error in
AssertIsOnMainThread()
guard let error = error else { return }
owsFailDebug("Failed to report incoming call with error \(error)")
let nsError = error as NSError
Logger.warn("nsError: \(nsError.domain), \(nsError.code)")
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")
case CXErrorCodeIncomingCallError.filteredByBlockList.rawValue:
Logger.warn("filteredByBlockList")
default:
Logger.warn("Unknown CXErrorCodeIncomingCallError")
}
}
self.callService.handleFailedCall(failedCall: call, error: error)
}
}
internal func reportMissedCall(_ call: SignalCall) {
AssertIsOnMainThread()
let callerName = self.contactsManager.displayName(for: call.individualCall.remoteAddress)
adaptee(for: call).reportMissedCall(call, callerName: callerName)
}
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.individualCall.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(address: SignalServiceAddress, hasLocalVideo: Bool) {
AssertIsOnMainThread()
defaultAdaptee.startAndShowOutgoingCall(address: address, 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 localHangupCall(localId: UUID) {
AssertIsOnMainThread()
guard let call = self.callService.currentCall else {
owsFailDebug("No current call.")
return
}
guard call.individualCall.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
}
}