session-ios/Session/Calls/CallService.swift

1171 lines
42 KiB
Swift

//
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
//
import Foundation
import SignalRingRTC
import PromiseKit
import SessionUtilitiesKit
// All Observer methods will be invoked from the main thread.
@objc(OWSCallServiceObserver)
protocol CallServiceObserver: AnyObject {
/**
* Fired whenever the call changes.
*/
func didUpdateCall(from oldValue: SignalCall?, to newValue: SignalCall?)
}
@objc
public final class CallService: NSObject {
public typealias CallManagerType = CallManager<SignalCall, CallService>
public let callManager = CallManagerType()
@objc
public let individualCallService = IndividualCallService()
let groupCallMessageHandler = GroupCallUpdateMessageHandler()
let groupCallRemoteVideoManager = GroupCallRemoteVideoManager()
lazy private(set) var audioService = CallAudioService()
private var _currentCall: SignalCall?
@objc
public private(set) var currentCall: SignalCall? {
set {
AssertIsOnMainThread()
let oldValue = _currentCall
_currentCall = newValue
oldValue?.removeObserver(self)
newValue?.addObserverAndSyncState(observer: self)
updateIsVideoEnabled()
// Prevent device from sleeping while we have an active call.
if oldValue != newValue {
if let oldValue = oldValue {
DeviceSleepManager.sharedInstance.removeBlock(blockObject: oldValue)
}
if let newValue = newValue {
assert(calls.contains(newValue))
DeviceSleepManager.sharedInstance.addBlock(blockObject: newValue)
if newValue.isIndividualCall { individualCallService.startCallTimer() }
} else {
individualCallService.stopAnyCallTimer()
}
}
Logger.debug("\(oldValue as Optional) -> \(newValue as Optional)")
for observer in observers.elements {
observer.didUpdateCall(from: oldValue, to: newValue)
}
}
get {
AssertIsOnMainThread()
return _currentCall
}
}
/// True whenever CallService has any call in progress.
/// The call may not yet be visible to the user if we are still in the middle of signaling.
@objc
public var hasCallInProgress: Bool {
calls.count > 0
}
/// Track all calls that are currently "in play". Usually this is 1 or 0, but when dealing
/// with a rapid succession of calls, it's possible to have multiple.
///
/// For example, if the client receives two call offers, we hand them both off to RingRTC,
/// which will let us know which one, if any, should become the "current call". But in the
/// meanwhile, we still want to track that calls are in-play so we can prevent the user from
/// placing an outgoing call.
private let _calls = AtomicValue<Set<SignalCall>>(Set())
private var calls: Set<SignalCall> {
get {
_calls.get()
}
}
private func addCall(_ call: SignalCall) {
var calls = _calls.get()
calls.insert(call)
_calls.set(calls)
}
private func removeCall(_ call: SignalCall) -> Bool {
var calls = _calls.get()
let result = calls.remove(call)
_calls.set(calls)
return result != nil
}
public override init() {
super.init()
SwiftSingletons.register(self)
callManager.delegate = self
addObserverAndSyncState(observer: groupCallMessageHandler)
addObserverAndSyncState(observer: groupCallRemoteVideoManager)
NotificationCenter.default.addObserver(
self,
selector: #selector(didEnterBackground),
name: .OWSApplicationDidEnterBackground,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(didBecomeActive),
name: .OWSApplicationDidBecomeActive,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(configureBandwidthMode),
name: Self.callServicePreferencesDidChange,
object: nil)
// Note that we're not using the usual .owsReachabilityChanged
// We want to update our bandwidth mode if the app has been backgrounded
NotificationCenter.default.addObserver(
self,
selector: #selector(configureBandwidthMode),
name: .reachabilityChanged,
object: nil)
AppReadiness.runNowOrWhenAppDidBecomeReadyAsync {
SDSDatabaseStorage.shared.appendDatabaseChangeDelegate(self)
}
}
deinit {
NotificationCenter.default.removeObserver(self)
}
// MARK: - Observers
private var observers = WeakArray<CallServiceObserver>()
@objc
func addObserverAndSyncState(observer: CallServiceObserver) {
addObserver(observer: observer, syncStateImmediately: true)
}
@objc
func addObserver(observer: CallServiceObserver, syncStateImmediately: Bool) {
AssertIsOnMainThread()
observers.append(observer)
if syncStateImmediately {
// Synchronize observer with current call state
observer.didUpdateCall(from: nil, to: currentCall)
}
}
// The observer-related methods should be invoked on the main thread.
@objc
func removeObserver(_ observer: CallServiceObserver) {
AssertIsOnMainThread()
observers.removeAll { $0 === observer }
}
// The observer-related methods should be invoked on the main thread.
func removeAllObservers() {
AssertIsOnMainThread()
observers = []
}
// MARK: -
/**
* Local user toggled to mute audio.
*/
func updateIsLocalAudioMuted(isLocalAudioMuted: Bool) {
AssertIsOnMainThread()
// Keep a reference to the call before permissions were requested...
guard let call = 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: call, 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 =
UIApplication.shared.findFrontmostViewController(ignoringAlerts: true,
window: OWSWindowManager.shared.callViewWindow) 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.currentCall === call else {
Logger.info("ignoring microphone permissions for obsolete call")
return
}
guard granted else {
return frontmostViewController.ows_showNoMicrophonePermissionActionSheet()
}
// Success callback; microphone permissions are granted.
self.updateIsLocalAudioMutedWithMicrophonePermission(call: call, isLocalAudioMuted: isLocalAudioMuted)
}
}
private func updateIsLocalAudioMutedWithMicrophonePermission(call: SignalCall, isLocalAudioMuted: Bool) {
AssertIsOnMainThread()
guard call === self.currentCall else {
cleanupStaleCall(call)
return
}
switch call.mode {
case .group(let groupCall):
groupCall.isOutgoingAudioMuted = isLocalAudioMuted
call.groupCall(onLocalDeviceStateChanged: groupCall)
case .individual(let individualCall):
individualCall.isMuted = isLocalAudioMuted
individualCallService.ensureAudioState(call: call)
}
}
/**
* Local user toggled video.
*/
func updateIsLocalVideoMuted(isLocalVideoMuted: Bool) {
AssertIsOnMainThread()
// Keep a reference to the call before permissions were requested...
guard let call = 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: call, 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.
guard let frontmostViewController =
UIApplication.shared.findFrontmostViewController(ignoringAlerts: true,
window: OWSWindowManager.shared.callViewWindow) 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.currentCall === call else {
Logger.info("ignoring camera permissions for obsolete call")
return
}
if granted {
// Success callback; camera permissions are granted.
self.updateIsLocalVideoMutedWithCameraPermissions(call: call, isLocalVideoMuted: isLocalVideoMuted)
}
}
}
private func updateIsLocalVideoMutedWithCameraPermissions(call: SignalCall, isLocalVideoMuted: Bool) {
AssertIsOnMainThread()
guard call === self.currentCall else {
cleanupStaleCall(call)
return
}
switch call.mode {
case .group(let groupCall):
groupCall.isOutgoingVideoMuted = isLocalVideoMuted
call.groupCall(onLocalDeviceStateChanged: groupCall)
case .individual(let individualCall):
individualCall.hasLocalVideo = !isLocalVideoMuted
}
updateIsVideoEnabled()
}
func updateCameraSource(call: SignalCall, isUsingFrontCamera: Bool) {
AssertIsOnMainThread()
call.videoCaptureController.switchCamera(isUsingFrontCamera: isUsingFrontCamera)
}
func cleanupStaleCall(_ staleCall: SignalCall, function: StaticString = #function, line: UInt = #line) {
assert(staleCall !== currentCall)
if let currentCall = currentCall {
let error = OWSAssertionError("trying \(function):\(line) for call: \(staleCall) which is not currentCall: \(currentCall as Optional)")
handleFailedCall(failedCall: staleCall, error: error)
} else {
Logger.info("ignoring \(function):\(line) for call: \(staleCall) since currentCall has ended.")
}
}
@objc
func configureBandwidthMode() {
guard AppReadiness.isAppReady else { return }
guard let currentCall = currentCall else { return }
let useLowBandwidth = Self.useLowBandwidthWithSneakyTransaction()
Logger.info("Configuring call for \(useLowBandwidth ? "low" : "standard") bandwidth")
switch currentCall.mode {
case let .group(call):
call.updateBandwidthMode(bandwidthMode: useLowBandwidth ? .low : .normal)
case let .individual(call) where call.state == .connected:
callManager.udpateBandwidthMode(bandwidthMode: useLowBandwidth ? .low : .normal)
default:
// Do nothing. We'll reapply the bandwidth mode once connected
break
}
}
static func useLowBandwidthWithSneakyTransaction() -> Bool {
let highBandwidthInterfaces = databaseStorage.read { readTx in
Self.highBandwidthNetworkInterfaces(readTx: readTx)
}
return !Self.reachabilityManager.isReachable(with: highBandwidthInterfaces)
}
// 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.
public func handleFailedCall(failedCall: SignalCall, error: Error) {
AssertIsOnMainThread()
Logger.debug("")
let callError: SignalCall.CallError = {
switch error {
case let callError as SignalCall.CallError:
return callError
default:
return SignalCall.CallError.externalError(underlyingError: error)
}
}()
failedCall.error = callError
if failedCall.isIndividualCall {
individualCallService.handleFailedCall(failedCall: failedCall, error: callError)
}
}
/**
* Clean up any existing call state and get ready to receive a new call.
*/
func terminate(call: SignalCall) {
AssertIsOnMainThread()
Logger.info("call: \(call as Optional)")
// If call is for the current call, clear it out first.
if call === currentCall { currentCall = nil }
if !removeCall(call) {
owsFailDebug("unknown call: \(call)")
}
if !hasCallInProgress {
audioSession.isRTCAudioEnabled = false
}
audioSession.endAudioActivity(call.audioActivity)
switch call.mode {
case .individual:
break
case .group(let groupCall):
groupCall.leave()
groupCall.disconnect()
// Kick off a peek now that we've disconnected to get an updated participant state.
if let thread = call.thread as? TSGroupThread {
peekCallAndUpdateThread(thread)
} else {
owsFailDebug("Invalid thread type")
}
}
// Apparently WebRTC will sometimes disable device orientation notifications.
// After every call ends, we need to ensure they are enabled.
UIDevice.current.beginGeneratingDeviceOrientationNotifications()
}
// MARK: - Video
var shouldHaveLocalVideoTrack: Bool {
AssertIsOnMainThread()
guard let call = self.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 .group(let groupCall):
return !groupCall.isOutgoingVideoMuted
}
}
func updateIsVideoEnabled() {
AssertIsOnMainThread()
guard let call = self.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 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.
if !Platform.isSimulator && individualCall.hasLocalVideo {
call.videoCaptureController.startCapture()
} else {
call.videoCaptureController.stopCapture()
}
}
case .group(let groupCall):
if !Platform.isSimulator && !groupCall.isOutgoingVideoMuted {
call.videoCaptureController.startCapture()
} else {
call.videoCaptureController.stopCapture()
}
}
}
// MARK: -
func buildAndConnectGroupCallIfPossible(thread: TSGroupThread) -> SignalCall? {
AssertIsOnMainThread()
guard !hasCallInProgress else { return nil }
guard let call = SignalCall.groupCall(thread: thread) else { return nil }
addCall(call)
currentCall = call
call.groupCall.isOutgoingAudioMuted = false
call.groupCall.isOutgoingVideoMuted = false
call.groupCall.connect()
return call
}
func joinGroupCallIfNecessary(_ call: SignalCall) {
owsAssertDebug(call.isGroupCall)
guard currentCall == nil || currentCall == call else {
return owsFailDebug("A call is already in progress")
}
// The joined/joining call must always be the current call.
currentCall = call
// If we're not yet connected, connect now. This may happen if, for
// example, the call ended unexpectedly.
if call.groupCall.localDeviceState.connectionState == .notConnected { call.groupCall.connect() }
// 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.
if call.groupCall.localDeviceState.joinState == .notJoined { call.groupCall.join() }
}
func buildOutgoingIndividualCallIfPossible(address: SignalServiceAddress, hasVideo: Bool) -> SignalCall? {
AssertIsOnMainThread()
guard !hasCallInProgress else { return nil }
let call = SignalCall.outgoingIndividualCall(localId: UUID(), remoteAddress: address)
call.individualCall.offerMediaType = hasVideo ? .video : .audio
addCall(call)
return call
}
func prepareIncomingIndividualCall(
thread: TSContactThread,
sentAtTimestamp: UInt64,
callType: SSKProtoCallMessageOfferType
) -> SignalCall {
AssertIsOnMainThread()
let offerMediaType: TSRecentCallOfferType
switch callType {
case .offerAudioCall:
offerMediaType = .audio
case .offerVideoCall:
offerMediaType = .video
}
let newCall = SignalCall.incomingIndividualCall(
localId: UUID(),
remoteAddress: thread.contactAddress,
sentAtTimestamp: sentAtTimestamp,
offerMediaType: offerMediaType
)
addCall(newCall)
return newCall
}
// MARK: - Notifications
@objc func didEnterBackground() {
AssertIsOnMainThread()
self.updateIsVideoEnabled()
}
@objc func didBecomeActive() {
AssertIsOnMainThread()
self.updateIsVideoEnabled()
}
// MARK: -
private func updateGroupMembersForCurrentCallIfNecessary() {
DispatchQueue.main.async {
guard let call = self.currentCall, call.isGroupCall,
let groupThread = call.thread as? TSGroupThread,
let memberInfo = self.groupMemberInfo(for: groupThread) else { return }
call.groupCall.updateGroupMembers(members: memberInfo)
}
}
private func groupMemberInfo(for thread: TSGroupThread) -> [GroupMemberInfo]? {
AssertIsOnMainThread()
// Make sure we're working with the latest group state.
databaseStorage.read { thread.anyReload(transaction: $0) }
guard let groupModel = thread.groupModel as? TSGroupModelV2,
let groupV2Params = try? groupModel.groupV2Params() else {
owsFailDebug("Unexpected group thread.")
return nil
}
return thread.groupMembership.fullMembers.compactMap {
guard let uuid = $0.uuid else {
owsFailDebug("Skipping group member, missing uuid")
return nil
}
guard let uuidCipherText = try? groupV2Params.userId(forUuid: uuid) else {
owsFailDebug("Skipping group member, missing uuidCipherText")
return nil
}
return GroupMemberInfo(userId: uuid, userIdCipherText: uuidCipherText)
}
}
private func fetchGroupMembershipProof(for thread: TSGroupThread) -> Promise<Data> {
guard let groupModel = thread.groupModel as? TSGroupModelV2 else {
owsFailDebug("unexpectedly missing group model")
return Promise(error: OWSAssertionError("Invalid group"))
}
return firstly {
try groupsV2Impl.fetchGroupExternalCredentials(groupModel: groupModel)
}.map(on: .main) { (credential) -> Data in
guard let tokenData = credential.token?.data(using: .utf8) else {
throw OWSAssertionError("Invalid credential")
}
return tokenData
}
}
// MARK: - Bandwidth
static let callServicePreferencesDidChange = Notification.Name("CallServicePreferencesDidChange")
private static let keyValueStore = SDSKeyValueStore(collection: "CallService")
private static let highBandwidthPreferenceKey = "HighBandwidthPreferenceKey"
static func setHighBandwidthInterfaces(_ interfaceSet: NetworkInterfaceSet, writeTx: SDSAnyWriteTransaction) {
Logger.info("Updating preferred low bandwidth interfaces: \(interfaceSet.rawValue)")
keyValueStore.setUInt(interfaceSet.rawValue, key: highBandwidthPreferenceKey, transaction: writeTx)
writeTx.addSyncCompletion {
NotificationCenter.default.postNotificationNameAsync(callServicePreferencesDidChange, object: nil)
}
}
static func highBandwidthNetworkInterfaces(readTx: SDSAnyReadTransaction) -> NetworkInterfaceSet {
guard let highBandwidthPreference = keyValueStore.getUInt(
highBandwidthPreferenceKey,
transaction: readTx) else { return .wifiAndCellular }
return NetworkInterfaceSet(rawValue: highBandwidthPreference)
}
}
extension CallService: CallObserver {
public func individualCallStateDidChange(_ call: SignalCall, state: CallState) {
AssertIsOnMainThread()
updateIsVideoEnabled()
configureBandwidthMode()
}
public func individualCallLocalVideoMuteDidChange(_ call: SignalCall, isVideoMuted: Bool) {
AssertIsOnMainThread()
updateIsVideoEnabled()
}
public func groupCallLocalDeviceStateChanged(_ call: SignalCall) {
owsAssertDebug(call.isGroupCall)
Logger.info("groupCallLocalDeviceStateChanged")
AssertIsOnMainThread()
updateIsVideoEnabled()
updateGroupMembersForCurrentCallIfNecessary()
configureBandwidthMode()
}
public func groupCallRemoteDeviceStatesChanged(_ call: SignalCall) {}
public func groupCallPeekChanged(_ call: SignalCall) {
guard let thread = call.thread as? TSGroupThread else {
owsFailDebug("Invalid thread for call: \(call)")
return
}
guard let peekInfo = call.groupCall.peekInfo else {
Logger.warn("No peek info for call: \(call)")
return
}
updateGroupCallMessageWithInfo(peekInfo, for: thread, timestamp: Date.ows_millisecondTimestamp())
}
public func groupCallRequestMembershipProof(_ call: SignalCall) {
owsAssertDebug(call.isGroupCall)
Logger.info("groupCallUpdateGroupMembershipProof")
guard call === currentCall else { return cleanupStaleCall(call) }
guard let groupThread = call.thread as? TSGroupThread else {
return owsFailDebug("unexpectedly missing thread")
}
firstly {
self.fetchGroupMembershipProof(for: groupThread)
}.done(on: .main) { proof in
call.groupCall.updateMembershipProof(proof: proof)
}.catch(on: .main) { error in
if error.isNetworkFailureOrTimeout {
Logger.warn("Failed to fetch group call credentials \(error)")
} else {
owsFailDebug("Failed to fetch group call credentials \(error)")
}
}
}
public func groupCallRequestGroupMembers(_ call: SignalCall) {
owsAssertDebug(call.isGroupCall)
Logger.info("groupCallUpdateGroupMembers")
guard call === currentCall else { return cleanupStaleCall(call) }
updateGroupMembersForCurrentCallIfNecessary()
}
}
// MARK: - Group call participant updates
extension CallService {
@objc @available(swift, obsoleted: 1.0)
func peekCallAndUpdateThread(_ thread: TSGroupThread) {
AssertIsOnMainThread()
self.peekCallAndUpdateThread(thread)
}
@objc
func peekCallAndUpdateThread(_ thread: TSGroupThread, expectedEraId: String? = nil, triggerEventTimestamp: UInt64 = NSDate.ows_millisecondTimeStamp()) {
AssertIsOnMainThread()
guard RemoteConfig.groupCalling, thread.isLocalUserFullMember else { return }
// If the currentCall is for the provided thread, we don't need to perform an explict
// peek. Connected calls will receive automatic updates from RingRTC
guard currentCall?.thread != thread else {
Logger.info("Ignoring peek request for the current call")
return
}
guard let memberInfo = self.groupMemberInfo(for: thread) else {
owsFailDebug("Failed to fetch group member info to peek \(thread.uniqueId)")
return
}
firstly(on: .global()) {
if let expectedEraId = expectedEraId {
// If we're expecting a call with `expectedEraId`, prepopulate an entry in the database.
// If it's the current call, we'll update with the PeekInfo once fetched
// Otherwise, it'll be marked as ended as soon as we complete the fetch
// If we fail to fetch, the entry will be kept around until the next PeekInfo fetch completes.
self.insertPlaceholderGroupCallMessageIfNecessary(eraId: expectedEraId,
timestamp: triggerEventTimestamp,
thread: thread)
}
firstly {
self.fetchGroupMembershipProof(for: thread)
}.then(on: .main) { (proof: Data) -> Promise<PeekInfo> in
let sfuURL = DebugFlags.callingUseTestSFU.get() ? TSConstants.sfuTestURL : TSConstants.sfuURL
return self.callManager.peekGroupCall(sfuUrl: sfuURL, membershipProof: proof, groupMembers: memberInfo)
}.done(on: .main) { info in
// If we're expecting an eraId, the timestamp is only valid for PeekInfo with the same eraId.
// We may have a more appropriate timestamp waiting in the message processing queue.
Logger.info("Fetched group call PeekInfo for thread: \(thread.uniqueId) eraId: \(info.eraId ?? "(null)")")
if expectedEraId == nil || info.eraId == nil || expectedEraId == info.eraId {
self.updateGroupCallMessageWithInfo(info, for: thread, timestamp: triggerEventTimestamp)
}
}.catch(on: .global()) { error in
if error.isNetworkFailureOrTimeout {
Logger.warn("Failed to fetch PeekInfo for \(thread.uniqueId): \(error)")
} else {
owsFailDebug("Failed to fetch PeekInfo for \(thread.uniqueId): \(error)")
}
}
}.catch(on: .global()) { error in
owsFailDebug("Failed to fetch PeekInfo for \(thread.uniqueId): \(error)")
}
}
fileprivate func updateGroupCallMessageWithInfo(_ info: PeekInfo, for thread: TSGroupThread, timestamp: UInt64) {
databaseStorage.write { writeTx in
let results = GRDBInteractionFinder.unendedCallsForGroupThread(thread, transaction: writeTx)
// Update everything that doesn't match the current call era to mark as ended
results
.filter { $0.eraId != info.eraId }
.forEach { toExpire in
toExpire.update(withHasEnded: true, transaction: writeTx)
}
// Update the message for the current era if it exists, or insert a new one.
guard let currentEraId = info.eraId, let creatorUuid = info.creator else {
Logger.info("No active call")
return
}
let currentEraMessages = results.filter { $0.eraId == currentEraId }
owsAssertDebug(currentEraMessages.count <= 1)
if let currentMessage = currentEraMessages.first {
let wasOldMessageEmpty = currentMessage.joinedMemberUuids?.count == 0 && !currentMessage.hasEnded
currentMessage.update(
withJoinedMemberUuids: info.joinedMembers,
creatorUuid: creatorUuid,
transaction: writeTx)
// Only notify if the message we updated had no participants
if wasOldMessageEmpty {
self.postUserNotificationIfNecessary(message: currentMessage, transaction: writeTx)
}
} else if !info.joinedMembers.isEmpty {
let newMessage = OWSGroupCallMessage(
eraId: currentEraId,
joinedMemberUuids: info.joinedMembers,
creatorUuid: creatorUuid,
thread: thread,
sentAtTimestamp: timestamp)
newMessage.anyInsert(transaction: writeTx)
self.postUserNotificationIfNecessary(message: newMessage, transaction: writeTx)
}
}
}
fileprivate func insertPlaceholderGroupCallMessageIfNecessary(eraId: String, timestamp: UInt64, thread: TSGroupThread) {
databaseStorage.write { writeTx in
guard !GRDBInteractionFinder.existsGroupCallMessageForEraId(eraId, thread: thread, transaction: writeTx) else { return }
Logger.info("Inserting placeholder group call message with eraId: \(eraId)")
let message = OWSGroupCallMessage(eraId: eraId, joinedMemberUuids: [], creatorUuid: nil, thread: thread, sentAtTimestamp: timestamp)
message.anyInsert(transaction: writeTx)
}
}
fileprivate func postUserNotificationIfNecessary(message: OWSGroupCallMessage, transaction: SDSAnyWriteTransaction) {
// The message can't be for the current call
guard self.currentCall?.thread.uniqueId != message.uniqueThreadId else { return }
// The creator of the call must be known, and it can't be the local user
guard let creator = message.creatorUuid, !SignalServiceAddress(uuidString: creator).isLocalAddress else { return }
// The message must have at least one participant
guard (message.joinedMemberUuids?.count ?? 0) > 0 else { return }
guard let thread = TSGroupThread.anyFetch(uniqueId: message.uniqueThreadId, transaction: transaction) else {
owsFailDebug("Unknown thread")
return
}
Self.notificationPresenter.notifyUser(for: message, thread: thread, wantsSound: true, transaction: transaction)
}
}
extension CallService: DatabaseChangeDelegate {
public func databaseChangesWillUpdate() {}
public func databaseChangesDidUpdate(databaseChanges: DatabaseChanges) {
AssertIsOnMainThread()
owsAssertDebug(AppReadiness.isAppReady)
guard let thread = currentCall?.thread,
thread.isGroupThread,
databaseChanges.didUpdate(thread: thread) else { return }
updateGroupMembersForCurrentCallIfNecessary()
}
public func databaseChangesDidUpdateExternally() {
AssertIsOnMainThread()
owsAssertDebug(AppReadiness.isAppReady)
updateGroupMembersForCurrentCallIfNecessary()
}
public func databaseChangesDidReset() {
AssertIsOnMainThread()
owsAssertDebug(AppReadiness.isAppReady)
updateGroupMembersForCurrentCallIfNecessary()
}
}
extension CallService: CallManagerDelegate {
public typealias CallManagerDelegateCallType = SignalCall
/**
* A call message should be sent to the given remote recipient.
* Invoked on the main thread, asychronously.
* 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
) {
AssertIsOnMainThread()
Logger.info("shouldSendCallMessage")
// It's unlikely that this would ever have more than one call. But technically
// we don't know which call this message is on behalf of. So we assume it's every
// call with a participant with recipientUuid
let relevantCalls = calls.filter { (call: SignalCall) -> Bool in
call.participantAddresses
.compactMap { $0.uuid }
.contains(recipientUuid)
}
databaseStorage.write(.promise) { transaction in
TSContactThread.getOrCreateThread(
withContactAddress: SignalServiceAddress(uuid: recipientUuid),
transaction: transaction
)
}.then { thread throws -> Promise<Void> in
let opaqueBuilder = SSKProtoCallMessageOpaque.builder()
opaqueBuilder.setData(message)
let callMessage = OWSOutgoingCallMessage(
thread: thread,
opaqueMessage: try opaqueBuilder.build()
)
return self.messageSender.sendMessage(.promise, callMessage.asPreparer)
}.done { _ in
// TODO: Tell RingRTC we succeeded in sending the message. API TBD
}.catch { error in
if error.isNetworkFailureOrTimeout {
Logger.warn("Failed to send opaque message \(error)")
} else if error.isUntrustedIdentityError {
relevantCalls.forEach { $0.publishSendFailureUntrustedParticipantIdentity() }
} else {
Logger.error("Failed to send opaque message \(error)")
}
// TODO: Tell RingRTC something went wrong. API TBD
}
}
/**
* A HTTP request should be sent to the given url.
* Invoked on the main thread, asychronously.
* The result of the call should be indicated by calling the receivedHttpResponse() function.
*/
public func callManager(
_ callManager: CallManager<SignalCall, CallService>,
shouldSendHttpRequest requestId: UInt32,
url: String,
method: CallManagerHttpMethod,
headers: [String: String],
body: Data?
) {
AssertIsOnMainThread()
Logger.info("shouldSendHttpRequest")
let httpMethod: HTTPMethod
switch method {
case .get: httpMethod = .get
case .post: httpMethod = .post
case .put: httpMethod = .put
case .delete: httpMethod = .delete
}
let session = OWSURLSession(
securityPolicy: OWSURLSession.signalServiceSecurityPolicy,
configuration: OWSURLSession.defaultConfigurationWithoutCaching
)
session.require2xxOr3xx = false
session.allowRedirects = true
session.customRedirectHandler = { request in
var request = request
if let authHeader = headers.first(where: {
$0.key.caseInsensitiveCompare("Authorization") == .orderedSame
}) {
request.addValue(authHeader.value, forHTTPHeaderField: authHeader.key)
}
return request
}
firstly(on: .sharedUtility) {
session.dataTaskPromise(url, method: httpMethod, headers: headers, body: body)
}.done(on: .main) { response in
self.callManager.receivedHttpResponse(
requestId: requestId,
statusCode: UInt16(response.statusCode),
body: response.responseData
)
}.catch(on: .main) { error in
if error.isNetworkFailureOrTimeout {
Logger.warn("Call manager http request failed \(error)")
} else {
owsFailDebug("Call manager http request failed \(error)")
}
self.callManager.httpRequestFailed(requestId: requestId)
}
}
public func callManager(
_ callManager: CallManager<SignalCall, CallService>,
shouldCompareCalls call1: SignalCall,
call2: SignalCall
) -> Bool {
Logger.info("shouldCompareCalls")
return call1.thread.uniqueId == call2.thread.uniqueId
}
// MARK: - 1:1 Call Delegates
public func callManager(
_ callManager: CallManager<SignalCall, CallService>,
shouldStartCall call: SignalCall,
callId: UInt64,
isOutgoing: Bool,
callMediaType: CallMediaType
) {
AssertIsOnMainThread()
owsAssertDebug(call.isIndividualCall)
guard currentCall == nil else {
handleFailedCall(failedCall: call, error: OWSAssertionError("a current call is already set"))
return
}
if !calls.contains(call) {
owsFailDebug("unknown call: \(call)")
}
call.individualCall.callId = callId
// The call to be started is provided by the event.
currentCall = call
individualCallService.callManager(
callManager,
shouldStartCall: call,
callId: callId,
isOutgoing: isOutgoing,
callMediaType: callMediaType
)
}
public func callManager(
_ callManager: CallManager<SignalCall, CallService>,
onEvent call: SignalCall,
event: CallManagerEvent
) {
AssertIsOnMainThread()
owsAssertDebug(call.isIndividualCall)
individualCallService.callManager(
callManager,
onEvent: call,
event: event
)
}
public func callManager(
_ callManager: CallManager<SignalCall, CallService>,
shouldSendOffer callId: UInt64,
call: SignalCall,
destinationDeviceId: UInt32?,
opaque: Data,
callMediaType: CallMediaType
) {
AssertIsOnMainThread()
owsAssertDebug(call.isIndividualCall)
individualCallService.callManager(
callManager,
shouldSendOffer: callId,
call: call,
destinationDeviceId: destinationDeviceId,
opaque: opaque,
callMediaType: callMediaType
)
}
public func callManager(
_ callManager: CallManager<SignalCall, CallService>,
shouldSendAnswer callId: UInt64,
call: SignalCall,
destinationDeviceId: UInt32?,
opaque: Data
) {
AssertIsOnMainThread()
owsAssertDebug(call.isIndividualCall)
individualCallService.callManager(
callManager,
shouldSendAnswer: callId,
call: call,
destinationDeviceId: destinationDeviceId,
opaque: opaque
)
}
public func callManager(
_ callManager: CallManager<SignalCall, CallService>,
shouldSendIceCandidates callId: UInt64,
call: SignalCall,
destinationDeviceId: UInt32?,
candidates: [Data]
) {
AssertIsOnMainThread()
owsAssertDebug(call.isIndividualCall)
individualCallService.callManager(
callManager,
shouldSendIceCandidates: callId,
call: call,
destinationDeviceId: destinationDeviceId,
candidates: candidates
)
}
public func callManager(
_ callManager: CallManager<SignalCall, CallService>,
shouldSendHangup callId: UInt64,
call: SignalCall,
destinationDeviceId: UInt32?,
hangupType: HangupType,
deviceId: UInt32,
useLegacyHangupMessage: Bool
) {
AssertIsOnMainThread()
owsAssertDebug(call.isIndividualCall)
individualCallService.callManager(
callManager,
shouldSendHangup: callId,
call: call,
destinationDeviceId: destinationDeviceId,
hangupType: hangupType,
deviceId: deviceId,
useLegacyHangupMessage: useLegacyHangupMessage
)
}
public func callManager(
_ callManager: CallManager<SignalCall, CallService>,
shouldSendBusy callId: UInt64,
call: SignalCall,
destinationDeviceId: UInt32?
) {
AssertIsOnMainThread()
owsAssertDebug(call.isIndividualCall)
individualCallService.callManager(
callManager,
shouldSendBusy: callId,
call: call,
destinationDeviceId: destinationDeviceId
)
}
public func callManager(
_ callManager: CallManager<SignalCall, CallService>,
onUpdateLocalVideoSession call: SignalCall,
session: AVCaptureSession?
) {
AssertIsOnMainThread()
owsAssertDebug(call.isIndividualCall)
individualCallService.callManager(
callManager,
onUpdateLocalVideoSession: call,
session: session
)
}
public func callManager(
_ callManager: CallManager<SignalCall, CallService>,
onAddRemoteVideoTrack call: SignalCall,
track: RTCVideoTrack
) {
AssertIsOnMainThread()
owsAssertDebug(call.isIndividualCall)
individualCallService.callManager(
callManager,
onAddRemoteVideoTrack: call,
track: track
)
}
}
private extension Error {
var isUntrustedIdentityError: Bool {
let nsError = self as NSError
return nsError.domain == OWSSignalServiceKitErrorDomain && nsError.code == OWSErrorCode.untrustedIdentity.rawValue
}
}