2017-01-12 21:55:14 +01:00
|
|
|
//
|
|
|
|
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
|
|
|
//
|
2016-11-12 18:22:29 +01:00
|
|
|
|
|
|
|
import Foundation
|
|
|
|
import WebRTC
|
|
|
|
import PromiseKit
|
|
|
|
|
2017-01-09 19:36:45 +01:00
|
|
|
// TODO move this somewhere else.
|
2016-11-12 18:22:29 +01:00
|
|
|
@objc class CallAudioService: NSObject {
|
|
|
|
private let TAG = "[CallAudioService]"
|
|
|
|
private var vibrateTimer: Timer?
|
|
|
|
private let audioManager = AppAudioManager.sharedInstance()
|
|
|
|
|
|
|
|
// Mark: Vibration config
|
|
|
|
private let vibrateRepeatDuration = 1.6
|
|
|
|
|
|
|
|
// Our ring buzz is a pair of vibrations.
|
|
|
|
// `pulseDuration` is the small pause between the two vibrations in the pair.
|
|
|
|
private let pulseDuration = 0.2
|
|
|
|
|
|
|
|
public var isSpeakerphoneEnabled = false {
|
|
|
|
didSet {
|
|
|
|
handleUpdatedSpeakerphone()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public func handleState(_ state: CallState) {
|
|
|
|
switch state {
|
|
|
|
case .idle: handleIdle()
|
|
|
|
case .dialing: handleDialing()
|
|
|
|
case .answering: handleAnswering()
|
|
|
|
case .remoteRinging: handleRemoteRinging()
|
|
|
|
case .localRinging: handleLocalRinging()
|
|
|
|
case .connected: handleConnected()
|
|
|
|
case .localFailure: handleLocalFailure()
|
|
|
|
case .localHangup: handleLocalHangup()
|
|
|
|
case .remoteHangup: handleRemoteHangup()
|
|
|
|
case .remoteBusy: handleBusy()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func handleIdle() {
|
|
|
|
Logger.debug("\(TAG) \(#function)")
|
|
|
|
}
|
|
|
|
|
|
|
|
private func handleDialing() {
|
|
|
|
Logger.debug("\(TAG) \(#function)")
|
|
|
|
}
|
|
|
|
|
|
|
|
private func handleAnswering() {
|
|
|
|
Logger.debug("\(TAG) \(#function)")
|
|
|
|
stopRinging()
|
|
|
|
}
|
|
|
|
|
|
|
|
private func handleRemoteRinging() {
|
|
|
|
Logger.debug("\(TAG) \(#function)")
|
|
|
|
}
|
|
|
|
|
|
|
|
private func handleLocalRinging() {
|
|
|
|
Logger.debug("\(TAG) \(#function)")
|
|
|
|
audioManager.setAudioEnabled(true)
|
|
|
|
audioManager.handleInboundRing()
|
2017-01-06 09:21:58 +01:00
|
|
|
do {
|
|
|
|
// Respect silent switch.
|
|
|
|
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategorySoloAmbient)
|
|
|
|
Logger.debug("\(TAG) set audio category to SoloAmbient")
|
|
|
|
} catch {
|
|
|
|
Logger.error("\(TAG) failed to change audio category to soloAmbient in \(#function)")
|
|
|
|
}
|
|
|
|
|
2016-11-12 18:22:29 +01:00
|
|
|
vibrateTimer = Timer.scheduledTimer(timeInterval: vibrateRepeatDuration, target: self, selector: #selector(vibrate), userInfo: nil, repeats: true)
|
|
|
|
}
|
|
|
|
|
|
|
|
private func handleConnected() {
|
|
|
|
Logger.debug("\(TAG) \(#function)")
|
|
|
|
stopRinging()
|
2017-01-06 09:21:58 +01:00
|
|
|
do {
|
|
|
|
// Start recording
|
|
|
|
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayAndRecord)
|
|
|
|
Logger.debug("\(TAG) set audio category to PlayAndRecord")
|
|
|
|
} catch {
|
|
|
|
Logger.error("\(TAG) failed to change audio category to soloAmbient in \(#function)")
|
|
|
|
}
|
2016-11-12 18:22:29 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
private func handleLocalFailure() {
|
|
|
|
Logger.debug("\(TAG) \(#function)")
|
|
|
|
stopRinging()
|
|
|
|
}
|
|
|
|
|
|
|
|
private func handleLocalHangup() {
|
|
|
|
Logger.debug("\(TAG) \(#function)")
|
|
|
|
stopRinging()
|
|
|
|
}
|
|
|
|
|
|
|
|
private func handleRemoteHangup() {
|
|
|
|
Logger.debug("\(TAG) \(#function)")
|
|
|
|
stopRinging()
|
|
|
|
}
|
|
|
|
|
|
|
|
private func handleBusy() {
|
|
|
|
Logger.debug("\(TAG) \(#function)")
|
|
|
|
stopRinging()
|
|
|
|
}
|
|
|
|
|
|
|
|
private func handleUpdatedSpeakerphone() {
|
|
|
|
audioManager.toggleSpeakerPhone(isEnabled: isSpeakerphoneEnabled)
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: Helpers
|
|
|
|
|
|
|
|
private func stopRinging() {
|
|
|
|
// Disables external speaker used for ringing, unless user enables speakerphone.
|
|
|
|
audioManager.setDefaultAudioProfile()
|
|
|
|
audioManager.cancelAllAudio()
|
|
|
|
|
|
|
|
vibrateTimer?.invalidate()
|
|
|
|
vibrateTimer = nil
|
|
|
|
}
|
|
|
|
|
|
|
|
public func vibrate() {
|
|
|
|
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate)
|
|
|
|
DispatchQueue.default.asyncAfter(deadline: DispatchTime.now() + pulseDuration) {
|
|
|
|
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-01-13 19:28:34 +01:00
|
|
|
// TODO: Add category so that button handlers can be defined where button is created.
|
|
|
|
// TODO: Add logic to button handlers.
|
|
|
|
// TODO: Ensure buttons enabled & disabled as necessary.
|
2016-11-12 18:22:29 +01:00
|
|
|
@objc(OWSCallViewController)
|
2017-01-09 15:28:04 +01:00
|
|
|
class CallViewController: UIViewController, CallDelegate {
|
2016-11-12 18:22:29 +01:00
|
|
|
|
|
|
|
enum CallDirection {
|
|
|
|
case unspecified, outgoing, incoming
|
|
|
|
}
|
|
|
|
|
|
|
|
let TAG = "[CallViewController]"
|
|
|
|
|
|
|
|
// Dependencies
|
2017-01-12 21:55:14 +01:00
|
|
|
|
2016-11-12 18:22:29 +01:00
|
|
|
let callUIAdapter: CallUIAdapter
|
|
|
|
let contactsManager: OWSContactsManager
|
|
|
|
let audioService: CallAudioService
|
|
|
|
|
|
|
|
// MARK: Properties
|
|
|
|
|
|
|
|
var peerConnectionClient: PeerConnectionClient?
|
|
|
|
var callDirection: CallDirection = .unspecified
|
|
|
|
var thread: TSContactThread!
|
|
|
|
var call: SignalCall!
|
|
|
|
|
2017-01-12 21:55:14 +01:00
|
|
|
// MARK: Layout
|
|
|
|
|
|
|
|
var hasConstraints = false
|
|
|
|
|
|
|
|
// MARK: Background
|
|
|
|
|
|
|
|
var blurView: UIVisualEffectView!
|
|
|
|
|
|
|
|
// MARK: Contact Views
|
2016-11-12 18:22:29 +01:00
|
|
|
|
2017-01-12 21:55:14 +01:00
|
|
|
var contactNameLabel: UILabel!
|
|
|
|
var contactAvatarView: AvatarImageView!
|
|
|
|
var callStatusLabel: UILabel!
|
2016-11-12 18:22:29 +01:00
|
|
|
|
2017-01-12 21:55:14 +01:00
|
|
|
// MARK: Ongoing Call Controls
|
|
|
|
|
|
|
|
var ongoingCallView: UIView!
|
|
|
|
|
|
|
|
var hangUpButton: UIButton!
|
|
|
|
var muteButton: UIButton!
|
|
|
|
var speakerPhoneButton: UIButton!
|
2017-01-13 21:08:33 +01:00
|
|
|
var textMessageButton: UIButton!
|
2017-01-12 21:55:14 +01:00
|
|
|
var videoButton: UIButton!
|
2016-11-12 18:22:29 +01:00
|
|
|
|
|
|
|
// MARK: Incoming Call Controls
|
|
|
|
|
2017-01-13 00:19:21 +01:00
|
|
|
var incomingCallView: UIView!
|
|
|
|
|
2017-01-12 21:55:14 +01:00
|
|
|
var acceptIncomingButton: UIButton!
|
|
|
|
var declineIncomingButton: UIButton!
|
2016-11-12 18:22:29 +01:00
|
|
|
|
2017-01-13 15:38:18 +01:00
|
|
|
// MARK: Control Groups
|
|
|
|
|
|
|
|
var allControls: [UIView] {
|
|
|
|
return incomingCallControls + ongoingCallControls
|
|
|
|
}
|
|
|
|
|
|
|
|
var incomingCallControls: [UIView] {
|
2017-01-13 21:08:33 +01:00
|
|
|
return [ acceptIncomingButton, declineIncomingButton ]
|
2017-01-13 15:38:18 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
var ongoingCallControls: [UIView] {
|
2017-01-13 21:08:33 +01:00
|
|
|
return [ muteButton, speakerPhoneButton, textMessageButton, hangUpButton, videoButton ]
|
2017-01-13 15:38:18 +01:00
|
|
|
}
|
|
|
|
|
2016-11-12 18:22:29 +01:00
|
|
|
// MARK: Initializers
|
|
|
|
|
|
|
|
required init?(coder aDecoder: NSCoder) {
|
|
|
|
contactsManager = Environment.getCurrent().contactsManager
|
2017-01-09 17:19:50 +01:00
|
|
|
let callService = Environment.getCurrent().callService!
|
2016-11-12 18:22:29 +01:00
|
|
|
callUIAdapter = callService.callUIAdapter
|
|
|
|
audioService = CallAudioService()
|
|
|
|
super.init(coder: aDecoder)
|
|
|
|
}
|
|
|
|
|
|
|
|
required init() {
|
|
|
|
contactsManager = Environment.getCurrent().contactsManager
|
2017-01-09 17:19:50 +01:00
|
|
|
let callService = Environment.getCurrent().callService!
|
2016-11-12 18:22:29 +01:00
|
|
|
callUIAdapter = callService.callUIAdapter
|
|
|
|
audioService = CallAudioService()
|
|
|
|
super.init(nibName: nil, bundle: nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
override func viewDidLoad() {
|
2017-01-12 21:55:14 +01:00
|
|
|
super.viewDidLoad()
|
2016-11-12 18:22:29 +01:00
|
|
|
|
|
|
|
guard let thread = self.thread else {
|
|
|
|
Logger.error("\(TAG) tried to show call call without specifying thread.")
|
|
|
|
showCallFailed(error: OWSErrorMakeAssertionError())
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-01-12 21:55:14 +01:00
|
|
|
createViews()
|
|
|
|
|
2016-11-12 18:22:29 +01:00
|
|
|
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.
|
|
|
|
}
|
|
|
|
|
2017-01-09 15:28:04 +01:00
|
|
|
call.delegate = self
|
|
|
|
stateDidChange(call: call, state: call.state)
|
2016-11-12 18:22:29 +01:00
|
|
|
}
|
|
|
|
|
2017-01-12 21:55:14 +01:00
|
|
|
func createViews() {
|
|
|
|
// Dark blurred background.
|
|
|
|
let blurEffect = UIBlurEffect(style: .dark)
|
|
|
|
blurView = UIVisualEffectView(effect: blurEffect)
|
|
|
|
self.view.addSubview(blurView)
|
|
|
|
|
2017-01-13 00:19:21 +01:00
|
|
|
createContactViews()
|
|
|
|
createOngoingCallControls()
|
|
|
|
createIncomingCallControls()
|
|
|
|
}
|
|
|
|
|
|
|
|
func createContactViews() {
|
2017-01-12 21:55:14 +01:00
|
|
|
contactNameLabel = UILabel()
|
|
|
|
contactNameLabel.font = UIFont.ows_lightFont(withSize:ScaleFromIPhone5To7Plus(32, 40))
|
|
|
|
contactNameLabel.textColor = UIColor.white
|
|
|
|
self.view.addSubview(contactNameLabel)
|
|
|
|
|
|
|
|
callStatusLabel = UILabel()
|
|
|
|
callStatusLabel.font = UIFont.ows_regularFont(withSize:ScaleFromIPhone5To7Plus(19, 25))
|
|
|
|
callStatusLabel.textColor = UIColor.white
|
|
|
|
self.view.addSubview(callStatusLabel)
|
|
|
|
|
|
|
|
contactAvatarView = AvatarImageView()
|
|
|
|
self.view.addSubview(contactAvatarView)
|
2017-01-13 00:19:21 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func buttonSize() -> CGFloat {
|
2017-01-13 19:05:40 +01:00
|
|
|
return ScaleFromIPhone5To7Plus(84, 108)
|
2017-01-13 00:19:21 +01:00
|
|
|
}
|
|
|
|
|
2017-01-13 21:08:33 +01:00
|
|
|
func buttonInset() -> CGFloat {
|
|
|
|
return ScaleFromIPhone5To7Plus(7, 9)
|
|
|
|
}
|
|
|
|
|
2017-01-13 00:19:21 +01:00
|
|
|
func createOngoingCallControls() {
|
2017-01-12 21:55:14 +01:00
|
|
|
|
2017-01-13 21:08:33 +01:00
|
|
|
textMessageButton = createButton(imageName:"message-active-wide",
|
|
|
|
action:#selector(didPressTextMessage))
|
2017-01-13 19:05:40 +01:00
|
|
|
muteButton = createButton(imageName:"mute-active-wide",
|
2017-01-13 21:08:33 +01:00
|
|
|
action:#selector(didPressMute))
|
2017-01-13 19:05:40 +01:00
|
|
|
speakerPhoneButton = createButton(imageName:"speaker-active-wide",
|
2017-01-13 21:08:33 +01:00
|
|
|
action:#selector(didPressSpeakerphone))
|
2017-01-13 19:05:40 +01:00
|
|
|
videoButton = createButton(imageName:"video-active-wide",
|
2017-01-13 21:08:33 +01:00
|
|
|
action:#selector(didPressVideo))
|
2017-01-13 19:05:40 +01:00
|
|
|
hangUpButton = createButton(imageName:"hangup-active-wide",
|
2017-01-13 21:08:33 +01:00
|
|
|
action:#selector(didPressHangup))
|
2017-01-12 21:55:14 +01:00
|
|
|
|
2017-01-13 00:19:21 +01:00
|
|
|
ongoingCallView = createContainerForCallControls(controlGroups : [
|
2017-01-13 21:08:33 +01:00
|
|
|
[textMessageButton, videoButton],
|
2017-01-13 00:19:21 +01:00
|
|
|
[muteButton, speakerPhoneButton ],
|
2017-01-13 15:38:18 +01:00
|
|
|
[hangUpButton ]
|
2017-01-13 00:19:21 +01:00
|
|
|
])
|
|
|
|
}
|
|
|
|
|
|
|
|
func createIncomingCallControls() {
|
2017-01-12 21:55:14 +01:00
|
|
|
|
2017-01-13 19:05:40 +01:00
|
|
|
acceptIncomingButton = createButton(imageName:"call-active-wide",
|
2017-01-13 21:08:33 +01:00
|
|
|
action:#selector(didPressAnswerCall))
|
2017-01-13 19:05:40 +01:00
|
|
|
declineIncomingButton = createButton(imageName:"hangup-active-wide",
|
2017-01-13 21:08:33 +01:00
|
|
|
action:#selector(didPressDeclineCall))
|
2017-01-13 00:19:21 +01:00
|
|
|
|
|
|
|
incomingCallView = createContainerForCallControls(controlGroups : [
|
2017-01-13 15:38:18 +01:00
|
|
|
[acceptIncomingButton, declineIncomingButton ]
|
2017-01-13 00:19:21 +01:00
|
|
|
])
|
|
|
|
}
|
|
|
|
|
|
|
|
func createContainerForCallControls(controlGroups: [[UIView]]) -> UIView {
|
|
|
|
let containerView = UIView()
|
|
|
|
self.view.addSubview(containerView)
|
|
|
|
var rows: [UIView] = []
|
|
|
|
for controlGroup in controlGroups {
|
2017-01-13 21:08:33 +01:00
|
|
|
rows.append(rowWithSubviews(subviews:controlGroup))
|
2017-01-13 00:19:21 +01:00
|
|
|
}
|
2017-01-13 19:05:40 +01:00
|
|
|
let rowspacing = ScaleFromIPhone5To7Plus(6, 7)
|
2017-01-13 15:38:18 +01:00
|
|
|
var prevRow: UIView?
|
2017-01-13 00:19:21 +01:00
|
|
|
for row in rows {
|
|
|
|
containerView.addSubview(row)
|
|
|
|
row.autoPinWidthToSuperview()
|
2017-01-13 15:38:18 +01:00
|
|
|
if prevRow != nil {
|
|
|
|
row.autoPinEdge(.top, to:.bottom, of:prevRow!, withOffset:rowspacing)
|
2017-01-13 00:19:21 +01:00
|
|
|
}
|
2017-01-13 15:38:18 +01:00
|
|
|
prevRow = row
|
2017-01-13 00:19:21 +01:00
|
|
|
}
|
2017-01-12 21:55:14 +01:00
|
|
|
|
2017-01-13 00:19:21 +01:00
|
|
|
containerView.setContentHuggingVerticalHigh()
|
|
|
|
rows.first!.autoPinEdge(toSuperviewEdge:.top)
|
|
|
|
rows.last!.autoPinEdge(toSuperviewEdge:.bottom)
|
|
|
|
return containerView
|
2017-01-12 21:55:14 +01:00
|
|
|
}
|
|
|
|
|
2017-01-13 21:08:33 +01:00
|
|
|
func createButton(imageName: String, action: Selector) -> UIButton {
|
2017-01-12 21:55:14 +01:00
|
|
|
let image = UIImage(named:imageName)
|
|
|
|
let button = UIButton()
|
|
|
|
button.setImage(image, for:.normal)
|
2017-01-13 21:08:33 +01:00
|
|
|
button.imageEdgeInsets = UIEdgeInsetsMake(buttonInset(),
|
|
|
|
buttonInset(),
|
|
|
|
buttonInset(),
|
|
|
|
buttonInset())
|
2017-01-12 21:55:14 +01:00
|
|
|
button.addTarget(self, action:action, for:.touchUpInside)
|
2017-01-13 21:08:33 +01:00
|
|
|
button.autoSetDimension(.width, toSize:buttonSize())
|
|
|
|
button.autoSetDimension(.height, toSize:buttonSize())
|
2017-01-12 21:55:14 +01:00
|
|
|
return button
|
|
|
|
}
|
|
|
|
|
2017-01-13 15:38:18 +01:00
|
|
|
// Creates a row containing a given set of subviews.
|
2017-01-13 21:08:33 +01:00
|
|
|
func rowWithSubviews(subviews: [UIView]) -> UIView {
|
2017-01-12 21:55:14 +01:00
|
|
|
let row = UIView()
|
|
|
|
row.setContentHuggingVerticalHigh()
|
2017-01-13 21:08:33 +01:00
|
|
|
row.autoSetDimension(.height, toSize:buttonSize())
|
2017-01-12 21:55:14 +01:00
|
|
|
|
|
|
|
if subviews.count > 1 {
|
2017-01-13 15:38:18 +01:00
|
|
|
// If there's more than one subview in the row,
|
|
|
|
// space them evenly within the row.
|
2017-01-12 21:55:14 +01:00
|
|
|
var lastSubview: UIView?
|
|
|
|
var lastSpacer: UIView?
|
|
|
|
for subview in subviews {
|
|
|
|
row.addSubview(subview)
|
|
|
|
subview.setContentHuggingHorizontalHigh()
|
|
|
|
subview.autoVCenterInSuperview()
|
|
|
|
|
|
|
|
if lastSubview != nil {
|
|
|
|
let spacer = UIView()
|
|
|
|
spacer.isHidden = true
|
|
|
|
row.addSubview(spacer)
|
|
|
|
spacer.autoPinEdge(.left, to:.right, of:lastSubview!)
|
|
|
|
spacer.autoPinEdge(.right, to:.left, of:subview)
|
|
|
|
spacer.setContentHuggingHorizontalLow()
|
|
|
|
spacer.autoVCenterInSuperview()
|
|
|
|
if lastSpacer != nil {
|
2017-01-13 15:38:18 +01:00
|
|
|
spacer.autoMatch(.width, to:.width, of:lastSpacer!)
|
2017-01-12 21:55:14 +01:00
|
|
|
}
|
|
|
|
lastSpacer = spacer
|
|
|
|
}
|
|
|
|
|
|
|
|
lastSubview = subview
|
|
|
|
}
|
|
|
|
subviews.first!.autoPinEdge(toSuperviewEdge:.left)
|
|
|
|
subviews.last!.autoPinEdge(toSuperviewEdge:.right)
|
|
|
|
} else if subviews.count == 1 {
|
2017-01-13 15:38:18 +01:00
|
|
|
// If there's only one subview in this row, center it.
|
2017-01-12 21:55:14 +01:00
|
|
|
let subview = subviews.first!
|
|
|
|
row.addSubview(subview)
|
|
|
|
subview.autoCenterInSuperview()
|
|
|
|
}
|
|
|
|
|
|
|
|
return row
|
|
|
|
}
|
|
|
|
|
|
|
|
override func updateViewConstraints() {
|
2017-01-12 23:55:44 +01:00
|
|
|
if !hasConstraints {
|
2017-01-12 21:55:14 +01:00
|
|
|
// We only want to create our constraints once.
|
|
|
|
//
|
|
|
|
// Note that constraints are also created elsewhere.
|
|
|
|
// This only creates the constraints for the top-level contents of the view.
|
|
|
|
hasConstraints = true
|
|
|
|
|
|
|
|
let topMargin = CGFloat(40)
|
|
|
|
let contactHMargin = CGFloat(30)
|
|
|
|
let contactVSpacing = CGFloat(3)
|
2017-01-13 19:05:40 +01:00
|
|
|
let ongoingHMargin = ScaleFromIPhone5To7Plus(46, 72)
|
|
|
|
let incomingHMargin = ScaleFromIPhone5To7Plus(46, 72)
|
|
|
|
let ongoingBottomMargin = ScaleFromIPhone5To7Plus(23, 41)
|
|
|
|
let incomingBottomMargin = CGFloat(41)
|
|
|
|
let avatarTopSpacing = ScaleFromIPhone5To7Plus(25, 50)
|
|
|
|
// The buttons have built-in 10% margins, so to appear centered
|
|
|
|
// the avatar's bottom spacing should be a bit less.
|
|
|
|
let avatarBottomSpacing = ScaleFromIPhone5To7Plus(18, 41)
|
2017-01-12 21:55:14 +01:00
|
|
|
|
|
|
|
// Dark blurred background.
|
|
|
|
blurView.autoPinEdgesToSuperviewEdges()
|
|
|
|
|
|
|
|
contactNameLabel.autoPinEdge(toSuperviewEdge:.top, withInset:topMargin)
|
|
|
|
contactNameLabel.autoPinWidthToSuperview(withMargin:contactHMargin)
|
|
|
|
contactNameLabel.setContentHuggingVerticalHigh()
|
|
|
|
|
|
|
|
callStatusLabel.autoPinEdge(.top, to:.bottom, of:contactNameLabel, withOffset:contactVSpacing)
|
|
|
|
callStatusLabel.autoPinWidthToSuperview(withMargin:contactHMargin)
|
|
|
|
callStatusLabel.setContentHuggingVerticalHigh()
|
|
|
|
|
2017-01-13 19:05:40 +01:00
|
|
|
contactAvatarView.autoPinEdge(.top, to:.bottom, of:callStatusLabel, withOffset:+avatarTopSpacing)
|
|
|
|
contactAvatarView.autoPinEdge(.bottom, to:.top, of:ongoingCallView, withOffset:-avatarBottomSpacing)
|
2017-01-12 21:55:14 +01:00
|
|
|
contactAvatarView.autoHCenterInSuperview()
|
|
|
|
// Stretch that avatar to fill the available space.
|
|
|
|
contactAvatarView.setContentHuggingLow()
|
|
|
|
contactAvatarView.setCompressionResistanceLow()
|
|
|
|
// Preserve square aspect ratio of contact avatar.
|
|
|
|
contactAvatarView.autoMatch(.width, to:.height, of:contactAvatarView)
|
|
|
|
|
|
|
|
// Ongoing call controls
|
|
|
|
ongoingCallView.autoPinEdge(toSuperviewEdge:.bottom, withInset:ongoingBottomMargin)
|
|
|
|
ongoingCallView.autoPinWidthToSuperview(withMargin:ongoingHMargin)
|
|
|
|
ongoingCallView.setContentHuggingVerticalHigh()
|
|
|
|
|
|
|
|
// Incoming call controls
|
2017-01-13 00:19:21 +01:00
|
|
|
incomingCallView.autoPinEdge(toSuperviewEdge:.bottom, withInset:incomingBottomMargin)
|
|
|
|
incomingCallView.autoPinWidthToSuperview(withMargin:incomingHMargin)
|
|
|
|
incomingCallView.setContentHuggingVerticalHigh()
|
2017-01-12 21:55:14 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
super.updateViewConstraints()
|
|
|
|
}
|
|
|
|
|
2016-11-12 18:22:29 +01:00
|
|
|
// 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)")
|
|
|
|
}
|
|
|
|
|
|
|
|
func localizedTextForCallState(_ callState: CallState) -> String {
|
|
|
|
switch callState {
|
|
|
|
case .idle, .remoteHangup, .localHangup:
|
|
|
|
return NSLocalizedString("IN_CALL_TERMINATED", comment: "Call setup status label")
|
|
|
|
case .dialing:
|
|
|
|
return NSLocalizedString("IN_CALL_CONNECTING", comment: "Call setup status label")
|
|
|
|
case .remoteRinging, .localRinging:
|
|
|
|
return NSLocalizedString("IN_CALL_RINGING", comment: "Call setup status label")
|
|
|
|
case .answering:
|
|
|
|
return NSLocalizedString("IN_CALL_SECURING", comment: "Call setup status label")
|
|
|
|
case .connected:
|
|
|
|
return NSLocalizedString("IN_CALL_TALKING", comment: "Call setup status label")
|
|
|
|
case .remoteBusy:
|
|
|
|
return NSLocalizedString("END_CALL_RESPONDER_IS_BUSY", comment: "Call setup status label")
|
|
|
|
case .localFailure:
|
|
|
|
return NSLocalizedString("END_CALL_UNCATEGORIZED_FAILURE", comment: "Call setup status label")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func updateCallUI(callState: CallState) {
|
|
|
|
let textForState = localizedTextForCallState(callState)
|
|
|
|
Logger.info("\(TAG) new call status: \(callState) aka \"\(textForState)\"")
|
|
|
|
|
|
|
|
self.callStatusLabel.text = textForState
|
|
|
|
|
|
|
|
// Show Incoming vs. (Outgoing || Accepted) call controls
|
2017-01-12 21:55:14 +01:00
|
|
|
let isRinging = callState == .localRinging
|
2017-01-13 15:38:18 +01:00
|
|
|
for subview in allControls {
|
2017-01-12 21:55:14 +01:00
|
|
|
if isRinging {
|
|
|
|
// Show incoming controls
|
2017-01-13 15:38:18 +01:00
|
|
|
let isIncomingCallControl = incomingCallControls.contains(subview)
|
2017-01-12 21:55:14 +01:00
|
|
|
subview.isHidden = !isIncomingCallControl
|
|
|
|
} else {
|
|
|
|
// Show ongoing controls
|
2017-01-13 15:38:18 +01:00
|
|
|
let isOngoingCallControl = ongoingCallControls.contains(subview)
|
2017-01-12 21:55:14 +01:00
|
|
|
subview.isHidden = !isOngoingCallControl
|
|
|
|
}
|
|
|
|
}
|
2016-11-12 18:22:29 +01:00
|
|
|
|
|
|
|
// Dismiss Handling
|
|
|
|
switch callState {
|
|
|
|
case .remoteHangup, .remoteBusy, .localFailure:
|
|
|
|
Logger.debug("\(TAG) dismissing after delay because new state is \(textForState)")
|
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
|
|
|
|
self.dismiss(animated: true)
|
|
|
|
}
|
|
|
|
case .localHangup:
|
|
|
|
Logger.debug("\(TAG) dismissing immediately from local hangup")
|
|
|
|
self.dismiss(animated: true)
|
|
|
|
|
|
|
|
default: break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: - Actions
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Ends a connected call. Do not confuse with `didPressDeclineCall`.
|
|
|
|
*/
|
2017-01-12 21:55:14 +01:00
|
|
|
func didPressHangup(sender: UIButton) {
|
2016-11-12 18:22:29 +01:00
|
|
|
Logger.info("\(TAG) called \(#function)")
|
|
|
|
if let call = self.call {
|
|
|
|
callUIAdapter.endCall(call)
|
|
|
|
} else {
|
|
|
|
Logger.warn("\(TAG) hung up, but call was unexpectedly nil")
|
|
|
|
}
|
|
|
|
|
|
|
|
self.dismiss(animated: true)
|
|
|
|
}
|
|
|
|
|
2017-01-12 21:55:14 +01:00
|
|
|
func didPressMute(sender muteButton: UIButton) {
|
2016-11-12 18:22:29 +01:00
|
|
|
Logger.info("\(TAG) called \(#function)")
|
|
|
|
muteButton.isSelected = !muteButton.isSelected
|
2017-01-09 15:28:04 +01:00
|
|
|
if let call = self.call {
|
|
|
|
callUIAdapter.toggleMute(call: call, isMuted: muteButton.isSelected)
|
|
|
|
} else {
|
2017-01-09 17:19:50 +01:00
|
|
|
Logger.warn("\(TAG) pressed mute, but call was unexpectedly nil")
|
2016-11-12 18:22:29 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-01-12 21:55:14 +01:00
|
|
|
func didPressSpeakerphone(sender speakerphoneButton: UIButton) {
|
2016-11-12 18:22:29 +01:00
|
|
|
Logger.info("\(TAG) called \(#function)")
|
|
|
|
speakerphoneButton.isSelected = !speakerphoneButton.isSelected
|
|
|
|
audioService.isSpeakerphoneEnabled = speakerphoneButton.isSelected
|
|
|
|
}
|
|
|
|
|
2017-01-12 21:55:14 +01:00
|
|
|
func didPressTextMessage(sender speakerphoneButton: UIButton) {
|
|
|
|
Logger.info("\(TAG) called \(#function)")
|
|
|
|
}
|
|
|
|
|
|
|
|
func didPressAnswerCall(sender: UIButton) {
|
2016-11-12 18:22:29 +01:00
|
|
|
Logger.info("\(TAG) called \(#function)")
|
|
|
|
|
|
|
|
guard let call = self.call else {
|
|
|
|
Logger.error("\(TAG) call was unexpectedly nil. Terminating call.")
|
|
|
|
self.callStatusLabel.text = NSLocalizedString("END_CALL_UNCATEGORIZED_FAILURE", comment: "Call setup status label")
|
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
|
|
|
|
self.dismiss(animated: true)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-01-09 17:19:50 +01:00
|
|
|
callUIAdapter.answerCall(call)
|
2016-11-12 18:22:29 +01:00
|
|
|
}
|
|
|
|
|
2017-01-12 21:55:14 +01:00
|
|
|
func didPressVideo(sender: UIButton) {
|
|
|
|
Logger.info("\(TAG) called \(#function)")
|
|
|
|
}
|
|
|
|
|
2016-11-12 18:22:29 +01:00
|
|
|
/**
|
|
|
|
* Denies an incoming not-yet-connected call, Do not confuse with `didPressHangup`.
|
|
|
|
*/
|
2017-01-12 21:55:14 +01:00
|
|
|
func didPressDeclineCall(sender: UIButton) {
|
2016-11-12 18:22:29 +01:00
|
|
|
Logger.info("\(TAG) called \(#function)")
|
|
|
|
|
|
|
|
if let call = self.call {
|
|
|
|
callUIAdapter.declineCall(call)
|
|
|
|
} else {
|
|
|
|
Logger.warn("\(TAG) denied call, but call was unexpectedly nil")
|
|
|
|
}
|
|
|
|
|
|
|
|
self.dismiss(animated: true)
|
|
|
|
}
|
2017-01-09 15:28:04 +01:00
|
|
|
|
|
|
|
// MARK: - Call Delegate
|
|
|
|
|
|
|
|
internal func stateDidChange(call: SignalCall, state: CallState) {
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
self.updateCallUI(callState: state)
|
|
|
|
}
|
|
|
|
self.audioService.handleState(state)
|
|
|
|
}
|
|
|
|
|
|
|
|
internal func muteDidChange(call: SignalCall, isMuted: Bool) {
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
self.muteButton.isSelected = call.isMuted
|
|
|
|
}
|
|
|
|
}
|
2016-11-12 18:22:29 +01:00
|
|
|
}
|