TM-SGNL-iOS/Signal/Calls/CallService.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

1658 lines
63 KiB
Swift

//
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import AVFoundation
import LibSignalClient
import SignalRingRTC
import SignalServiceKit
import SignalUI
import WebRTC
/// Manages events related to both 1:1 and group calls, while the main app is
/// running.
///
/// Responsible for the 1:1 or group call this device is currently active in, if
/// any, as well as any other updates to other calls that we learn about.
final class CallService: CallServiceStateObserver, CallServiceStateDelegate {
public typealias CallManagerType = CallManager<SignalCall, CallService>
public let callManager: CallManagerType
// Even though we never use this, we need to retain it to ensure
// `callManager` continues to work properly.
private let callManagerHttpClient: AnyObject
private var adHocCallRecordManager: any AdHocCallRecordManager { DependenciesBridge.shared.adHocCallRecordManager }
private let appReadiness: AppReadiness
private var audioSession: AudioSession { SUIEnvironment.shared.audioSessionRef }
private var callLinkStore: any CallLinkRecordStore { DependenciesBridge.shared.callLinkStore }
let authCredentialManager: any AuthCredentialManager
private var databaseStorage: SDSDatabaseStorage { SSKEnvironment.shared.databaseStorageRef }
private let db: any DB
private var deviceSleepManager: DeviceSleepManager { DeviceSleepManager.shared }
private var groupCallManager: GroupCallManager { SSKEnvironment.shared.groupCallManagerRef }
private var messageSenderJobQueue: MessageSenderJobQueue { SSKEnvironment.shared.messageSenderJobQueueRef }
private var reachabilityManager: SSKReachabilityManager { SSKEnvironment.shared.reachabilityManagerRef }
public var callUIAdapter: CallUIAdapter
let individualCallService: IndividualCallService
let groupCallRemoteVideoManager: GroupCallRemoteVideoManager
let callLinkManager: CallLinkManagerImpl
let callLinkFetcher: CallLinkFetcherImpl
let callLinkStateUpdater: CallLinkStateUpdater
private var adHocCallStateObserver: AdHocCallStateObserver?
/// Needs to be lazily initialized, because it uses singletons that are not
/// available when this class is initialized.
private lazy var groupCallAccessoryMessageDelegate: GroupCallAccessoryMessageDelegate = {
return GroupCallAccessoryMessageHandler(
databaseStorage: databaseStorage,
groupCallRecordManager: DependenciesBridge.shared.groupCallRecordManager,
messageSenderJobQueue: messageSenderJobQueue
)
}()
/// Needs to be lazily initialized, because it uses singletons that are not
/// available when this class is initialized.
private lazy var groupCallRecordRingUpdateDelegate: GroupCallRecordRingUpdateDelegate = {
return GroupCallRecordRingUpdateHandler(
callRecordStore: DependenciesBridge.shared.callRecordStore,
groupCallRecordManager: DependenciesBridge.shared.groupCallRecordManager,
interactionStore: DependenciesBridge.shared.interactionStore,
threadStore: DependenciesBridge.shared.threadStore
)
}()
@MainActor
private(set) lazy var audioService: CallAudioService = {
let result = CallAudioService(audioSession: self.audioSession)
callServiceState.addObserver(result, syncStateImmediately: true)
return result
}()
public let earlyRingNextIncomingCall = AtomicBool(false, lock: .init())
let callServiceState: CallServiceState
var notificationObservers: [any NSObjectProtocol] = []
@MainActor
public init(
appContext: any AppContext,
appReadiness: AppReadiness,
authCredentialManager: any AuthCredentialManager,
callLinkPublicParams: GenericServerPublicParams,
callLinkStore: any CallLinkRecordStore,
callRecordDeleteManager: any CallRecordDeleteManager,
callRecordStore: any CallRecordStore,
db: any DB,
mutableCurrentCall: AtomicValue<SignalCall?>,
networkManager: NetworkManager,
tsAccountManager: any TSAccountManager
) {
self.appReadiness = appReadiness
self.authCredentialManager = authCredentialManager
let httpClient = CallHTTPClient()
self.callManager = CallManager<SignalCall, CallService>(
httpClient: httpClient.ringRtcHttpClient,
fieldTrials: RingrtcFieldTrials.trials(with: appContext.appUserDefaults())
)
self.callManagerHttpClient = httpClient
let callUIAdapter = CallUIAdapter()
self.callUIAdapter = callUIAdapter
self.callServiceState = CallServiceState(currentCall: mutableCurrentCall)
self.individualCallService = IndividualCallService(
callManager: self.callManager,
callServiceState: self.callServiceState
)
self.groupCallRemoteVideoManager = GroupCallRemoteVideoManager(
callServiceState: self.callServiceState
)
self.callLinkFetcher = CallLinkFetcherImpl()
self.callLinkManager = CallLinkManagerImpl(
networkManager: networkManager,
serverParams: callLinkPublicParams,
tsAccountManager: tsAccountManager
)
self.callLinkStateUpdater = CallLinkStateUpdater(
authCredentialManager: authCredentialManager,
callLinkFetcher: self.callLinkFetcher,
callLinkManager: self.callLinkManager,
callLinkStore: callLinkStore,
callRecordDeleteManager: callRecordDeleteManager,
callRecordStore: callRecordStore,
db: db,
tsAccountManager: tsAccountManager
)
self.db = db
self.callManager.delegate = self
SwiftSingletons.register(self)
self.callServiceState.addObserver(self)
notificationObservers.append(NotificationCenter.default.addObserver(forName: .OWSApplicationDidEnterBackground, object: nil, queue: .main) { [weak self] _ in
MainActor.assumeIsolated { self?.didEnterBackground() }
})
notificationObservers.append(NotificationCenter.default.addObserver(forName: .OWSApplicationDidBecomeActive, object: nil, queue: .main) { [weak self] _ in
MainActor.assumeIsolated { self?.didBecomeActive() }
})
notificationObservers.append(NotificationCenter.default.addObserver(forName: Self.callServicePreferencesDidChange, object: nil, queue: .main) { [weak self] _ in
MainActor.assumeIsolated { self?.configureDataMode() }
})
notificationObservers.append(NotificationCenter.default.addObserver(forName: .registrationStateDidChange, object: nil, queue: .main) { [weak self] _ in
MainActor.assumeIsolated { self?.registrationChanged() }
})
// Note that we're not using the usual .owsReachabilityChanged
// We want to update our data mode if the app has been backgrounded
notificationObservers.append(NotificationCenter.default.addObserver(forName: .reachabilityChanged, object: nil, queue: .main) { [weak self] _ in
MainActor.assumeIsolated { self?.configureDataMode() }
})
// We don't support a rotating call screen on phones,
// but we do still want to rotate the various icons.
if !UIDevice.current.isIPad {
notificationObservers.append(NotificationCenter.default.addObserver(forName: UIDevice.orientationDidChangeNotification, object: nil, queue: .main) { [weak self] _ in
MainActor.assumeIsolated { self?.phoneOrientationDidChange() }
})
}
appReadiness.runNowOrWhenAppWillBecomeReady {
if let localAci = DependenciesBridge.shared.tsAccountManager.localIdentifiersWithMaybeSneakyTransaction?.aci {
self.callManager.setSelfUuid(localAci.rawUUID)
}
}
appReadiness.runNowOrWhenAppDidBecomeReadyAsync {
DependenciesBridge.shared.databaseChangeObserver.appendDatabaseChangeDelegate(self)
self.callServiceState.addObserver(self.groupCallAccessoryMessageDelegate, syncStateImmediately: true)
self.callServiceState.addObserver(self.groupCallRemoteVideoManager, syncStateImmediately: true)
}
}
deinit {
for observer in notificationObservers {
NotificationCenter.default.removeObserver(observer)
}
}
/**
* Choose whether to use CallKit or a Notification backed interface for calling.
*/
@MainActor
public func rebuildCallUIAdapter() {
if let currentCall = callServiceState.currentCall {
Logger.warn("ending current call in. Did user toggle callkit preference while in a call?")
callServiceState.terminateCall(currentCall)
}
self.callUIAdapter = CallUIAdapter()
}
private let sleepBlockObject = DeviceSleepManager.BlockObject(blockReason: "call")
func didUpdateCall(from oldValue: SignalCall?, to newValue: SignalCall?) {
switch oldValue?.mode {
case nil:
break
case .individual(let call):
call.removeObserver(self)
case .groupThread(let call):
call.removeObserver(self)
case .callLink(let call):
self.adHocCallStateObserver = nil
call.removeObserver(self)
}
switch newValue?.mode {
case nil:
break
case .individual(let call):
call.addObserverAndSyncState(self)
case .groupThread(let call):
call.addObserver(self, syncStateImmediately: true)
case .callLink(let call):
self.adHocCallStateObserver = AdHocCallStateObserver(
callLinkCall: call,
adHocCallRecordManager: adHocCallRecordManager,
callLinkStore: callLinkStore,
messageSenderJobQueue: messageSenderJobQueue,
db: db
)
call.addObserver(self, syncStateImmediately: true)
}
updateIsVideoEnabled()
// Prevent device from sleeping while we have an active call.
if oldValue != nil {
self.deviceSleepManager.removeBlock(blockObject: sleepBlockObject)
}
if newValue != nil {
self.deviceSleepManager.addBlock(blockObject: sleepBlockObject)
}
if !UIDevice.current.isIPad {
if oldValue != nil {
UIDevice.current.endGeneratingDeviceOrientationNotifications()
}
if newValue != nil {
UIDevice.current.beginGeneratingDeviceOrientationNotifications()
}
}
switch newValue?.mode {
case .individual:
// By default, individual calls should start out with speakerphone disabled.
self.audioService.requestSpeakerphone(isEnabled: false)
case .groupThread, .callLink, nil:
break
}
// To be safe, we reset the early ring on any call change so it's not left set from an unexpected state change.
earlyRingNextIncomingCall.set(false)
}
func callServiceState(_ callServiceState: CallServiceState, didTerminateCall call: SignalCall) {
if callServiceState.currentCall == nil {
audioSession.isRTCAudioEnabled = false
}
audioSession.endAudioActivity(call.commonState.audioActivity)
switch call.mode {
case .individual:
break
case .groupThread(let call):
// Kick off a peek now that we've disconnected to get an updated participant state.
Task {
await self.groupCallManager.peekGroupCallAndUpdateThread(
forGroupId: call.groupId,
peekTrigger: .localEvent()
)
}
case .callLink:
break
}
}
// MARK: -
/**
* Local user toggled to mute audio.
*/
@MainActor
func updateIsLocalAudioMuted(isLocalAudioMuted: Bool) {
// Keep a reference to the call before permissions were requested...
guard let currentCall = callServiceState.currentCall else {
owsFailDebug("missing currentCall")
return
}
// If we're disabling the microphone, we don't need permission. Only need
// permission to *enable* the microphone.
guard !isLocalAudioMuted else {
return updateIsLocalAudioMutedWithMicrophonePermission(call: currentCall, isLocalAudioMuted: isLocalAudioMuted)
}
// This method can be initiated either from the CallViewController.videoButton or via CallKit
// in either case we want to show the alert on the callViewWindow.
guard let frontmostViewController = AppEnvironment.shared.windowManagerRef.callViewWindow.findFrontmostViewController(ignoringAlerts: true) else {
owsFailDebug("could not identify frontmostViewController")
return
}
frontmostViewController.ows_askForMicrophonePermissions { granted in
// Make sure the call is still valid (the one we asked permissions for).
guard self.callServiceState.currentCall === currentCall else {
Logger.info("ignoring microphone permissions for obsolete call")
return
}
if !granted {
frontmostViewController.ows_showNoMicrophonePermissionActionSheet()
}
let mutedAfterAskingForPermission = !granted
self.updateIsLocalAudioMutedWithMicrophonePermission(call: currentCall, isLocalAudioMuted: mutedAfterAskingForPermission)
}
}
@MainActor
private func updateIsLocalAudioMutedWithMicrophonePermission(call: SignalCall, isLocalAudioMuted: Bool) {
owsPrecondition(call === callServiceState.currentCall)
switch call.mode {
case .groupThread(let call as GroupCall), .callLink(let call as GroupCall):
call.ringRtcCall.isOutgoingAudioMuted = isLocalAudioMuted
call.groupCall(onLocalDeviceStateChanged: call.ringRtcCall)
case .individual(let individualCall):
individualCall.isMuted = isLocalAudioMuted
individualCallService.ensureAudioState(call: call)
}
}
/**
* Local user toggled video.
*/
@MainActor
func updateIsLocalVideoMuted(isLocalVideoMuted: Bool) {
// Keep a reference to the call before permissions were requested...
guard let currentCall = callServiceState.currentCall else {
owsFailDebug("missing currentCall")
return
}
// If we're disabling local video, we don't need permission. Only need
// permission to *enable* video.
guard !isLocalVideoMuted else {
return updateIsLocalVideoMutedWithCameraPermissions(call: currentCall, isLocalVideoMuted: isLocalVideoMuted)
}
// This method can be initiated either from the CallViewController.videoButton or via CallKit
// in either case we want to show the alert on the callViewWindow.
let frontmostViewController = AppEnvironment.shared.windowManagerRef.callViewWindow.findFrontmostViewController(ignoringAlerts: true)
guard let frontmostViewController else {
owsFailDebug("could not identify frontmostViewController")
return
}
frontmostViewController.ows_askForCameraPermissions { granted in
// Make sure the call is still valid (the one we asked permissions for).
guard self.callServiceState.currentCall === currentCall else {
Logger.info("ignoring camera permissions for obsolete call")
return
}
let mutedAfterAskingForPermission = !granted
self.updateIsLocalVideoMutedWithCameraPermissions(call: currentCall, isLocalVideoMuted: mutedAfterAskingForPermission)
}
}
@MainActor
private func updateIsLocalVideoMutedWithCameraPermissions(call: SignalCall, isLocalVideoMuted: Bool) {
owsPrecondition(call === callServiceState.currentCall)
switch call.mode {
case .groupThread(let call as GroupCall), .callLink(let call as GroupCall):
call.ringRtcCall.isOutgoingVideoMuted = isLocalVideoMuted
call.groupCall(onLocalDeviceStateChanged: call.ringRtcCall)
case .individual(let individualCall):
individualCall.hasLocalVideo = !isLocalVideoMuted
}
updateIsVideoEnabled()
}
@MainActor
func updateCameraSource(call: SignalCall, isUsingFrontCamera: Bool) {
call.videoCaptureController.switchCamera(isUsingFrontCamera: isUsingFrontCamera)
}
@MainActor
private func configureDataMode() {
guard appReadiness.isAppReady else { return }
guard let currentCall = callServiceState.currentCall else { return }
switch currentCall.mode {
case .groupThread(let call):
let useLowData = shouldUseLowDataWithSneakyTransaction(for: call.ringRtcCall.localDeviceState.networkRoute)
Logger.info("Configuring call for \(useLowData ? "low" : "standard") data")
call.ringRtcCall.updateDataMode(dataMode: useLowData ? .low : .normal)
case let .individual(call) where call.state == .connected:
let useLowData = shouldUseLowDataWithSneakyTransaction(for: call.networkRoute)
Logger.info("Configuring call for \(useLowData ? "low" : "standard") data")
callManager.updateDataMode(dataMode: useLowData ? .low : .normal)
default:
// Do nothing. We'll reapply the data mode once connected
break
}
}
func shouldUseLowDataWithSneakyTransaction(for networkRoute: NetworkRoute) -> Bool {
let highDataInterfaces = databaseStorage.read { readTx in
Self.highDataNetworkInterfaces(readTx: readTx)
}
if let allowsHighData = highDataInterfaces.includes(networkRoute.localAdapterType) {
return !allowsHighData
}
// If we aren't sure whether the current route's high-data, fall back to checking reachability.
// This also handles the situation where WebRTC doesn't know what interface we're on,
// which is always true on iOS 11.
return !reachabilityManager.isReachable(with: highDataInterfaces)
}
// MARK: -
// This method should be called when a fatal error occurred for a call.
//
// * If we know which call it was, we should update that call's state
// to reflect the error.
// * IFF that call is the current call, we want to terminate it.
@MainActor
public func handleFailedCall(failedCall: SignalCall, error: Error) {
switch failedCall.mode {
case .individual:
individualCallService.handleFailedCall(
failedCall: failedCall,
error: error,
shouldResetUI: false,
shouldResetRingRTC: true
)
case .groupThread(let groupCall as GroupCall), .callLink(let groupCall as GroupCall):
leaveAndTerminateGroupCall(failedCall, groupCall: groupCall)
}
}
@MainActor
func handleLocalHangupCall(_ call: SignalCall) {
switch call.mode {
case .individual:
individualCallService.handleLocalHangupCall(call)
case .groupThread(let groupThreadCall):
if case .incomingRing(_, let ringId) = groupThreadCall.groupCallRingState {
groupCallAccessoryMessageDelegate.localDeviceDeclinedGroupRing(
ringId: ringId,
groupId: groupThreadCall.groupId
)
do {
try callManager.cancelGroupRing(
groupId: groupThreadCall.groupId.serialize().asData,
ringId: ringId,
reason: .declinedByUser
)
} catch {
owsFailDebug("RingRTC failed to cancel group ring \(ringId): \(error)")
}
}
leaveAndTerminateGroupCall(call, groupCall: groupThreadCall)
case .callLink(let callLinkCall):
leaveAndTerminateGroupCall(call, groupCall: callLinkCall)
}
}
// MARK: - Video
@MainActor
var shouldHaveLocalVideoTrack: Bool {
guard let call = self.callServiceState.currentCall else {
return false
}
// The iOS simulator doesn't provide any sort of camera capture
// support or emulation (http://goo.gl/rHAnC1) so don't bother
// trying to open a local stream.
guard !Platform.isSimulator else { return false }
guard UIApplication.shared.applicationState != .background else { return false }
switch call.mode {
case .individual(let individualCall):
return individualCall.state == .connected && individualCall.hasLocalVideo
case .groupThread(let call as GroupCall), .callLink(let call as GroupCall):
return !call.ringRtcCall.isOutgoingVideoMuted
}
}
@MainActor
func updateIsVideoEnabled() {
guard let call = self.callServiceState.currentCall else { return }
switch call.mode {
case .individual(let individualCall):
if individualCall.state == .connected || individualCall.state == .reconnecting {
callManager.setLocalVideoEnabled(enabled: shouldHaveLocalVideoTrack, call: call)
} else if individualCall.isViewLoaded, individualCall.hasLocalVideo, !Platform.isSimulator {
// If we're not yet connected, just enable the camera but don't tell RingRTC
// to start sending video. This allows us to show a "vanity" view while connecting.
individualCall.videoCaptureController.startCapture()
} else {
individualCall.videoCaptureController.stopCapture()
}
case .groupThread(let call as GroupCall), .callLink(let call as GroupCall):
if shouldHaveLocalVideoTrack {
call.videoCaptureController.startCapture()
} else {
call.videoCaptureController.stopCapture()
}
}
}
// MARK: -
@MainActor
func buildAndConnectGroupCall(for groupId: GroupIdentifier, isVideoMuted: Bool) -> (SignalCall, GroupThreadCall)? {
return _buildAndConnectGroupCall(isOutgoingVideoMuted: isVideoMuted) { () -> (SignalCall, GroupThreadCall)? in
let videoCaptureController = VideoCaptureController()
let sfuUrl = DebugFlags.callingUseTestSFU.get() ? TSConstants.sfuTestURL : TSConstants.sfuURL
let ringRtcCall = callManager.createGroupCall(
groupId: groupId.serialize().asData,
sfuUrl: sfuUrl,
hkdfExtraInfo: Data(),
audioLevelsIntervalMillis: nil,
videoCaptureController: videoCaptureController
)
guard let ringRtcCall else {
return nil
}
let groupThreadCall = GroupThreadCall(
delegate: self,
ringRtcCall: ringRtcCall,
groupId: groupId,
videoCaptureController: videoCaptureController
)
guard let groupThreadCall else {
return nil
}
return (SignalCall(groupThreadCall: groupThreadCall), groupThreadCall)
}
}
/// Rather than always fetching the current `CallLinkState`,
/// there may be times when we already have a reasonably
/// up-to-date copy of the state and do not wish to have to,
/// say, block UI waiting on a re-fetch. If in doubt, use
/// `.fetch`. Because that is "so fetch."
enum CallLinkStateRetrievalStrategy {
case reuse(SignalServiceKit.CallLinkState)
case fetch
}
@MainActor
func buildAndConnectCallLinkCall(
callLink: CallLink,
callLinkStateRetrievalStrategy: CallLinkStateRetrievalStrategy
) async throws -> (SignalCall, CallLinkCall)? {
let state: SignalServiceKit.CallLinkState
switch callLinkStateRetrievalStrategy {
case .reuse(let callLinkState):
state = callLinkState
case .fetch:
state = try await callLinkStateUpdater.readCallLink(rootKey: callLink.rootKey).get()
}
let localIdentifiers = DependenciesBridge.shared.tsAccountManager.localIdentifiersWithMaybeSneakyTransaction!
let authCredential = try await authCredentialManager.fetchCallLinkAuthCredential(localIdentifiers: localIdentifiers)
let (adminPasskey, isDeleted) = try databaseStorage.read { tx -> (Data?, Bool) in
let callLinkRecord = try callLinkStore.fetch(roomId: callLink.rootKey.deriveRoomId(), tx: tx.asV2Read)
return (callLinkRecord?.adminPasskey, callLinkRecord?.isDeleted == true)
}
if isDeleted {
throw OWSGenericError("Can't join a call link that you've deleted.")
}
return _buildAndConnectGroupCall(isOutgoingVideoMuted: false) { () -> (SignalCall, CallLinkCall)? in
let videoCaptureController = VideoCaptureController()
let sfuUrl = DebugFlags.callingUseTestSFU.get() ? TSConstants.sfuTestURL : TSConstants.sfuURL
let secretParams = CallLinkSecretParams.deriveFromRootKey(callLink.rootKey.bytes)
let authCredentialPresentation = authCredential.present(callLinkParams: secretParams)
let ringRtcCall = callManager.createCallLinkCall(
sfuUrl: sfuUrl,
authCredentialPresentation: authCredentialPresentation.serialize(),
linkRootKey: callLink.rootKey,
adminPasskey: adminPasskey,
hkdfExtraInfo: Data(),
audioLevelsIntervalMillis: nil,
videoCaptureController: videoCaptureController
)
guard let ringRtcCall else {
return nil
}
let callLinkCall = CallLinkCall(
callLink: callLink,
adminPasskey: adminPasskey,
callLinkState: state,
ringRtcCall: ringRtcCall,
videoCaptureController: videoCaptureController
)
return (SignalCall(callLinkCall: callLinkCall), callLinkCall)
}
}
@MainActor
private func _buildAndConnectGroupCall<T: GroupCall>(
isOutgoingVideoMuted: Bool,
createCall: () -> (SignalCall, T)?
) -> (SignalCall, T)? {
guard callServiceState.currentCall == nil else {
return nil
}
guard let (call, groupCall) = createCall() else {
owsFailDebug("Failed to create call")
return nil
}
// By default, group calls should start out with speakerphone enabled.
self.audioService.requestSpeakerphone(isEnabled: true)
groupCall.ringRtcCall.isOutgoingAudioMuted = false
groupCall.ringRtcCall.isOutgoingVideoMuted = isOutgoingVideoMuted
callServiceState.setCurrentCall(call)
// Connect (but don't join) to subscribe to live updates.
guard connectGroupCallIfNeeded(groupCall) else {
callServiceState.terminateCall(call)
return nil
}
return (call, groupCall)
}
@MainActor
func joinGroupCallIfNecessary(_ call: SignalCall, groupCall: GroupCall) {
guard call === self.callServiceState.currentCall else {
owsFailDebug("Can't join a group call if it's not the current call")
return
}
// If we're disconnected, it means we hit an error with the first
// connection, so connect now. (Ex: You try to join a call that's full, and
// then you try to join again.)
guard connectGroupCallIfNeeded(groupCall) else {
owsFailDebug("Can't join a group call if we can't connect()")
return
}
// If we're not yet joined, join now. In general, it's unexpected that
// this method would be called when you're already joined, but it is
// safe to do so.
let ringRtcCall = groupCall.ringRtcCall
if ringRtcCall.localDeviceState.joinState == .notJoined {
ringRtcCall.join()
// Group calls can get disconnected, but we don't count that as ending the call.
// So this call may have already been reported.
if groupCall.commonState.systemState == .notReported {
callUIAdapter.startOutgoingCall(call: call)
}
}
}
@MainActor
private func connectGroupCallIfNeeded(_ groupCall: GroupCall) -> Bool {
if groupCall.hasInvokedConnectMethod {
return true
}
// If we haven't invoked the method, we shouldn't be connected. (Note: The
// converse is NOT true, and that's why we need `hasInvokedConnectMethod`.)
owsAssertDebug(groupCall.ringRtcCall.localDeviceState.connectionState == .notConnected)
let result = groupCall.ringRtcCall.connect()
if result {
groupCall.hasInvokedConnectMethod = true
}
return result
}
/// Leaves the group call & schedules it for termination.
///
/// If the call has already "ended" (RingRTC term), perhaps because we
/// encountered an error, it will terminate the group call immediately.
///
/// We wait for the call to end before terminating to ensure that observers
/// have an opportunity to handle the "call ended" event.
@MainActor
private func leaveAndTerminateGroupCall(_ call: SignalCall, groupCall: GroupCall) {
if groupCall.hasInvokedConnectMethod {
groupCall.ringRtcCall.disconnect()
groupCall.shouldTerminateOnEndEvent = true
} else {
callServiceState.terminateCall(call)
}
}
func initiateCall(to callTarget: CallTarget, isVideo: Bool) {
switch callTarget {
case .individual(let contactThread):
Task { await self.initiateIndividualCall(thread: contactThread, isVideo: isVideo) }
case .groupThread(let groupId):
GroupCallViewController.presentLobby(forGroupId: groupId, videoMuted: !isVideo)
case .callLink(let callLink):
GroupCallViewController.presentLobby(for: callLink)
}
}
@MainActor
private func initiateIndividualCall(thread: TSContactThread, isVideo: Bool) async {
let untrustedThreshold = Date(timeIntervalSinceNow: -OWSIdentityManagerImpl.Constants.defaultUntrustedInterval)
guard let frontmostViewController = UIApplication.shared.frontmostViewController else {
owsFail("Can't start a call if there's no view controller")
}
guard await CallStarter.prepareToStartCall(from: frontmostViewController, shouldAskForCameraPermission: isVideo) else {
return
}
guard await SafetyNumberConfirmationSheet.presentRepeatedlyAsNecessary(
for: { [thread.contactAddress] },
from: frontmostViewController,
confirmationText: CallStrings.confirmAndCallButtonTitle,
untrustedThreshold: untrustedThreshold
) else {
return
}
self.callUIAdapter.startAndShowOutgoingCall(thread: thread, hasLocalVideo: isVideo)
}
func buildOutgoingIndividualCallIfPossible(thread: TSContactThread, hasVideo: Bool) -> (SignalCall, IndividualCall)? {
AssertIsOnMainThread()
guard callServiceState.currentCall == nil else { return nil }
let individualCall = IndividualCall.outgoingIndividualCall(
thread: thread,
offerMediaType: hasVideo ? .video : .audio
)
let call = SignalCall(individualCall: individualCall)
return (call, individualCall)
}
// MARK: - Notifications
@MainActor
private func didEnterBackground() {
self.updateIsVideoEnabled()
}
@MainActor
private func didBecomeActive() {
self.updateIsVideoEnabled()
}
@MainActor
private func registrationChanged() {
if let localAci = DependenciesBridge.shared.tsAccountManager.localIdentifiersWithMaybeSneakyTransaction?.aci {
callManager.setSelfUuid(localAci.rawUUID)
}
}
/// The object is the rotation angle necessary to match the new orientation.
static var phoneOrientationDidChange = Notification.Name("CallService.phoneOrientationDidChange")
@MainActor
private func phoneOrientationDidChange() {
guard callServiceState.currentCall != nil else {
return
}
sendPhoneOrientationNotification()
}
@MainActor
private func shouldReorientUI(for call: SignalCall) -> Bool {
owsAssertDebug(!UIDevice.current.isIPad, "iPad has full UIKit rotation support")
switch call.mode {
case .individual(let individualCall):
// If we're in an audio-only 1:1 call, the user isn't going to be looking at the screen.
// Don't distract them with rotating icons.
return individualCall.hasLocalVideo || individualCall.isRemoteVideoEnabled
case .groupThread, .callLink:
// If we're in a group call, we don't want to use rotating icons because we
// don't rotate user video at the same time, and that's very obvious for
// grid view or any non-speaker tile in speaker view.
return false
}
}
@MainActor
private func sendPhoneOrientationNotification() {
owsAssertDebug(!UIDevice.current.isIPad, "iPad has full UIKit rotation support")
let rotationAngle: CGFloat
if let call = callServiceState.currentCall, !shouldReorientUI(for: call) {
// We still send the notification in case we *previously* rotated the UI and now we need to revert back.
// Example:
// 1. In a 1:1 call, either the user or their contact (but not both) has video on
// 2. the user has the phone in landscape
// 3. whoever had video turns it off (but the icons are still landscape-oriented)
// 4. the user rotates back to portrait
rotationAngle = 0
} else {
switch UIDevice.current.orientation {
case .landscapeLeft:
rotationAngle = .halfPi
case .landscapeRight:
rotationAngle = -.halfPi
case .portrait, .portraitUpsideDown, .faceDown, .faceUp, .unknown:
fallthrough
@unknown default:
rotationAngle = 0
}
}
NotificationCenter.default.post(name: Self.phoneOrientationDidChange, object: rotationAngle)
}
/// Pretend the phone just changed orientations so that the call UI will autorotate.
@MainActor
func sendInitialPhoneOrientationNotification() {
guard !UIDevice.current.isIPad else {
return
}
sendPhoneOrientationNotification()
}
// MARK: -
private func updateGroupMembersForCurrentCallIfNecessary() {
DispatchQueue.main.async {
let currentCall = self.callServiceState.currentCall
guard let groupThreadCall = currentCall?.unpackGroupCall() else {
return
}
let membershipInfo: [GroupMemberInfo]
do {
membershipInfo = try self.databaseStorage.read { tx in
try self.groupCallManager.groupCallPeekClient.groupMemberInfo(
forGroupId: groupThreadCall.groupId,
tx: tx.asV2Read
)
}
} catch {
owsFailDebug("Failed to fetch membership info: \(error)")
return
}
groupThreadCall.ringRtcCall.updateGroupMembers(members: membershipInfo)
}
}
// MARK: - Data Modes
static let callServicePreferencesDidChange = Notification.Name("CallServicePreferencesDidChange")
private static let keyValueStore = KeyValueStore(collection: "CallService")
// This used to be called "high bandwidth", but "data" is more accurate.
private static let highDataPreferenceKey = "HighBandwidthPreferenceKey"
static func setHighDataInterfaces(_ interfaceSet: NetworkInterfaceSet, writeTx: SDSAnyWriteTransaction) {
Logger.info("Updating preferred low data interfaces: \(interfaceSet.rawValue)")
keyValueStore.setUInt(interfaceSet.rawValue, key: highDataPreferenceKey, transaction: writeTx.asV2Write)
writeTx.addSyncCompletion {
NotificationCenter.default.postNotificationNameAsync(callServicePreferencesDidChange, object: nil)
}
}
static func highDataNetworkInterfaces(readTx: SDSAnyReadTransaction) -> NetworkInterfaceSet {
guard let highDataPreference = keyValueStore.getUInt(
highDataPreferenceKey,
transaction: readTx.asV2Read) else { return .wifiAndCellular }
return NetworkInterfaceSet(rawValue: highDataPreference)
}
}
extension CallService: IndividualCallObserver {
func individualCallStateDidChange(_ call: IndividualCall, state: CallState) {
AssertIsOnMainThread()
updateIsVideoEnabled()
configureDataMode()
}
func individualCallLocalVideoMuteDidChange(_ call: IndividualCall, isVideoMuted: Bool) {
AssertIsOnMainThread()
updateIsVideoEnabled()
}
}
extension CallService: GroupCallObserver {
@MainActor
func groupCallLocalDeviceStateChanged(_ call: GroupCall) {
let ringRtcCall = call.ringRtcCall
Logger.info("")
updateIsVideoEnabled()
configureDataMode()
switch call.concreteType {
case .groupThread(let call):
updateGroupMembersForCurrentCallIfNecessary()
if
ringRtcCall.localDeviceState.isJoined,
case .shouldRing = call.groupCallRingState,
call.ringRestrictions.isEmpty,
ringRtcCall.remoteDeviceStates.isEmpty
{
// Don't start ringing until we join the call successfully.
call.groupCallRingState = .ringing
ringRtcCall.ringAll()
audioService.playOutboundRing()
}
if ringRtcCall.localDeviceState.isJoined {
if let eraId = ringRtcCall.peekInfo?.eraId {
groupCallAccessoryMessageDelegate.localDeviceMaybeJoinedGroupCall(
eraId: eraId,
groupId: call.groupId,
groupCallRingState: call.groupCallRingState
)
}
} else {
groupCallAccessoryMessageDelegate.localDeviceMaybeLeftGroupCall(
groupId: call.groupId,
groupCall: ringRtcCall
)
}
case .callLink:
self.adHocCallStateObserver!.checkIfJoined()
}
}
func groupCallPeekChanged(_ call: GroupCall) {
AssertIsOnMainThread()
let ringRtcCall = call.ringRtcCall
guard let peekInfo = ringRtcCall.peekInfo else {
Logger.warn("No peek info for call: \(call)")
return
}
switch call.concreteType {
case .groupThread(let call):
let groupId = call.groupId
if
ringRtcCall.localDeviceState.isJoined,
let eraId = peekInfo.eraId
{
groupCallAccessoryMessageDelegate.localDeviceMaybeJoinedGroupCall(
eraId: eraId,
groupId: call.groupId,
groupCallRingState: call.groupCallRingState
)
}
databaseStorage.asyncWrite { tx in
self.groupCallManager.updateGroupCallModelsForPeek(
peekInfo: peekInfo,
groupId: groupId,
triggerEventTimestamp: MessageTimestampGenerator.sharedInstance.generateTimestamp(),
tx: tx
)
}
case .callLink:
self.adHocCallStateObserver!.checkIfActive()
self.adHocCallStateObserver!.checkIfJoined()
}
}
@MainActor
func groupCallEnded(_ groupCall: GroupCall, reason: GroupCallEndReason) {
groupCallAccessoryMessageDelegate.localDeviceGroupCallDidEnd()
let call = callServiceState.currentCall
switch call?.mode {
case nil, .individual:
owsFail("Can't receive callback without an active group call")
case .groupThread(let currentCall as GroupCall), .callLink(let currentCall as GroupCall):
owsPrecondition(currentCall === groupCall)
if currentCall.shouldTerminateOnEndEvent {
callServiceState.terminateCall(call!)
}
}
}
@MainActor
public func groupCallRemoteDeviceStatesChanged(_ call: GroupCall) {
switch call.concreteType {
case .groupThread(let call):
if
case .ringing = call.groupCallRingState,
!call.ringRtcCall.remoteDeviceStates.isEmpty
{
// The first time someone joins after a ring, we need to mark the call accepted.
// (But if we didn't ring, the call will have already been marked accepted.)
callUIAdapter.recipientAcceptedCall(.groupThread(call))
}
case .callLink:
break
}
}
}
extension CallService: GroupThreadCallDelegate {
func groupThreadCallRequestMembershipProof(_ call: GroupThreadCall) {
Logger.info("")
let groupCall = call.ringRtcCall
Task { [groupCallManager] in
let databaseStorage = SSKEnvironment.shared.databaseStorageRef
let groupThread = databaseStorage.read { tx in
return TSGroupThread.fetch(forGroupId: call.groupId, tx: tx)
}
guard let groupModel = groupThread?.groupModel as? TSGroupModelV2 else {
owsFailDebug("Missing v2 model for group call.")
return
}
do {
let proof = try await groupCallManager.groupCallPeekClient.fetchGroupMembershipProof(secretParams: try groupModel.secretParams())
await MainActor.run {
groupCall.updateMembershipProof(proof: proof)
}
} catch {
if error.isNetworkFailureOrTimeout {
Logger.warn("Failed to fetch group call credentials \(error)")
} else {
owsFailDebug("Failed to fetch group call credentials \(error)")
}
}
}
}
func groupThreadCallRequestGroupMembers(_ call: GroupThreadCall) {
Logger.info("")
updateGroupMembersForCurrentCallIfNecessary()
}
}
extension SignalCall {
func unpackGroupCall() -> GroupThreadCall? {
switch mode {
case .individual:
return nil
case .groupThread(let groupThreadCall):
return groupThreadCall
case .callLink:
return nil
}
}
}
private extension LocalDeviceState {
var isJoined: Bool {
switch joinState {
case .joined: return true
case .pending, .joining, .notJoined: return false
}
}
}
// MARK: - Group call participant updates
extension CallService: DatabaseChangeDelegate {
public func databaseChangesDidUpdate(databaseChanges: DatabaseChanges) {
owsAssertDebug(appReadiness.isAppReady)
switch callServiceState.currentCall?.mode {
case nil, .individual, .callLink:
break
case .groupThread(let call):
if databaseChanges.threadUniqueIds.contains(call.threadUniqueId) {
updateGroupMembersForCurrentCallIfNecessary()
}
}
}
public func databaseChangesDidUpdateExternally() {
owsAssertDebug(appReadiness.isAppReady)
updateGroupMembersForCurrentCallIfNecessary()
}
public func databaseChangesDidReset() {
owsAssertDebug(appReadiness.isAppReady)
updateGroupMembersForCurrentCallIfNecessary()
}
}
extension CallService: CallManagerDelegate {
public typealias CallManagerDelegateCallType = SignalCall
/**
* Send a generic call message to the given remote recipient.
* Invoked on the main thread, asynchronously.
* If there is any error, the UI can reset UI state and invoke the reset() API.
*/
public func callManager(
_ callManager: CallManager<SignalCall, CallService>,
shouldSendCallMessage recipientUuid: UUID,
message: Data,
urgency: CallMessageUrgency
) {
AssertIsOnMainThread()
Logger.info("")
let callAtStart = self.callServiceState.currentCall
Task {
let opaqueBuilder = SSKProtoCallMessageOpaque.builder()
opaqueBuilder.setData(message)
opaqueBuilder.setUrgency(urgency.protobufValue)
await self.sendCallMessage(
opaqueBuilder.buildInfallibly(),
to: Aci(fromUUID: recipientUuid),
callAtStart: callAtStart
)
}
}
@MainActor
private func sendCallMessage(
_ opaqueMessage: SSKProtoCallMessageOpaque,
to recipientAci: Aci,
callAtStart: SignalCall?
) async {
do {
let sendPromise = await databaseStorage.awaitableWrite { transaction in
let thread = TSContactThread.getOrCreateThread(
withContactAddress: SignalServiceAddress(recipientAci),
transaction: transaction
)
let callMessage = OWSOutgoingCallMessage(
thread: thread,
opaqueMessage: opaqueMessage,
overrideRecipients: nil,
transaction: transaction
)
let preparedMessage = PreparedOutgoingMessage.preprepared(
transientMessageWithoutAttachments: callMessage
)
return ThreadUtil.enqueueMessagePromise(
message: preparedMessage,
limitToCurrentProcessLifetime: true,
isHighPriority: true,
transaction: transaction
)
}
try await sendPromise.awaitable()
// TODO: Tell RingRTC we succeeded in sending the message. API TBD
} catch {
self.publishUntrustedIdentityErrorIfNeeded(error, callAtStart: callAtStart)
Logger.warn("Failed to send opaque message \(error)")
// TODO: Tell RingRTC something went wrong. API TBD
}
}
/**
* Send a generic call message to a group. Send to all members of the group
* or, if overrideRecipients is not empty, send to the given subset of members
* using multi-recipient sealed sender. If the sealed sender request fails,
* clients should provide a fallback mechanism.
* Invoked on the main thread, asynchronously.
* If there is any error, the UI can reset UI state and invoke the reset() API.
*/
public func callManager(
_ callManager: CallManager<SignalCall, CallService>,
shouldSendCallMessageToGroup groupId: Data,
message: Data,
urgency: CallMessageUrgency,
overrideRecipients: [UUID]
) {
Logger.info("")
let callAtStart = self.callServiceState.currentCall
Task {
let opaqueBuilder = SSKProtoCallMessageOpaque.builder()
opaqueBuilder.setData(message)
opaqueBuilder.setUrgency(urgency.protobufValue)
await self.sendCallMessageToGroup(
opaqueBuilder.buildInfallibly(),
groupId: groupId,
overrideRecipients: overrideRecipients,
callAtStart: callAtStart
)
}
}
@MainActor
private func sendCallMessageToGroup(
_ opaqueMessage: SSKProtoCallMessageOpaque,
groupId: Data,
overrideRecipients: [UUID],
callAtStart: SignalCall?
) async {
do {
let sendPromise = try await self.databaseStorage.awaitableWrite { transaction in
guard let thread = TSGroupThread.fetch(groupId: groupId, transaction: transaction) else {
throw OWSAssertionError("tried to send call message to unknown group")
}
let overrideRecipients = overrideRecipients.map {
return AciObjC(Aci(fromUUID: $0))
}
let callMessage = OWSOutgoingCallMessage(
thread: thread,
opaqueMessage: opaqueMessage,
overrideRecipients: overrideRecipients.isEmpty ? nil : overrideRecipients,
transaction: transaction
)
let preparedMessage = PreparedOutgoingMessage.preprepared(
transientMessageWithoutAttachments: callMessage
)
return ThreadUtil.enqueueMessagePromise(
message: preparedMessage,
limitToCurrentProcessLifetime: true,
isHighPriority: true,
transaction: transaction
)
}
try await sendPromise.awaitable()
// TODO: Tell RingRTC we succeeded in sending the message. API TBD
} catch {
self.publishUntrustedIdentityErrorIfNeeded(error, callAtStart: callAtStart)
Logger.warn("Failed to send opaque message \(error)")
// TODO: Tell RingRTC something went wrong. API TBD
}
}
@MainActor
private func publishUntrustedIdentityErrorIfNeeded(_ error: any Error, callAtStart: SignalCall?) {
guard error is UntrustedIdentityError else {
return
}
switch callAtStart?.mode {
case nil:
Logger.warn("The relevant call has already ended.")
case .individual:
owsFailDebug("This method isn't implemented for 1:1 calls.")
case .groupThread(let call as GroupCall), .callLink(let call as GroupCall):
call.handleUntrustedIdentityError()
}
}
public func callManager(
_ callManager: CallManager<SignalCall, CallService>,
shouldCompareCalls call1: SignalCall,
call2: SignalCall
) -> Bool {
Logger.info("")
guard case .individual(let call1) = call1.mode else {
owsFailDebug("Can't compare multi-participant calls.")
return false
}
guard case .individual(let call2) = call2.mode else {
owsFailDebug("Can't compare multi-participant calls.")
return false
}
return call1.thread.uniqueId == call2.thread.uniqueId
}
// MARK: - 1:1 Call Delegates
@MainActor
public func callManager(
_ callManager: CallManager<SignalCall, CallService>,
shouldStartCall call: SignalCall,
callId: UInt64,
isOutgoing: Bool,
callMediaType: CallMediaType
) {
guard callServiceState.currentCall == nil else {
handleFailedCall(failedCall: call, error: OWSGenericError("a current call is already set"))
return
}
switch call.mode {
case .individual(let individualCall) where isOutgoing:
individualCall.setOutgoingCallIdAndUpdateCallRecord(callId)
case .individual:
break
case .groupThread, .callLink:
owsFail("Can't start a group call using this method.")
}
// We grab this before updating the currentCall since it will unset it by default as a precaution.
let shouldEarlyRing = earlyRingNextIncomingCall.swap(false) && !isOutgoing
// The call to be started is provided by the event.
callServiceState.setCurrentCall(call)
individualCallService.callManager(
callManager,
shouldStartCall: call,
callId: callId,
isOutgoing: isOutgoing,
callMediaType: callMediaType,
shouldEarlyRing: shouldEarlyRing
)
}
@MainActor
public func callManager(
_ callManager: CallManager<SignalCall, CallService>,
onEvent call: SignalCall,
event: CallManagerEvent
) {
individualCallService.callManager(
callManager,
onEvent: call,
event: event
)
}
/**
* onNetworkRouteChangedFor will be invoked when changes to the network routing (e.g. wifi/cellular) are detected.
* Invoked on the main thread, asynchronously.
*/
@MainActor
public func callManager(
_ callManager: CallManager<SignalCall, CallService>,
onNetworkRouteChangedFor call: SignalCall,
networkRoute: NetworkRoute
) {
Logger.info("Network route changed for call: \(call): \(networkRoute.localAdapterType.rawValue)")
switch call.mode {
case .individual(let individualCall):
individualCall.networkRoute = networkRoute
configureDataMode()
case .groupThread, .callLink:
owsFail("Can't set the network route for a group call.")
}
}
public func callManager(
_ callManager: CallManager<SignalCall, CallService>,
onAudioLevelsFor call: SignalCall,
capturedLevel: UInt16,
receivedLevel: UInt16
) {
// TODO: Implement audio level handling for individual calls.
}
public func callManager(
_ callManager: CallManager<SignalCall, CallService>,
onLowBandwidthForVideoFor call: SignalCall,
recovered: Bool
) {
// TODO: Implement handling of the "low outgoing bandwidth for video" notification.
}
@MainActor
public func callManager(
_ callManager: CallManager<SignalCall, CallService>,
shouldSendOffer callId: UInt64,
call: SignalCall,
destinationDeviceId: UInt32?,
opaque: Data,
callMediaType: CallMediaType
) {
individualCallService.callManager(
callManager,
shouldSendOffer: callId,
call: call,
destinationDeviceId: destinationDeviceId,
opaque: opaque,
callMediaType: callMediaType
)
}
@MainActor
public func callManager(
_ callManager: CallManager<SignalCall, CallService>,
shouldSendAnswer callId: UInt64,
call: SignalCall,
destinationDeviceId: UInt32?,
opaque: Data
) {
individualCallService.callManager(
callManager,
shouldSendAnswer: callId,
call: call,
destinationDeviceId: destinationDeviceId,
opaque: opaque
)
}
@MainActor
public func callManager(
_ callManager: CallManager<SignalCall, CallService>,
shouldSendIceCandidates callId: UInt64,
call: SignalCall,
destinationDeviceId: UInt32?,
candidates: [Data]
) {
individualCallService.callManager(
callManager,
shouldSendIceCandidates: callId,
call: call,
destinationDeviceId: destinationDeviceId,
candidates: candidates
)
}
@MainActor
public func callManager(
_ callManager: CallManager<SignalCall, CallService>,
shouldSendHangup callId: UInt64,
call: SignalCall,
destinationDeviceId: UInt32?,
hangupType: HangupType,
deviceId: UInt32
) {
individualCallService.callManager(
callManager,
shouldSendHangup: callId,
call: call,
destinationDeviceId: destinationDeviceId,
hangupType: hangupType,
deviceId: deviceId
)
}
@MainActor
public func callManager(
_ callManager: CallManager<SignalCall, CallService>,
shouldSendBusy callId: UInt64,
call: SignalCall,
destinationDeviceId: UInt32?
) {
individualCallService.callManager(
callManager,
shouldSendBusy: callId,
call: call,
destinationDeviceId: destinationDeviceId
)
}
@MainActor
public func callManager(
_ callManager: CallManager<SignalCall, CallService>,
onUpdateLocalVideoSession call: SignalCall,
session: AVCaptureSession?
) {
individualCallService.callManager(
callManager,
onUpdateLocalVideoSession: call,
session: session
)
}
@MainActor
public func callManager(
_ callManager: CallManager<SignalCall, CallService>,
onAddRemoteVideoTrack call: SignalCall,
track: RTCVideoTrack
) {
individualCallService.callManager(
callManager,
onAddRemoteVideoTrack: call,
track: track
)
}
/**
* An update from `sender` has come in for the ring in `groupId` identified by `ringId`.
*
* `sender` will be the current user's ID if the update came from another device.
*
* Invoked on the main thread, asynchronously.
*/
@MainActor
public func callManager(
_ callManager: CallManager<SignalCall, CallService>,
didUpdateRingForGroup groupId: Data,
ringId: Int64,
sender: UUID,
update: RingUpdate
) {
let senderAci = Aci(fromUUID: sender)
/// Let our ``CallRecord`` delegate know we got a ring update.
databaseStorage.asyncWrite { tx in
self.groupCallRecordRingUpdateDelegate.didReceiveRingUpdate(
groupId: groupId,
ringId: ringId,
ringUpdate: update,
ringUpdateSender: senderAci,
tx: tx.asV2Write
)
}
guard update == .requested else {
if
let currentCall = self.callServiceState.currentCall,
case .groupThread(let groupThreadCall) = currentCall.mode,
case .incomingRing(_, ringId) = groupThreadCall.groupCallRingState
{
switch update {
case .requested:
owsFail("checked above")
case .expiredRing:
self.callUIAdapter.remoteDidHangupCall(currentCall)
case .acceptedOnAnotherDevice:
self.callUIAdapter.didAnswerElsewhere(call: currentCall)
case .declinedOnAnotherDevice:
self.callUIAdapter.didDeclineElsewhere(call: currentCall)
case .busyLocally:
owsFailDebug("shouldn't get reported here")
fallthrough
case .busyOnAnotherDevice:
self.callUIAdapter.wasBusyElsewhere(call: currentCall)
case .cancelledByRinger:
self.callUIAdapter.remoteDidHangupCall(currentCall)
}
self.leaveAndTerminateGroupCall(currentCall, groupCall: groupThreadCall)
groupThreadCall.groupCallRingState = .incomingRingCancelled
}
databaseStorage.asyncWrite { transaction in
do {
try CancelledGroupRing(id: ringId).insert(transaction.unwrapGrdbWrite.database)
try CancelledGroupRing.deleteExpired(expiration: Date().addingTimeInterval(-30 * kMinuteInterval),
transaction: transaction)
} catch {
owsFailDebug("failed to update cancellation table: \(error)")
}
}
return
}
enum RingAction {
case cancel
case ring(GroupIdentifier)
}
let action: RingAction = databaseStorage.read { transaction in
guard let groupId = try? GroupIdentifier(contents: [UInt8](groupId)) else {
owsFailDebug("discarding group ring \(ringId) from \(senderAci) for invalid group")
return .cancel
}
guard let thread = TSGroupThread.fetch(forGroupId: groupId, tx: transaction) else {
owsFailDebug("discarding group ring \(ringId) from \(senderAci) for unknown group")
return .cancel
}
guard GroupsV2MessageProcessor.discardMode(
forMessageFrom: senderAci,
groupId: groupId.serialize().asData,
tx: transaction
) == .doNotDiscard else {
Logger.warn("discarding group ring \(ringId) from \(senderAci)")
return .cancel
}
guard thread.groupMembership.fullMembers.count <= RemoteConfig.current.maxGroupCallRingSize else {
Logger.warn("discarding group ring \(ringId) from \(senderAci) for too-large group")
return .cancel
}
do {
if try CancelledGroupRing.exists(transaction.unwrapGrdbRead.database, key: ringId) {
return .cancel
}
} catch {
owsFailDebug("unable to check cancellation table: \(error)")
}
return .ring(groupId)
}
switch action {
case .cancel:
do {
try callManager.cancelGroupRing(groupId: groupId, ringId: ringId, reason: nil)
} catch {
owsFailDebug("RingRTC failed to cancel group ring \(ringId): \(error)")
}
case .ring(let groupId):
let currentCall = self.callServiceState.currentCall
if case .groupThread(let call) = currentCall?.mode, call.groupId.serialize() == groupId.serialize() {
// We're already ringing or connected, or at the very least already in the lobby.
return
}
guard currentCall == nil else {
do {
try callManager.cancelGroupRing(groupId: groupId.serialize().asData, ringId: ringId, reason: .busy)
} catch {
owsFailDebug("RingRTC failed to cancel group ring \(ringId): \(error)")
}
return
}
// Mute video by default unless the user has already approved it.
// This keeps us from popping the "give permission to use your camera" alert before the user answers.
let videoMuted = AVCaptureDevice.authorizationStatus(for: .video) != .authorized
guard let (call, groupThreadCall) = buildAndConnectGroupCall(
for: groupId,
isVideoMuted: videoMuted
) else {
return owsFailDebug("Failed to build group call")
}
groupThreadCall.groupCallRingState = .incomingRing(caller: SignalServiceAddress(senderAci), ringId: ringId)
self.callUIAdapter.reportIncomingCall(call)
}
}
}
extension CallMessageUrgency {
var protobufValue: SSKProtoCallMessageOpaqueUrgency {
switch self {
case .droppable: return .droppable
case .handleImmediately: return .handleImmediately
}
}
}
extension NetworkInterfaceSet {
func includes(_ ringRtcAdapter: NetworkAdapterType) -> Bool? {
switch ringRtcAdapter {
case .unknown, .vpn, .anyAddress:
if self.isEmpty {
return false
} else if self.inverted.isEmpty {
return true
} else {
// We don't know the underlying interface, so we can't assume anything.
return nil
}
case .cellular, .cellular2G, .cellular3G, .cellular4G, .cellular5G:
return self.contains(.cellular)
case .ethernet, .wifi, .loopback:
return self.contains(.wifi)
}
}
}