diff --git a/Signal/src/call/CallService.swift b/Signal/src/call/CallService.swift index 6eff5c5be..81bb41dc5 100644 --- a/Signal/src/call/CallService.swift +++ b/Signal/src/call/CallService.swift @@ -77,8 +77,8 @@ enum CallError: Error { case timeout(description: String) } -// FIXME TODO do we need to timeout? -fileprivate let timeoutSeconds = 60 +// Should be roughly synced with Android client for consistency +fileprivate let connectingTimeoutSeconds = 120 // All Observer methods will be invoked from the main thread. protocol CallServiceObserver: class { @@ -298,7 +298,7 @@ protocol CallServiceObserver: class { return getIceServers().then { iceServers -> Promise in Logger.debug("\(self.TAG) got ice servers:\(iceServers)") - let peerConnectionClient = PeerConnectionClient(iceServers: iceServers, delegate: self, callType: .outgoing) + let peerConnectionClient = PeerConnectionClient(iceServers: iceServers, delegate: self, callDirection: .outgoing) assert(self.peerConnectionClient == nil, "Unexpected PeerConnectionClient instance") Logger.debug("\(self.TAG) setting peerConnectionClient in \(#function)") @@ -311,6 +311,17 @@ protocol CallServiceObserver: class { let callMessage = OWSOutgoingCallMessage(thread: thread, offerMessage: offerMessage) return self.messageSender.sendCallMessage(callMessage) } + }.then { + let (callConnectedPromise, fulfill, _) = Promise.pending() + self.fulfillCallConnectedPromise = fulfill + + // Don't let the outgoing call ring forever. We don't support inbound ringing forever anyway. + let timeout: Promise = after(interval: TimeInterval(connectingTimeoutSeconds)).then { () -> Void in + // rejecting a promise by throwing is safely a no-op if the promise has already been fulfilled + throw CallError.timeout(description: "timed out waiting to receive call answer") + } + + return race(timeout, callConnectedPromise) }.catch { error in Logger.error("\(self.TAG) placing call failed with error: \(error)") @@ -456,7 +467,7 @@ protocol CallServiceObserver: class { } assert(self.peerConnectionClient == nil, "Unexpected PeerConnectionClient instance") Logger.debug("\(self.self.TAG) setting peerConnectionClient in \(#function)") - self.peerConnectionClient = PeerConnectionClient(iceServers: iceServers, delegate: self, callType: .incoming) + self.peerConnectionClient = PeerConnectionClient(iceServers: iceServers, delegate: self, callDirection: .incoming) let offerSessionDescription = RTCSessionDescription(type: .offer, sdp: callerSessionDescription) let constraints = RTCMediaConstraints(mandatoryConstraints: nil, optionalConstraints: nil) @@ -481,7 +492,7 @@ protocol CallServiceObserver: class { let (promise, fulfill, _) = Promise.pending() - let timeout: Promise = after(interval: TimeInterval(timeoutSeconds)).then { () -> Void in + let timeout: Promise = after(interval: TimeInterval(connectingTimeoutSeconds)).then { () -> Void in // rejecting a promise by throwing is safely a no-op if the promise has already been fulfilled throw CallError.timeout(description: "timed out waiting for call to connect") } diff --git a/Signal/src/call/PeerConnectionClient.swift b/Signal/src/call/PeerConnectionClient.swift index ba1ed865c..676f23c01 100644 --- a/Signal/src/call/PeerConnectionClient.swift +++ b/Signal/src/call/PeerConnectionClient.swift @@ -64,11 +64,6 @@ protocol PeerConnectionClientDelegate: class { */ class PeerConnectionClient: NSObject, RTCPeerConnectionDelegate, RTCDataChannelDelegate { - enum CallType { - case incoming - case outgoing - } - let TAG = "[PeerConnectionClient]" enum Identifiers: String { case mediaStream = "ARDAMS", @@ -125,7 +120,7 @@ class PeerConnectionClient: NSObject, RTCPeerConnectionDelegate, RTCDataChannelD private var remoteVideoTrack: RTCVideoTrack? private var cameraConstraints: RTCMediaConstraints - init(iceServers: [RTCIceServer], delegate: PeerConnectionClientDelegate, callType: CallType) { + init(iceServers: [RTCIceServer], delegate: PeerConnectionClientDelegate, callDirection: CallDirection) { AssertIsOnMainThread() self.iceServers = iceServers @@ -152,7 +147,7 @@ class PeerConnectionClient: NSObject, RTCPeerConnectionDelegate, RTCDataChannelD createAudioSender() createVideoSender() - if callType == .outgoing { + if callDirection == .outgoing { // When placing an outgoing call, it's our responsibility to create the DataChannel. // Recipient will not have to do this explicitly. createSignalingDataChannel() diff --git a/Signal/src/call/SignalCall.swift b/Signal/src/call/SignalCall.swift index b97c419fd..1d93762bc 100644 --- a/Signal/src/call/SignalCall.swift +++ b/Signal/src/call/SignalCall.swift @@ -17,6 +17,10 @@ enum CallState: String { case remoteBusy // terminal } +enum CallDirection { + case outgoing, incoming +} + // All Observer methods will be invoked from the main thread. protocol CallObserver: class { func stateDidChange(call: SignalCall, state: CallState) @@ -40,6 +44,8 @@ protocol CallObserver: class { // Signal Service identifier for this Call. Used to coordinate the call across remote clients. let signalingId: UInt64 + let direction: CallDirection + // Distinguishes between calls locally, e.g. in CallKit let localId: UUID @@ -105,7 +111,8 @@ protocol CallObserver: class { // MARK: Initializers and Factory Methods - init(localId: UUID, signalingId: UInt64, state: CallState, remotePhoneNumber: String) { + init(direction: CallDirection, localId: UUID, signalingId: UInt64, state: CallState, remotePhoneNumber: String) { + self.direction = direction self.localId = localId self.signalingId = signalingId self.state = state @@ -113,11 +120,11 @@ protocol CallObserver: class { } class func outgoingCall(localId: UUID, remotePhoneNumber: String) -> SignalCall { - return SignalCall(localId: localId, signalingId: newCallSignalingId(), state: .dialing, remotePhoneNumber: remotePhoneNumber) + return SignalCall(direction: .outgoing, localId: localId, signalingId: newCallSignalingId(), state: .dialing, remotePhoneNumber: remotePhoneNumber) } class func incomingCall(localId: UUID, remotePhoneNumber: String, signalingId: UInt64) -> SignalCall { - return SignalCall(localId: localId, signalingId: signalingId, state: .answering, remotePhoneNumber: remotePhoneNumber) + return SignalCall(direction: .incoming, localId: localId, signalingId: signalingId, state: .answering, remotePhoneNumber: remotePhoneNumber) } // - diff --git a/Signal/src/view controllers/CallViewController.swift b/Signal/src/view controllers/CallViewController.swift index 98b99fe47..06a4a049b 100644 --- a/Signal/src/view controllers/CallViewController.swift +++ b/Signal/src/view controllers/CallViewController.swift @@ -11,10 +11,6 @@ import PromiseKit @objc(OWSCallViewController) class CallViewController: UIViewController, CallObserver, CallServiceObserver, RTCEAGLVideoViewDelegate { - enum CallDirection { - case unspecified, outgoing, incoming - } - let TAG = "[CallViewController]" // Dependencies @@ -24,7 +20,6 @@ class CallViewController: UIViewController, CallObserver, CallServiceObserver, R // MARK: Properties - var callDirection: CallDirection = .unspecified var thread: TSContactThread! var call: SignalCall! @@ -139,17 +134,7 @@ class CallViewController: UIViewController, CallObserver, CallServiceObserver, R contactNameLabel.text = contactsManager.displayName(forPhoneIdentifier: thread.contactIdentifier()) contactAvatarView.image = OWSAvatarBuilder.buildImage(for: thread, contactsManager: contactsManager) - switch callDirection { - case .unspecified: - Logger.error("\(TAG) must set call direction before call starts.") - showCallFailed(error: OWSErrorMakeAssertionError()) - case .outgoing: - self.call = self.callUIAdapter.startOutgoingCall(handle: thread.contactIdentifier()) - case .incoming: - Logger.error("\(TAG) handling Incoming call") - // No-op, since call service is already set up at this point, the result of which was presenting this viewController. - } - + assert(call != nil) // Subscribe for future call updates call.addObserverAndSyncState(observer: self) @@ -496,16 +481,6 @@ class CallViewController: UIViewController, CallObserver, CallServiceObserver, R // MARK: - Methods - // objc accessible way to set our swift enum. - func setOutgoingCallDirection() { - callDirection = .outgoing - } - - // objc accessible way to set our swift enum. - func setIncomingCallDirection() { - callDirection = .incoming - } - func showCallFailed(error: Error) { // TODO Show something in UI. Logger.error("\(TAG) call failed with error: \(error)") @@ -553,6 +528,17 @@ class CallViewController: UIViewController, CallObserver, CallServiceObserver, R case .remoteBusy: return NSLocalizedString("END_CALL_RESPONDER_IS_BUSY", comment: "Call setup status label") case .localFailure: + if let error = call.error { + switch (error) { + case .timeout(description: _): + if self.call.direction == .outgoing { + return NSLocalizedString("CALL_SCREEN_STATUS_NO_ANSWER", comment: "Call setup status label after outgoing call times out") + } + default: + break + } + } + return NSLocalizedString("END_CALL_UNCATEGORIZED_FAILURE", comment: "Call setup status label") } } diff --git a/Signal/src/view controllers/MessagesViewController.m b/Signal/src/view controllers/MessagesViewController.m index 2f66e7c44..56eb94112 100644 --- a/Signal/src/view controllers/MessagesViewController.m +++ b/Signal/src/view controllers/MessagesViewController.m @@ -72,7 +72,6 @@ static NSTimeInterval const kTSMessageSentDateShowTimeInterval = 5 * 60; -static NSString *const OWSMessagesViewControllerSegueInitiateCall = @"initiateCallSegue"; static NSString *const OWSMessagesViewControllerSegueShowFingerprint = @"fingerprintSegue"; static NSString *const OWSMessagesViewControllerSeguePushConversationSettings = @"OWSMessagesViewControllerSeguePushConversationSettings"; @@ -1600,20 +1599,8 @@ typedef enum : NSUInteger { OWSConversationSettingsTableViewController *controller = (OWSConversationSettingsTableViewController *)segue.destinationViewController; [controller configureWithThread:self.thread]; - } else if ([segue.identifier isEqualToString:OWSMessagesViewControllerSegueInitiateCall]) { - if (![segue.destinationViewController isKindOfClass:[OWSCallViewController class]]) { - DDLogError(@"%@ Expected CallViewController but got: %@", self.tag, segue.destinationViewController); - return; - } - - OWSCallViewController *callViewController = (OWSCallViewController *)segue.destinationViewController; - - if (![self.thread isKindOfClass:[TSContactThread class]]) { - DDLogError(@"%@ Unexpectedly trying to call in group thread:%@. This isn't supported.", self.thread, self.tag); - return; - } - callViewController.thread = (TSContactThread *)self.thread; - [callViewController setOutgoingCallDirection]; + } else { + DDLogDebug(@"%@ Received segue: %@", self.tag, segue.identifier); } } diff --git a/Signal/src/view controllers/SignalsViewController.m b/Signal/src/view controllers/SignalsViewController.m index cb0c33f32..d114b7b05 100644 --- a/Signal/src/view controllers/SignalsViewController.m +++ b/Signal/src/view controllers/SignalsViewController.m @@ -519,7 +519,6 @@ NSString *const SignalsViewControllerSegueShowIncomingCall = @"ShowIncomingCallS return; } OWSCallViewController *callViewController = (OWSCallViewController *)segue.destinationViewController; - [callViewController setIncomingCallDirection]; if (![sender isKindOfClass:[SignalCall class]]) { DDLogError(@"%@ expecting call segueu to be sent by a SignalCall, but found: %@", self.tag, sender); diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index d6b150d95..6a1e986eb 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -76,6 +76,9 @@ /* Accessibilty label for placing call button */ "CALL_LABEL" = "Call"; +/* Call setup status label after outgoing call times out */ +"CALL_SCREEN_STATUS_NO_ANSWER" = "No Answer."; + /* embeds {{Call Status}} in call screen label. For ongoing calls, {{Call Status}} is a seconds timer like 01:23, otherwise {{Call Status}} is a short text like 'Ringing', 'Busy', or 'Failed Call' */ "CALL_STATUS_FORMAT" = "Signal %@";