diff --git a/Signal/src/call/CallService.swift b/Signal/src/call/CallService.swift index 1991e4691..f882f154d 100644 --- a/Signal/src/call/CallService.swift +++ b/Signal/src/call/CallService.swift @@ -309,7 +309,9 @@ protocol CallServiceObserver: class { in: thread) callRecord.save() - self.callUIAdapter.reportMissedCall(call) + DispatchQueue.main.async { + self.callUIAdapter.reportMissedCall(call) + } } /** @@ -514,7 +516,9 @@ protocol CallServiceObserver: class { call.state = .remoteRinging case .answering: call.state = .localRinging - self.callUIAdapter.reportIncomingCall(call, thread: thread) + DispatchQueue.main.async { + self.callUIAdapter.reportIncomingCall(call, thread: thread) + } // cancel connection timeout self.fulfillCallConnectedPromise?() case .remoteRinging: @@ -552,7 +556,9 @@ protocol CallServiceObserver: class { call.state = .remoteHangup // Notify UI - callUIAdapter.remoteDidHangupCall(call) + DispatchQueue.main.async { + self.callUIAdapter.remoteDidHangupCall(call) + } // self.call is nil'd in `terminateCall`, so it's important we update it's state *before* calling `terminateCall` terminateCall() @@ -821,7 +827,9 @@ protocol CallServiceObserver: class { return } - callUIAdapter.recipientAcceptedCall(call) + DispatchQueue.main.async { + self.callUIAdapter.recipientAcceptedCall(call) + } handleConnectedCall(call) } else if message.hasHangup() { @@ -958,7 +966,9 @@ protocol CallServiceObserver: class { // It's essential to set call.state before terminateCall, because terminateCall nils self.call call.error = error call.state = .localFailure - callUIAdapter.failCall(call, error: error) + DispatchQueue.main.async { + self.callUIAdapter.failCall(call, error: error) + } } else { // This can happen when we receive an out of band signaling message (e.g. IceUpdate) // after the call has ended @@ -1114,7 +1124,7 @@ protocol CallServiceObserver: class { } } } - + // Prevent screen from dimming during video call. let hasLocalOrRemoteVideo = localVideoTrack != nil || remoteVideoTrack != nil UIApplication.shared.isIdleTimerDisabled = hasLocalOrRemoteVideo diff --git a/Signal/src/call/NonCallKitCallUIAdaptee.swift b/Signal/src/call/NonCallKitCallUIAdaptee.swift index 876c9d68c..cc025565f 100644 --- a/Signal/src/call/NonCallKitCallUIAdaptee.swift +++ b/Signal/src/call/NonCallKitCallUIAdaptee.swift @@ -23,6 +23,8 @@ class NonCallKitCallUIAdaptee: CallUIAdaptee { } func startOutgoingCall(handle: String) -> SignalCall { + AssertIsOnMainThread() + let call = SignalCall.outgoingCall(localId: UUID(), remotePhoneNumber: handle) CallService.signalingQueue.async { @@ -37,6 +39,8 @@ class NonCallKitCallUIAdaptee: CallUIAdaptee { } func reportIncomingCall(_ call: SignalCall, callerName: String) { + AssertIsOnMainThread() + Logger.debug("\(TAG) \(#function)") // present Call View controller @@ -52,10 +56,14 @@ class NonCallKitCallUIAdaptee: CallUIAdaptee { } func reportMissedCall(_ call: SignalCall, callerName: String) { + AssertIsOnMainThread() + notificationsAdapter.presentMissedCall(call, callerName: callerName) } func answerCall(localId: UUID) { + AssertIsOnMainThread() + CallService.signalingQueue.async { guard let call = self.callService.call else { assertionFailure("\(self.TAG) in \(#function) No current call.") @@ -72,6 +80,8 @@ class NonCallKitCallUIAdaptee: CallUIAdaptee { } func answerCall(_ call: SignalCall) { + AssertIsOnMainThread() + CallService.signalingQueue.async { guard call.localId == self.callService.call?.localId else { assertionFailure("\(self.TAG) in \(#function) localId does not match current call") @@ -84,6 +94,8 @@ class NonCallKitCallUIAdaptee: CallUIAdaptee { } func declineCall(localId: UUID) { + AssertIsOnMainThread() + CallService.signalingQueue.async { guard let call = self.callService.call else { assertionFailure("\(self.TAG) in \(#function) No current call.") @@ -100,6 +112,8 @@ class NonCallKitCallUIAdaptee: CallUIAdaptee { } func declineCall(_ call: SignalCall) { + AssertIsOnMainThread() + CallService.signalingQueue.async { guard call.localId == self.callService.call?.localId else { assertionFailure("\(self.TAG) in \(#function) localId does not match current call") @@ -111,10 +125,14 @@ class NonCallKitCallUIAdaptee: CallUIAdaptee { } func recipientAcceptedCall(_ call: SignalCall) { + AssertIsOnMainThread() + PeerConnectionClient.startAudioSession() } func localHangupCall(_ call: SignalCall) { + AssertIsOnMainThread() + CallService.signalingQueue.async { // If both parties hang up at the same moment, // call might already be nil. @@ -128,14 +146,20 @@ class NonCallKitCallUIAdaptee: CallUIAdaptee { } internal func remoteDidHangupCall(_ call: SignalCall) { + AssertIsOnMainThread() + Logger.debug("\(TAG) in \(#function) is no-op") } internal func failCall(_ call: SignalCall, error: CallError) { + AssertIsOnMainThread() + Logger.debug("\(TAG) in \(#function) is no-op") } func setIsMuted(call: SignalCall, isMuted: Bool) { + AssertIsOnMainThread() + CallService.signalingQueue.async { guard call.localId == self.callService.call?.localId else { assertionFailure("\(self.TAG) in \(#function) localId does not match current call") @@ -147,6 +171,8 @@ class NonCallKitCallUIAdaptee: CallUIAdaptee { } func setHasLocalVideo(call: SignalCall, hasLocalVideo: Bool) { + AssertIsOnMainThread() + CallService.signalingQueue.async { guard call.localId == self.callService.call?.localId else { assertionFailure("\(self.TAG) in \(#function) localId does not match current call") diff --git a/Signal/src/call/Speakerbox/CallKitCallUIAdaptee.swift b/Signal/src/call/Speakerbox/CallKitCallUIAdaptee.swift index e4599a29e..f427d6032 100644 --- a/Signal/src/call/Speakerbox/CallKitCallUIAdaptee.swift +++ b/Signal/src/call/Speakerbox/CallKitCallUIAdaptee.swift @@ -49,6 +49,8 @@ final class CallKitCallUIAdaptee: NSObject, CallUIAdaptee, CXProviderDelegate { } init(callService: CallService, notificationsAdapter: CallNotificationsAdapter) { + AssertIsOnMainThread() + self.callManager = CallKitCallManager() self.callService = callService self.notificationsAdapter = notificationsAdapter @@ -62,6 +64,8 @@ final class CallKitCallUIAdaptee: NSObject, CallUIAdaptee, CXProviderDelegate { // MARK: CallUIAdaptee func startOutgoingCall(handle: String) -> SignalCall { + AssertIsOnMainThread() + let call = SignalCall.outgoingCall(localId: UUID(), remotePhoneNumber: handle) // Add the new outgoing call to the app's list of calls. @@ -74,11 +78,15 @@ final class CallKitCallUIAdaptee: NSObject, CallUIAdaptee, CXProviderDelegate { // Called from CallService after call has ended to clean up any remaining CallKit call state. func failCall(_ call: SignalCall, error: CallError) { + AssertIsOnMainThread() + provider.reportCall(with: call.localId, endedAt: Date(), reason: CXCallEndedReason.failed) self.callManager.removeCall(call) } func reportIncomingCall(_ call: SignalCall, callerName: String) { + AssertIsOnMainThread() + // Construct a CXCallUpdate describing the incoming call, including the caller. let update = CXCallUpdate() update.remoteHandle = CXHandle(type: .phoneNumber, value: call.remotePhoneNumber) @@ -104,39 +112,57 @@ final class CallKitCallUIAdaptee: NSObject, CallUIAdaptee, CXProviderDelegate { } func answerCall(localId: UUID) { + AssertIsOnMainThread() + assertionFailure("CallKit should answer calls via system call screen, not via notifications.") } func answerCall(_ call: SignalCall) { + AssertIsOnMainThread() + callManager.answer(call: call) } func declineCall(localId: UUID) { + AssertIsOnMainThread() + assertionFailure("CallKit should decline calls via system call screen, not via notifications.") } func declineCall(_ call: SignalCall) { + AssertIsOnMainThread() + callManager.localHangup(call: call) } func recipientAcceptedCall(_ call: SignalCall) { + AssertIsOnMainThread() + // no - op // TODO provider update call connected? } func localHangupCall(_ call: SignalCall) { + AssertIsOnMainThread() + callManager.localHangup(call: call) } func remoteDidHangupCall(_ call: SignalCall) { + AssertIsOnMainThread() + provider.reportCall(with: call.localId, endedAt: nil, reason: CXCallEndedReason.remoteEnded) } func setIsMuted(call: SignalCall, isMuted: Bool) { + AssertIsOnMainThread() + callManager.setIsMuted(call: call, isMuted: isMuted) } func setHasLocalVideo(call: SignalCall, hasLocalVideo: Bool) { + AssertIsOnMainThread() + let update = CXCallUpdate() update.remoteHandle = CXHandle(type: .phoneNumber, value: call.remotePhoneNumber) update.hasVideo = hasLocalVideo @@ -152,6 +178,8 @@ final class CallKitCallUIAdaptee: NSObject, CallUIAdaptee, CXProviderDelegate { // MARK: CXProviderDelegate func providerDidReset(_ provider: CXProvider) { + AssertIsOnMainThread() + Logger.debug("\(TAG) in \(#function)") // TODO @@ -174,6 +202,8 @@ final class CallKitCallUIAdaptee: NSObject, CallUIAdaptee, CXProviderDelegate { } func provider(_ provider: CXProvider, perform action: CXStartCallAction) { + AssertIsOnMainThread() + Logger.debug("\(TAG) in \(#function) CXStartCallAction") // TODO does this work when `action.handle.value` is not in e164 format, e.g. if called via intent? @@ -207,6 +237,8 @@ final class CallKitCallUIAdaptee: NSObject, CallUIAdaptee, CXProviderDelegate { } func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) { + AssertIsOnMainThread() + Logger.debug("\(TAG) Received \(#function) CXAnswerCallAction") // Retrieve the SpeakerboxCall instance corresponding to the action's call UUID guard let call = callManager.callWithLocalId(action.callUUID) else { @@ -232,6 +264,8 @@ final class CallKitCallUIAdaptee: NSObject, CallUIAdaptee, CXProviderDelegate { } public func provider(_ provider: CXProvider, perform action: CXEndCallAction) { + AssertIsOnMainThread() + Logger.debug("\(TAG) Received \(#function) CXEndCallAction") guard let call = callManager.callWithLocalId(action.callUUID) else { action.fail() @@ -257,6 +291,8 @@ final class CallKitCallUIAdaptee: NSObject, CallUIAdaptee, CXProviderDelegate { } public func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) { + AssertIsOnMainThread() + Logger.debug("\(TAG) Received \(#function) CXSetHeldCallAction") guard let call = callManager.callWithLocalId(action.callUUID) else { action.fail() @@ -282,6 +318,8 @@ final class CallKitCallUIAdaptee: NSObject, CallUIAdaptee, CXProviderDelegate { } public func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) { + AssertIsOnMainThread() + Logger.debug("\(TAG) Received \(#function) CXSetMutedCallAction") guard callManager.callWithLocalId(action.callUUID) != nil else { Logger.error("\(TAG) Failing CXSetMutedCallAction for unknown call: \(action.callUUID)") @@ -296,20 +334,28 @@ final class CallKitCallUIAdaptee: NSObject, CallUIAdaptee, CXProviderDelegate { } public func provider(_ provider: CXProvider, perform action: CXSetGroupCallAction) { + AssertIsOnMainThread() + Logger.warn("\(TAG) unimplemented \(#function) for CXSetGroupCallAction") } public func provider(_ provider: CXProvider, perform action: CXPlayDTMFCallAction) { + AssertIsOnMainThread() + Logger.warn("\(TAG) unimplemented \(#function) for CXPlayDTMFCallAction") } func provider(_ provider: CXProvider, timedOutPerforming action: CXAction) { + AssertIsOnMainThread() + Logger.debug("\(TAG) Timed out \(#function)") // React to the action timeout if necessary, such as showing an error UI. } func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) { + AssertIsOnMainThread() + Logger.debug("\(TAG) Received \(#function)") // Start recording @@ -317,6 +363,8 @@ final class CallKitCallUIAdaptee: NSObject, CallUIAdaptee, CXProviderDelegate { } func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) { + AssertIsOnMainThread() + Logger.debug("\(TAG) Received \(#function)") /* diff --git a/Signal/src/call/UserInterface/CallUIAdapter.swift b/Signal/src/call/UserInterface/CallUIAdapter.swift index b448dcd82..98e581a8c 100644 --- a/Signal/src/call/UserInterface/CallUIAdapter.swift +++ b/Signal/src/call/UserInterface/CallUIAdapter.swift @@ -63,6 +63,8 @@ extension CallUIAdaptee { private let audioService: CallAudioService required init(callService: CallService, contactsManager: OWSContactsManager, notificationsAdapter: CallNotificationsAdapter) { + AssertIsOnMainThread() + self.contactsManager = contactsManager if Platform.isSimulator { // CallKit doesn't seem entirely supported in simulator. @@ -82,6 +84,8 @@ extension CallUIAdaptee { } internal func reportIncomingCall(_ call: SignalCall, thread: TSContactThread) { + AssertIsOnMainThread() + call.addObserverAndSyncState(observer: audioService) let callerName = self.contactsManager.displayName(forPhoneIdentifier: call.remotePhoneNumber) @@ -89,11 +93,15 @@ extension CallUIAdaptee { } internal func reportMissedCall(_ call: SignalCall) { + AssertIsOnMainThread() + let callerName = self.contactsManager.displayName(forPhoneIdentifier: call.remotePhoneNumber) adaptee.reportMissedCall(call, callerName: callerName) } internal func startOutgoingCall(handle: String) -> SignalCall { + AssertIsOnMainThread() + let call = adaptee.startOutgoingCall(handle: handle) call.addObserverAndSyncState(observer: audioService) @@ -101,56 +109,82 @@ extension CallUIAdaptee { } internal func answerCall(localId: UUID) { + AssertIsOnMainThread() + adaptee.answerCall(localId: localId) } internal func answerCall(_ call: SignalCall) { + AssertIsOnMainThread() + adaptee.answerCall(call) } internal func declineCall(localId: UUID) { + AssertIsOnMainThread() + adaptee.declineCall(localId: localId) } internal func declineCall(_ call: SignalCall) { + AssertIsOnMainThread() + adaptee.declineCall(call) } internal func callBack(recipientId: String) { + AssertIsOnMainThread() + adaptee.callBack(recipientId: recipientId) } internal func recipientAcceptedCall(_ call: SignalCall) { + AssertIsOnMainThread() + adaptee.recipientAcceptedCall(call) } internal func remoteDidHangupCall(_ call: SignalCall) { + AssertIsOnMainThread() + adaptee.remoteDidHangupCall(call) } internal func localHangupCall(_ call: SignalCall) { + AssertIsOnMainThread() + adaptee.localHangupCall(call) } internal func failCall(_ call: SignalCall, error: CallError) { + AssertIsOnMainThread() + adaptee.failCall(call, error: error) } internal func showCall(_ call: SignalCall) { + AssertIsOnMainThread() + adaptee.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.setIsMuted(call: call, isMuted: isMuted) } internal func setHasLocalVideo(call: SignalCall, hasLocalVideo: Bool) { + AssertIsOnMainThread() + adaptee.setHasLocalVideo(call: call, hasLocalVideo: hasLocalVideo) } internal func setIsSpeakerphoneEnabled(call: SignalCall, isEnabled: Bool) { - // Speakerphone is not handled by CallKit (e.g. there is no CXAction), so we handle it w/o going through the + AssertIsOnMainThread() + + // Speakerphone is not handled by CallKit (e.g. there is no CXAction), so we handle it w/o going through the // adaptee, relying on the AudioService CallObserver to put the system in a state consistent with the call's // assigned property. CallService.signalingQueue.async { @@ -160,6 +194,8 @@ extension CallUIAdaptee { // CallKit handles ringing state on it's own. But for non-call kit we trigger ringing start/stop manually. internal var hasManualRinger: Bool { + AssertIsOnMainThread() + return adaptee.hasManualRinger } }