mirror of https://github.com/oxen-io/session-ios
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
314 lines
11 KiB
Swift
314 lines
11 KiB
Swift
//
|
|
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
import PromiseKit
|
|
import CallKit
|
|
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(publicKey: String, 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(publicKey: String, hasLocalVideo: Bool) {
|
|
AssertIsOnMainThread()
|
|
|
|
guard let call = self.callService.buildOutgoingIndividualCallIfPossible(
|
|
publicKey: publicKey,
|
|
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: \(publicKey)")
|
|
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 preferences = Environment.shared.preferences!
|
|
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.publicKey)")
|
|
|
|
// make sure we don't terminate audio session during call
|
|
_ = audioSession.startAudioActivity(call.audioActivity)
|
|
|
|
let publicKey = call.individualCall.publicKey
|
|
let callerName = Storage.shared.getContact(with: publicKey)?.displayName(for: .regular) ?? publicKey
|
|
|
|
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 publicKey = call.individualCall.publicKey
|
|
let callerName = Storage.shared.getContact(with: publicKey)?.displayName(for: .regular) ?? publicKey
|
|
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(publicKey: String, hasLocalVideo: Bool) {
|
|
AssertIsOnMainThread()
|
|
|
|
defaultAdaptee.startAndShowOutgoingCall(publicKey: publicKey, 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
|
|
}
|
|
}
|