diff --git a/Signal/Images.xcassets/video-active.imageset/Contents.json b/Signal/Images.xcassets/mute-selected-wide.imageset/Contents.json similarity index 85% rename from Signal/Images.xcassets/video-active.imageset/Contents.json rename to Signal/Images.xcassets/mute-selected-wide.imageset/Contents.json index f1da728b4..b23e910c5 100644 --- a/Signal/Images.xcassets/video-active.imageset/Contents.json +++ b/Signal/Images.xcassets/mute-selected-wide.imageset/Contents.json @@ -2,7 +2,7 @@ "images" : [ { "idiom" : "universal", - "filename" : "video-active.png", + "filename" : "mute-selected-wide.png", "scale" : "1x" }, { diff --git a/Signal/Images.xcassets/mute-selected-wide.imageset/mute-selected-wide.png b/Signal/Images.xcassets/mute-selected-wide.imageset/mute-selected-wide.png new file mode 100644 index 000000000..7d5d3d651 Binary files /dev/null and b/Signal/Images.xcassets/mute-selected-wide.imageset/mute-selected-wide.png differ diff --git a/Signal/Images.xcassets/video-inactive.imageset/Contents.json b/Signal/Images.xcassets/mute-unselected-wide.imageset/Contents.json similarity index 84% rename from Signal/Images.xcassets/video-inactive.imageset/Contents.json rename to Signal/Images.xcassets/mute-unselected-wide.imageset/Contents.json index e4fb17f90..33353ef80 100644 --- a/Signal/Images.xcassets/video-inactive.imageset/Contents.json +++ b/Signal/Images.xcassets/mute-unselected-wide.imageset/Contents.json @@ -2,7 +2,7 @@ "images" : [ { "idiom" : "universal", - "filename" : "video-inactive.png", + "filename" : "mute-unselected-wide.png", "scale" : "1x" }, { diff --git a/Signal/Images.xcassets/mute-unselected-wide.imageset/mute-unselected-wide.png b/Signal/Images.xcassets/mute-unselected-wide.imageset/mute-unselected-wide.png new file mode 100644 index 000000000..d9cb2cda3 Binary files /dev/null and b/Signal/Images.xcassets/mute-unselected-wide.imageset/mute-unselected-wide.png differ diff --git a/Signal/Images.xcassets/video-active.imageset/video-active.png b/Signal/Images.xcassets/video-active.imageset/video-active.png deleted file mode 100644 index b55abee96..000000000 Binary files a/Signal/Images.xcassets/video-active.imageset/video-active.png and /dev/null differ diff --git a/Signal/Images.xcassets/video-inactive.imageset/video-inactive.png b/Signal/Images.xcassets/video-inactive.imageset/video-inactive.png deleted file mode 100644 index b33b2e3fc..000000000 Binary files a/Signal/Images.xcassets/video-inactive.imageset/video-inactive.png and /dev/null differ diff --git a/Signal/src/AppDelegate.m b/Signal/src/AppDelegate.m index 1bcaaa5bf..326d75cd6 100644 --- a/Signal/src/AppDelegate.m +++ b/Signal/src/AppDelegate.m @@ -317,7 +317,13 @@ static NSString *const kURLHostVerifyPrefix = @"verify"; */ - (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray * _Nullable))restorationHandler { - DDLogWarn(@"%@ called %s with userActivity: %@, but not yet supported.", self.tag, __PRETTY_FUNCTION__, userActivity); + if ([userActivity.activityType isEqualToString:@"INStartVideoCallIntent"]) { + [[Environment getCurrent].callService handleCallKitStartVideo]; + } else { + DDLogWarn( + @"%@ called %s with userActivity: %@, but not yet supported.", self.tag, __PRETTY_FUNCTION__, userActivity); + } + // TODO Something like... // *phoneNumber = [[[[[[userActivity interaction] intent] contacts] firstObject] personHandle] value] // thread = blah diff --git a/Signal/src/call/CallService.swift b/Signal/src/call/CallService.swift index fe74cf6d6..3d553d39a 100644 --- a/Signal/src/call/CallService.swift +++ b/Signal/src/call/CallService.swift @@ -1,14 +1,16 @@ -// Created by Michael Kirk on 11/11/16. -// Copyright © 2016 Open Whisper Systems. All rights reserved. +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// import Foundation import PromiseKit import WebRTC /** - * `CallService` manages the state of a WebRTC backed Signal Call (as opposed to the legacy "RedPhone Call"). + * `CallService` is a global singleton that manages the state of WebRTC-backed Signal Calls + * (as opposed to legacy "RedPhone Calls"). * - * It serves as connection from the `CallUIAdapater` to the `PeerConnectionClient`. + * It serves as a connection between the `CallUIAdapter` and the `PeerConnectionClient`. * * ## Signaling * @@ -119,7 +121,7 @@ fileprivate let timeoutSeconds = 60 var incomingCallPromise: Promise? // Used to coordinate promises across delegate methods - var fulfillCallConnectedPromise: (()->())? + var fulfillCallConnectedPromise: (() -> Void)? required init(accountManager: AccountManager, contactsManager: OWSContactsManager, messageSender: MessageSender, notificationsAdapter: CallNotificationsAdapter) { self.accountManager = accountManager @@ -608,7 +610,7 @@ fileprivate let timeoutSeconds = 60 call.state = .connected // We don't risk transmitting any media until the remote client has admitted to being connected. - peerConnectionClient.setAudioEnabled(enabled: true) + peerConnectionClient.setAudioEnabled(enabled: !call.isMuted) peerConnectionClient.setVideoEnabled(enabled: call.hasVideo) } @@ -713,7 +715,7 @@ fileprivate let timeoutSeconds = 60 * * Can be used for Incoming and Outgoing calls. */ - func handleToggledMute(isMuted: Bool) { + func setIsMuted(isMuted: Bool) { assertOnSignalingQueue() guard let peerConnectionClient = self.peerConnectionClient else { @@ -730,6 +732,34 @@ fileprivate let timeoutSeconds = 60 peerConnectionClient.setAudioEnabled(enabled: !isMuted) } + /** + * Local user toggled video. + * + * Can be used for Incoming and Outgoing calls. + */ + func setHasVideo(hasVideo: Bool) { + assertOnSignalingQueue() + + guard let peerConnectionClient = self.peerConnectionClient else { + handleFailedCall(error: .assertionError(description:"\(TAG) peerConnectionClient unexpectedly nil in \(#function)")) + return + } + + guard let call = self.call else { + handleFailedCall(error: .assertionError(description:"\(TAG) call unexpectedly nil in \(#function)")) + return + } + + call.hasVideo = hasVideo + peerConnectionClient.setVideoEnabled(enabled: hasVideo) + } + + func handleCallKitStartVideo() { + CallService.signalingQueue.async { + self.setHasVideo(hasVideo:true) + } + } + /** * Local client received a message on the WebRTC data channel. * diff --git a/Signal/src/call/NonCallKitCallUIAdaptee.swift b/Signal/src/call/NonCallKitCallUIAdaptee.swift index 462821e55..3f5a39742 100644 --- a/Signal/src/call/NonCallKitCallUIAdaptee.swift +++ b/Signal/src/call/NonCallKitCallUIAdaptee.swift @@ -1,5 +1,6 @@ -// Created by Michael Kirk on 1/3/17. -// Copyright © 2017 Open Whisper Systems. All rights reserved. +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// import Foundation @@ -70,10 +71,15 @@ class NonCallKitCallUIAdaptee: CallUIAdaptee { } } - func toggleMute(call: SignalCall, isMuted: Bool) { + func setIsMuted(call: SignalCall, isMuted: Bool) { CallService.signalingQueue.async { - self.callService.handleToggledMute(isMuted: isMuted) + self.callService.setIsMuted(isMuted: isMuted) } } + func setHasVideo(call: SignalCall, hasVideo: Bool) { + CallService.signalingQueue.async { + self.callService.setHasVideo(hasVideo: hasVideo) + } + } } diff --git a/Signal/src/call/PeerConnectionClient.swift b/Signal/src/call/PeerConnectionClient.swift index 6d8dcd004..f264d69a0 100644 --- a/Signal/src/call/PeerConnectionClient.swift +++ b/Signal/src/call/PeerConnectionClient.swift @@ -1,5 +1,6 @@ -// Created by Michael Kirk on 11/29/16. -// Copyright © 2016 Open Whisper Systems. All rights reserved. +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// import Foundation import PromiseKit @@ -154,7 +155,7 @@ class PeerConnectionClient: NSObject, RTCPeerConnectionDelegate, RTCDataChannelD public func setVideoEnabled(enabled: Bool) { guard let videoTrack = self.videoTrack else { let action = enabled ? "enable" : "disable" - Logger.error("\(TAG)) trying to \(action) videoTack which doesn't exist") + Logger.error("\(TAG)) trying to \(action) videoTrack which doesn't exist") return } @@ -198,7 +199,7 @@ class PeerConnectionClient: NSObject, RTCPeerConnectionDelegate, RTCDataChannelD var defaultOfferConstraints: RTCMediaConstraints { let mandatoryConstraints = [ "OfferToReceiveAudio": "true", - "OfferToReceiveVideo" : "true" + "OfferToReceiveVideo": "true" ] return RTCMediaConstraints(mandatoryConstraints:mandatoryConstraints, optionalConstraints:nil) } diff --git a/Signal/src/call/SignalCall.swift b/Signal/src/call/SignalCall.swift index 18343b78f..98bb18b56 100644 --- a/Signal/src/call/SignalCall.swift +++ b/Signal/src/call/SignalCall.swift @@ -19,6 +19,7 @@ enum CallState: String { protocol CallDelegate: class { func stateDidChange(call: SignalCall, state: CallState) + func hasVideoDidChange(call: SignalCall, hasVideo: Bool) func muteDidChange(call: SignalCall, isMuted: Bool) } @@ -37,7 +38,12 @@ protocol CallDelegate: class { // Distinguishes between calls locally, e.g. in CallKit let localId: UUID - var hasVideo = false + var hasVideo = false { + didSet { + Logger.debug("\(TAG) hasVideo changed: \(oldValue) -> \(hasVideo)") + delegate?.hasVideoDidChange(call: self, hasVideo: hasVideo) + } + } var state: CallState { didSet { Logger.debug("\(TAG) state changed: \(oldValue) -> \(state)") diff --git a/Signal/src/call/Speakerbox/CallKitCallManager.swift b/Signal/src/call/Speakerbox/CallKitCallManager.swift index ee8f9165c..ff75d6af9 100644 --- a/Signal/src/call/Speakerbox/CallKitCallManager.swift +++ b/Signal/src/call/Speakerbox/CallKitCallManager.swift @@ -1,5 +1,6 @@ -// Created by Michael Kirk on 12/13/16. -// Copyright © 2016 Open Whisper Systems. All rights reserved. +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// import UIKit import CallKit @@ -48,7 +49,7 @@ final class CallKitCallManager: NSObject { requestTransaction(transaction) } - func toggleMute(call: SignalCall, isMuted: Bool) { + func setIsMuted(call: SignalCall, isMuted: Bool) { let muteCallAction = CXSetMutedCallAction(call: call.localId, muted: isMuted) let transaction = CXTransaction() transaction.addAction(muteCallAction) diff --git a/Signal/src/call/Speakerbox/CallKitCallUIAdaptee.swift b/Signal/src/call/Speakerbox/CallKitCallUIAdaptee.swift index f12c847dd..ad8d7a9b8 100644 --- a/Signal/src/call/Speakerbox/CallKitCallUIAdaptee.swift +++ b/Signal/src/call/Speakerbox/CallKitCallUIAdaptee.swift @@ -1,5 +1,6 @@ -// Created by Michael Kirk on 12/23/16. -// Copyright © 2016 Open Whisper Systems. All rights reserved. +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// import Foundation import UIKit @@ -99,8 +100,21 @@ final class CallKitCallUIAdaptee: NSObject, CallUIAdaptee, CXProviderDelegate { callManager.end(call: call) } - func toggleMute(call: SignalCall, isMuted: Bool) { - callManager.toggleMute(call: call, isMuted: isMuted) + func setIsMuted(call: SignalCall, isMuted: Bool) { + callManager.setIsMuted(call: call, isMuted: isMuted) + } + + func setHasVideo(call: SignalCall, hasVideo: Bool) { + let update = CXCallUpdate() + update.remoteHandle = CXHandle(type: .phoneNumber, value: call.remotePhoneNumber) + update.hasVideo = hasVideo + + // Update the CallKit UI. + provider.reportCall(with: call.localId, updated: update) + + CallService.signalingQueue.async { + self.callService.setHasVideo(hasVideo: hasVideo) + } } // MARK: CXProviderDelegate @@ -139,7 +153,7 @@ final class CallKitCallUIAdaptee: NSObject, CallUIAdaptee, CXProviderDelegate { CallService.signalingQueue.async { self.callService.handleOutgoingCall(call).then { action.fulfill() - }.catch { error in + }.catch { _ in self.callManager.removeCall(call) action.fail() } @@ -243,7 +257,7 @@ final class CallKitCallUIAdaptee: NSObject, CallUIAdaptee, CXProviderDelegate { } CallService.signalingQueue.async { - self.callService.handleToggledMute(isMuted: action.isMuted) + self.callService.setIsMuted(isMuted: action.isMuted) action.fulfill() } } diff --git a/Signal/src/call/UserInterface/CallUIAdapter.swift b/Signal/src/call/UserInterface/CallUIAdapter.swift index f3cb002ba..05cc90651 100644 --- a/Signal/src/call/UserInterface/CallUIAdapter.swift +++ b/Signal/src/call/UserInterface/CallUIAdapter.swift @@ -1,5 +1,6 @@ -// Created by Michael Kirk on 12/13/16. -// Copyright © 2016 Open Whisper Systems. All rights reserved. +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// import Foundation import PromiseKit @@ -15,7 +16,8 @@ protocol CallUIAdaptee { func declineCall(_ call: SignalCall) func recipientAcceptedCall(_ call: SignalCall) func endCall(_ call: SignalCall) - func toggleMute(call: SignalCall, isMuted: Bool) + func setIsMuted(call: SignalCall, isMuted: Bool) + func setHasVideo(call: SignalCall, hasVideo: Bool) } // Shared default implementations @@ -93,7 +95,11 @@ class CallUIAdapter { adaptee.showCall(call) } - internal func toggleMute(call: SignalCall, isMuted: Bool) { - adaptee.toggleMute(call: call, isMuted: isMuted) + internal func setIsMuted(call: SignalCall, isMuted: Bool) { + adaptee.setIsMuted(call: call, isMuted: isMuted) + } + + internal func setHasVideo(call: SignalCall, hasVideo: Bool) { + adaptee.setHasVideo(call: call, hasVideo: hasVideo) } } diff --git a/Signal/src/view controllers/CallViewController.swift b/Signal/src/view controllers/CallViewController.swift index a4034f679..87bbb70de 100644 --- a/Signal/src/view controllers/CallViewController.swift +++ b/Signal/src/view controllers/CallViewController.swift @@ -281,15 +281,23 @@ class CallViewController: UIViewController, CallDelegate { textMessageButton = createButton(imageName:"message-active-wide", action:#selector(didPressTextMessage)) - muteButton = createButton(imageName:"mute-active-wide", + muteButton = createButton(imageName:"mute-unselected-wide", action:#selector(didPressMute)) speakerPhoneButton = createButton(imageName:"speaker-active-wide", action:#selector(didPressSpeakerphone)) - videoButton = createButton(imageName:"video-active-wide", + videoButton = createButton(imageName:"video-inactive-wide", action:#selector(didPressVideo)) hangUpButton = createButton(imageName:"hangup-active-wide", action:#selector(didPressHangup)) + let muteSelectedImage = UIImage(named:"mute-selected-wide") + assert(muteSelectedImage != nil) + muteButton.setImage(muteSelectedImage, for:.selected) + + let videoSelectedImage = UIImage(named:"video-active-wide") + assert(videoSelectedImage != nil) + videoButton.setImage(videoSelectedImage, for:.selected) + ongoingCallView = createContainerForCallControls(controlGroups : [ [textMessageButton, videoButton], [muteButton, speakerPhoneButton ], @@ -335,6 +343,7 @@ class CallViewController: UIViewController, CallDelegate { func createButton(imageName: String, action: Selector) -> UIButton { let image = UIImage(named:imageName) + assert(image != nil) let button = UIButton() button.setImage(image, for:.normal) button.imageEdgeInsets = UIEdgeInsets(top: buttonInset(), @@ -513,6 +522,9 @@ class CallViewController: UIViewController, CallDelegate { assert(Thread.isMainThread) updateCallStatusLabel(callState: callState) + videoButton.isSelected = call.hasVideo + muteButton.isSelected = call.isMuted + // Show Incoming vs. Ongoing call controls let isRinging = callState == .localRinging incomingCallView.isHidden = !isRinging @@ -573,7 +585,7 @@ class CallViewController: UIViewController, CallDelegate { Logger.info("\(TAG) called \(#function)") muteButton.isSelected = !muteButton.isSelected if let call = self.call { - callUIAdapter.toggleMute(call: call, isMuted: muteButton.isSelected) + callUIAdapter.setIsMuted(call: call, isMuted: muteButton.isSelected) } else { Logger.warn("\(TAG) pressed mute, but call was unexpectedly nil") } @@ -608,8 +620,12 @@ class CallViewController: UIViewController, CallDelegate { func didPressVideo(sender: UIButton) { Logger.info("\(TAG) called \(#function)") - - // TODO: + videoButton.isSelected = !videoButton.isSelected + if let call = self.call { + callUIAdapter.setHasVideo(call: call, hasVideo: videoButton.isSelected) + } else { + Logger.warn("\(TAG) pressed video, but call was unexpectedly nil") + } } /** @@ -627,18 +643,25 @@ class CallViewController: UIViewController, CallDelegate { self.dismiss(animated: true) } - // MARK: - Call Delegate + // MARK: - CallDelegate internal func stateDidChange(call: SignalCall, state: CallState) { DispatchQueue.main.async { self.updateCallUI(callState: state) + Logger.info("\(self.TAG) new call status: \(call.state)") } self.audioService.handleState(state) } + internal func hasVideoDidChange(call: SignalCall, hasVideo: Bool) { + DispatchQueue.main.async { + self.updateCallUI(callState: call.state) + } + } + internal func muteDidChange(call: SignalCall, isMuted: Bool) { DispatchQueue.main.async { - self.muteButton.isSelected = call.isMuted + self.updateCallUI(callState: call.state) } } }