Merge branch 'mlin/PR/EarlyRingForNSEWake' into release/5.21.2

This commit is contained in:
Michelle Linington 2021-09-30 12:57:50 -07:00
commit bfa4148cba
9 changed files with 107 additions and 38 deletions

View File

@ -175,7 +175,7 @@ protocol CallAudioServiceDelegate: AnyObject {
return
}
if call.state == .localRinging {
if [.localRinging_Anticipatory, .localRinging_ReadyToAnswer, .accepting].contains(call.state) {
// The AudioSession for playing a ring tone.
setAudioSession(category: .playback, mode: .default)
} else if call.hasLocalVideo {
@ -210,7 +210,8 @@ protocol CallAudioServiceDelegate: AnyObject {
case .dialing: handleDialing(call: call)
case .answering: handleAnswering(call: call)
case .remoteRinging: handleRemoteRinging(call: call)
case .localRinging: handleLocalRinging(call: call)
case .localRinging_Anticipatory, .localRinging_ReadyToAnswer, .accepting:
handleLocalRinging(call: call)
case .connected: handleConnected(call: call)
case .reconnecting: handleReconnecting(call: call)
case .localFailure: handleLocalFailure(call: call)
@ -370,6 +371,9 @@ protocol CallAudioServiceDelegate: AnyObject {
return
}
// Only CallKit calls should be in the transitory ringing states
owsAssertDebug(call.state == .localRinging_ReadyToAnswer)
vibrateTimer?.invalidate()
vibrateTimer = .scheduledTimer(withTimeInterval: vibrateRepeatDuration, repeats: true) { [weak self] _ in
self?.ringVibration()

View File

@ -28,6 +28,8 @@ public final class CallService: NSObject {
lazy private(set) var audioService = CallAudioService()
public var earlyRingNextIncomingCall = false
private var _currentCall: SignalCall?
@objc
public private(set) var currentCall: SignalCall? {
@ -58,6 +60,9 @@ public final class CallService: NSObject {
}
}
// To be safe, we reset the early ring on any call change so it's not left set from an unexpected state change
earlyRingNextIncomingCall = false
Logger.debug("\(oldValue as Optional) -> \(newValue as Optional)")
let observers = self.observers
@ -1062,6 +1067,10 @@ extension CallService: CallManagerDelegate {
call.individualCall.callId = callId
// We grab this before updating the currentCall since it will unset it by default as a precaution.
let shouldEarlyRing = earlyRingNextIncomingCall && !isOutgoing
earlyRingNextIncomingCall = false
// The call to be started is provided by the event.
currentCall = call
@ -1070,7 +1079,8 @@ extension CallService: CallManagerDelegate {
shouldStartCall: call,
callId: callId,
isOutgoing: isOutgoing,
callMediaType: callMediaType
callMediaType: callMediaType,
shouldEarlyRing: shouldEarlyRing
)
}

View File

@ -11,7 +11,14 @@ public enum CallState: String {
case dialing
case answering
case remoteRinging
case localRinging
// The local ringing state is a bit more complex since we sometimes kick off
// a CallKit ring before RingRTC is ready to answer. We can only answer the call
// once both the user has answered and RingRTC is ready.
case localRinging_Anticipatory // RingRTC not ready. User has not answered
case localRinging_ReadyToAnswer // RingRTC ready. User has not answered
case accepting // RingRTC not ready. User has answered
case connected
case reconnecting
case localFailure // terminal
@ -109,7 +116,7 @@ public class IndividualCall: NSObject, IndividualCallNotificationInfo {
switch state {
case .localFailure, .localHangup, .remoteHangup, .remoteHangupNeedPermission, .remoteBusy, .answeredElsewhere, .declinedElsewhere, .busyElsewhere:
return true
case .idle, .dialing, .answering, .remoteRinging, .localRinging, .connected, .reconnecting:
case .idle, .dialing, .answering, .remoteRinging, .localRinging_Anticipatory, .localRinging_ReadyToAnswer, .accepting, .connected, .reconnecting:
return false
}
}
@ -141,6 +148,12 @@ public class IndividualCall: NSObject, IndividualCallNotificationInfo {
}
}
var deferredAnswerCompletion: (() -> Void)? {
didSet {
owsAssertDebug(deferredAnswerCompletion == nil || state == .accepting)
}
}
public var state: CallState {
didSet {
AssertIsOnMainThread()

View File

@ -92,6 +92,13 @@ import SignalMessaging
AssertIsOnMainThread()
Logger.info("\(call)")
defer {
// This should only be non-nil if we had to defer accepting the call while waiting for RingRTC
// If it's set, we need to make sure we call it before returning.
call.individualCall.deferredAnswerCompletion?()
call.individualCall.deferredAnswerCompletion = nil
}
guard callService.currentCall === call else {
let error = OWSAssertionError("accepting call: \(call) which is different from currentCall: \(callService.currentCall as Optional)")
handleFailedCall(failedCall: call, error: error)
@ -148,7 +155,7 @@ import SignalMessaging
if callRecord.callType == .outgoingIncomplete {
callRecord.updateCallType(.outgoingMissed)
}
} else if call.individualCall.state == .localRinging {
} else if [.localRinging_Anticipatory, .localRinging_ReadyToAnswer].contains(call.individualCall.state) {
let callRecord = TSCall(
callType: .incomingDeclined,
offerType: call.individualCall.offerMediaType,
@ -503,11 +510,23 @@ import SignalMessaging
// MARK: - Call Manager Events
public func callManager(_ callManager: CallService.CallManagerType, shouldStartCall call: SignalCall, callId: UInt64, isOutgoing: Bool, callMediaType: CallMediaType) {
public func callManager(_ callManager: CallService.CallManagerType, shouldStartCall call: SignalCall, callId: UInt64, isOutgoing: Bool, callMediaType: CallMediaType, shouldEarlyRing: Bool) {
AssertIsOnMainThread()
owsAssertDebug(call.isIndividualCall)
Logger.info("call: \(call)")
if shouldEarlyRing {
if isOutgoing {
// If we are using the NSE, we need to kick off a ring ASAP in case this incoming call
// has resulted in the NSE waking up the main app.
owsAssertDebug(callUIAdapter.adaptee(for: call) === callUIAdapter.callKitAdaptee)
Logger.info("Performing early ring")
handleRinging(call: call, isAnticipatory: true)
} else {
owsFailDebug("Cannot early ring an outgoing call")
}
}
// Start the call, asynchronously.
getIceServers().done(on: .main) { iceServers in
guard self.callService.currentCall === call else {
@ -579,7 +598,7 @@ import SignalMessaging
audioSession.isRTCAudioEnabled = false
switch call.individualCall.state {
case .idle, .dialing, .answering, .localRinging, .localFailure, .remoteBusy, .remoteRinging:
case .idle, .dialing, .answering, .localRinging_Anticipatory, .localRinging_ReadyToAnswer, .accepting, .localFailure, .remoteBusy, .remoteRinging:
handleMissedCall(call)
case .connected, .reconnecting, .localHangup, .remoteHangup, .remoteHangupNeedPermission, .answeredElsewhere, .declinedElsewhere, .busyElsewhere:
Logger.info("call is finished")
@ -601,7 +620,7 @@ import SignalMessaging
audioSession.isRTCAudioEnabled = false
switch call.individualCall.state {
case .idle, .dialing, .answering, .localRinging, .localFailure, .remoteBusy, .remoteRinging:
case .idle, .dialing, .answering, .localRinging_Anticipatory, .localRinging_ReadyToAnswer, .accepting, .localFailure, .remoteBusy, .remoteRinging:
handleMissedCall(call)
case .connected, .reconnecting, .localHangup, .remoteHangup, .remoteHangupNeedPermission, .answeredElsewhere, .declinedElsewhere, .busyElsewhere:
Logger.info("call is finished")
@ -626,10 +645,10 @@ import SignalMessaging
case .idle, .dialing, .remoteBusy, .remoteRinging, .answeredElsewhere, .declinedElsewhere, .busyElsewhere, .remoteHangup, .remoteHangupNeedPermission:
handleFailedCall(failedCall: call, error: OWSAssertionError("unexpected state for endedRemoteHangupAccepted: \(call.individualCall.state)"))
return
case .answering, .connected:
case .answering, .accepting, .connected:
Logger.info("tried answering locally, but answered somewhere else first. state: \(call.individualCall.state)")
handleAnsweredElsewhere(call: call)
case .localRinging, .reconnecting:
case .localRinging_Anticipatory, .localRinging_ReadyToAnswer, .reconnecting:
handleAnsweredElsewhere(call: call)
case .localFailure, .localHangup:
Logger.info("ignoring 'endedRemoteHangupAccepted' since call is already finished")
@ -647,10 +666,10 @@ import SignalMessaging
case .idle, .dialing, .remoteBusy, .remoteRinging, .answeredElsewhere, .declinedElsewhere, .busyElsewhere, .remoteHangup, .remoteHangupNeedPermission:
handleFailedCall(failedCall: call, error: OWSAssertionError("unexpected state for endedRemoteHangupDeclined: \(call.individualCall.state)"))
return
case .answering, .connected:
case .answering, .accepting, .connected:
Logger.info("tried answering locally, but declined somewhere else first. state: \(call.individualCall.state)")
handleDeclinedElsewhere(call: call)
case .localRinging, .reconnecting:
case .localRinging_Anticipatory, .localRinging_ReadyToAnswer, .reconnecting:
handleDeclinedElsewhere(call: call)
case .localFailure, .localHangup:
Logger.info("ignoring 'endedRemoteHangupDeclined' since call is already finished")
@ -668,10 +687,10 @@ import SignalMessaging
case .idle, .dialing, .remoteBusy, .remoteRinging, .answeredElsewhere, .declinedElsewhere, .busyElsewhere, .remoteHangup, .remoteHangupNeedPermission:
handleFailedCall(failedCall: call, error: OWSAssertionError("unexpected state for endedRemoteHangupBusy: \(call.individualCall.state)"))
return
case .answering, .connected:
case .answering, .accepting, .connected:
Logger.info("tried answering locally, but already in a call somewhere else first. state: \(call.individualCall.state)")
handleBusyElsewhere(call: call)
case .localRinging, .reconnecting:
case .localRinging_Anticipatory, .localRinging_ReadyToAnswer, .reconnecting:
handleBusyElsewhere(call: call)
case .localFailure, .localHangup:
Logger.info("ignoring 'endedRemoteHangupBusy' since call is already finished")
@ -1145,13 +1164,22 @@ import SignalMessaging
}
/**
* The clients can now communicate via WebRTC, so we can let the UI know.
* Present UI to begin ringing.
*
* Called by both caller and callee. Compatible ICE messages have been exchanged between the local and remote
* client.
* This can be performed in response to:
* - Established communication via WebRTC
* - Anticipation of an expected future ring.
*
* In the former case, compatible ICE messages have been exchanged between the local and remote
* client and we can ring with confidence that the call will connect.
*
* In the latter case, the ring is performed before any messages have been exchanged. This is to satisfy
* callservicesd which requires that we post a CallKit ring shortly after the NSE wakes the main app.
*/
private func handleRinging(call: SignalCall) {
private func handleRinging(call: SignalCall, isAnticipatory: Bool = false) {
AssertIsOnMainThread()
// Only incoming calls can use the early ring states
owsAssertDebug(!(call.individualCall.direction == .outgoing && isAnticipatory))
Logger.info("call: \(call)")
guard call === callService.currentCall else {
@ -1161,19 +1189,23 @@ import SignalMessaging
switch call.individualCall.state {
case .dialing:
if call.individualCall.state != .remoteRinging {
BenchEventComplete(eventId: "call-\(call.individualCall.localId)")
}
BenchEventComplete(eventId: "call-\(call.individualCall.localId)")
call.individualCall.state = .remoteRinging
case .answering:
if call.individualCall.state != .localRinging {
BenchEventComplete(eventId: "call-\(call.individualCall.localId)")
}
call.individualCall.state = .localRinging
BenchEventComplete(eventId: "call-\(call.individualCall.localId)")
call.individualCall.state = isAnticipatory ? .localRinging_Anticipatory : .localRinging_ReadyToAnswer
self.callUIAdapter.reportIncomingCall(call, thread: call.individualCall.thread)
case .localRinging_Anticipatory:
// RingRTC became ready during our anticipatory ring. User hasn't tried to answer yet.
owsAssertDebug(isAnticipatory == false)
call.individualCall.state = .localRinging_ReadyToAnswer
case .accepting:
// The user answered during our early ring, but we've been waiting for RingRTC to tell us to start
// actually ringing before trying to accept. We can do that now.
handleAcceptCall(call)
case .remoteRinging:
Logger.info("call already ringing. Ignoring \(#function): \(call).")
case .idle, .localRinging, .connected, .reconnecting, .localFailure, .localHangup, .remoteHangup, .remoteHangupNeedPermission, .remoteBusy, .answeredElsewhere, .declinedElsewhere, .busyElsewhere:
case .idle, .connected, .reconnecting, .localFailure, .localHangup, .remoteHangup, .remoteHangupNeedPermission, .remoteBusy, .answeredElsewhere, .declinedElsewhere, .busyElsewhere, .localRinging_ReadyToAnswer:
owsFailDebug("unexpected call state: \(call.individualCall.state): \(call).")
}
}
@ -1188,7 +1220,7 @@ import SignalMessaging
}
switch call.individualCall.state {
case .remoteRinging, .localRinging:
case .remoteRinging, .localRinging_Anticipatory, .localRinging_ReadyToAnswer, .accepting:
Logger.debug("disconnect while ringing... we'll keep ringing")
case .connected:
call.individualCall.state = .reconnecting
@ -1326,7 +1358,7 @@ import SignalMessaging
}()
switch failedCall.individualCall.state {
case .answering, .localRinging:
case .answering, .localRinging_Anticipatory, .localRinging_ReadyToAnswer, .accepting:
assert(failedCall.individualCall.callRecord == nil)
// call failed before any call record could be created, make one now.
handleMissedCall(failedCall)

View File

@ -350,15 +350,24 @@ final class CallKitCallUIAdaptee: NSObject, CallUIAdaptee, CXProviderDelegate {
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
AssertIsOnMainThread()
Logger.info("Received \(#function) CXAnswerCallAction")
Logger.info("Received \(#function) CXAnswerCallAction \(action.timeoutDate)")
guard let call = callManager.callWithLocalId(action.callUUID) else {
owsFailDebug("call as unexpectedly nil")
action.fail()
return
}
self.callService.individualCallService.handleAcceptCall(call)
action.fulfill()
if call.individualCall.state == .localRinging_Anticipatory {
// We can't answer the call until RingRTC is ready
call.individualCall.state = .accepting
call.individualCall.deferredAnswerCompletion = {
action.fulfill()
}
} else {
owsAssertDebug(call.individualCall.state == .localRinging_ReadyToAnswer)
self.callService.individualCallService.handleAcceptCall(call)
action.fulfill()
}
}
public func provider(_ provider: CXProvider, perform action: CXEndCallAction) {

View File

@ -8,7 +8,7 @@ import SignalServiceKit
import SignalMessaging
import WebRTC
protocol CallUIAdaptee {
protocol CallUIAdaptee: AnyObject {
var notificationPresenter: NotificationPresenter { get }
var callService: CallService { get }
var hasManualRinger: Bool { get }

View File

@ -641,7 +641,7 @@ class IndividualCallViewController: OWSViewController, CallObserver, CallAudioSe
}
private var isRenderingLocalVanityVideo: Bool {
return [.idle, .dialing, .remoteRinging, .localRinging].contains(call.individualCall.state) && !localVideoView.isHidden
return [.idle, .dialing, .remoteRinging, .localRinging_Anticipatory, .localRinging_ReadyToAnswer].contains(call.individualCall.state) && !localVideoView.isHidden
}
private var previousOrigin: CGPoint!
@ -726,14 +726,14 @@ class IndividualCallViewController: OWSViewController, CallObserver, CallAudioSe
return NSLocalizedString("IN_CALL_CONNECTING", comment: "Call setup status label")
case .remoteRinging:
return NSLocalizedString("IN_CALL_RINGING", comment: "Call setup status label")
case .localRinging:
case .localRinging_Anticipatory, .localRinging_ReadyToAnswer:
switch call.individualCall.offerMediaType {
case .audio:
return NSLocalizedString("IN_CALL_RINGING_AUDIO", comment: "Call setup status label")
case .video:
return NSLocalizedString("IN_CALL_RINGING_VIDEO", comment: "Call setup status label")
}
case .answering:
case .answering, .accepting:
return NSLocalizedString("IN_CALL_SECURING", comment: "Call setup status label")
case .connected:
let callDuration = call.connectionDuration()
@ -827,7 +827,7 @@ class IndividualCallViewController: OWSViewController, CallObserver, CallAudioSe
)
// Show Incoming vs. Ongoing call controls
if call.individualCall.state == .localRinging {
if [.localRinging_Anticipatory, .localRinging_ReadyToAnswer].contains(call.individualCall.state) {
let isVideoOffer = call.individualCall.offerMediaType == .video
incomingVideoCallControls.isHidden = !isVideoOffer
incomingAudioCallControls.isHidden = isVideoOffer

View File

@ -53,7 +53,7 @@ class NonCallKitCallUIAdaptee: NSObject, CallUIAdaptee {
// we re-present the notifiation every 3 seconds to make sure
// the user sees that their phone is ringing
incomingCallNotificationTimer = Timer.scheduledTimer(withTimeInterval: 3, repeats: true) { [weak self] _ in
guard call.individualCall.state == .localRinging else {
guard call.individualCall.state == .localRinging_ReadyToAnswer else {
self?.incomingCallNotificationTimer?.invalidate()
self?.incomingCallNotificationTimer = nil
return

View File

@ -132,6 +132,7 @@ public enum PushRegistrationError: Error {
if let callRelayPayload = callRelayPayload {
Logger.info("Received VoIP push from the NSE: \(callRelayPayload)")
owsAssertDebug(isWaitingForSignal.tryToSetFlag())
callService.earlyRingNextIncomingCall = true
}
let isUnexpectedPush = AtomicBool(false)