2017-01-12 21:55:14 +01:00
//
2018-01-10 16:54:17 +01:00
// C o p y r i g h t ( c ) 2 0 1 8 O p e n W h i s p e r S y s t e m s . A l l r i g h t s r e s e r v e d .
2017-01-12 21:55:14 +01:00
//
2016-11-12 18:22:29 +01:00
import Foundation
import WebRTC
import PromiseKit
2017-11-28 00:17:46 +01:00
import SignalServiceKit
2017-12-01 16:48:18 +01:00
import SignalMessaging
2016-11-12 18:22:29 +01:00
2017-01-13 19:28:34 +01:00
// TODO: A d d c a t e g o r y s o t h a t b u t t o n h a n d l e r s c a n b e d e f i n e d w h e r e b u t t o n i s c r e a t e d .
// TODO: E n s u r e b u t t o n s e n a b l e d & d i s a b l e d a s n e c e s s a r y .
2018-02-03 00:35:32 +01:00
class CallViewController : OWSViewController , CallObserver , CallServiceObserver , CallAudioServiceDelegate {
2016-11-12 18:22:29 +01:00
let TAG = " [CallViewController] "
// D e p e n d e n c i e s
2018-03-08 00:09:07 +01:00
var callUIAdapter : CallUIAdapter {
return SignalApp . shared ( ) . callUIAdapter
}
2018-03-14 14:02:44 +01:00
2018-04-18 17:23:57 +02:00
// F e a t u r e F l a g
@objc public static let kShowCallViewOnSeparateWindow = true
2016-11-12 18:22:29 +01:00
let contactsManager : OWSContactsManager
2018-04-24 23:00:39 +02:00
// MARK: - P r o p e r t i e s
2016-11-12 18:22:29 +01:00
2017-07-13 16:41:39 +02:00
let thread : TSContactThread
let call : SignalCall
2017-02-27 20:37:42 +01:00
var hasDismissed = false
2016-11-12 18:22:29 +01:00
2018-04-24 23:00:39 +02:00
// MARK: - V i e w s
2017-01-12 21:55:14 +01:00
var hasConstraints = false
2018-04-18 20:39:59 +02:00
var blurView : UIVisualEffectView !
2017-01-17 16:04:51 +01:00
var dateFormatter : DateFormatter ?
2017-01-12 21:55:14 +01:00
2018-04-24 23:00:39 +02:00
// MARK: - C o n t a c t V i e w s
2016-11-12 18:22:29 +01:00
2018-04-18 20:39:59 +02:00
var contactNameLabel : MarqueeLabel !
var contactAvatarView : AvatarImageView !
var contactAvatarContainerView : UIView !
var callStatusLabel : UILabel !
2017-01-17 16:04:51 +01:00
var callDurationTimer : Timer ?
2016-11-12 18:22:29 +01:00
2018-04-24 23:00:39 +02:00
// MARK: - O n g o i n g C a l l C o n t r o l s
2017-01-12 21:55:14 +01:00
2018-04-23 22:12:23 +02:00
var ongoingCallControls : UIStackView !
var ongoingAudioCallControls : UIStackView !
var ongoingVideoCallControls : UIStackView !
2017-01-12 21:55:14 +01:00
2018-04-18 20:39:59 +02:00
var hangUpButton : UIButton !
var audioSourceButton : UIButton !
var audioModeMuteButton : UIButton !
var audioModeVideoButton : UIButton !
var videoModeMuteButton : UIButton !
var videoModeVideoButton : UIButton !
2018-04-23 22:12:23 +02:00
var videoModeFlipCameraButton : UIButton !
2016-11-12 18:22:29 +01:00
2018-04-24 23:00:39 +02:00
// MARK: - I n c o m i n g C a l l C o n t r o l s
2016-11-12 18:22:29 +01:00
2018-04-23 22:12:23 +02:00
var incomingCallControls : UIStackView !
2017-01-13 00:19:21 +01:00
2018-04-18 20:39:59 +02:00
var acceptIncomingButton : UIButton !
var declineIncomingButton : UIButton !
2016-11-12 18:22:29 +01:00
2018-04-24 23:00:39 +02:00
// MARK: - V i d e o V i e w s
2017-01-26 16:05:41 +01:00
2018-04-18 20:39:59 +02:00
var remoteVideoView : RemoteVideoView !
var localVideoView : RTCCameraPreviewView !
2017-01-26 16:05:41 +01:00
weak var localVideoTrack : RTCVideoTrack ?
weak var remoteVideoTrack : RTCVideoTrack ?
2017-01-26 17:33:42 +01:00
var localVideoConstraints : [ NSLayoutConstraint ] = [ ]
2017-01-26 16:05:41 +01:00
2018-04-17 01:01:00 +02:00
override public var canBecomeFirstResponder : Bool {
return true
}
2017-01-30 20:06:57 +01:00
var shouldRemoteVideoControlsBeHidden = false {
2017-01-27 21:26:31 +01:00
didSet {
updateCallUI ( callState : call . state )
}
}
2018-04-24 23:00:39 +02:00
// MARK: - S e t t i n g s N a g V i e w s
2017-02-27 20:37:42 +01:00
var isShowingSettingsNag = false {
didSet {
if oldValue != isShowingSettingsNag {
updateCallUI ( callState : call . state )
}
}
}
2018-04-18 20:39:59 +02:00
var settingsNagView : UIView !
var settingsNagDescriptionLabel : UILabel !
2017-02-27 20:37:42 +01:00
2018-04-24 23:00:39 +02:00
// MARK: - A u d i o S o u r c e
2017-07-13 21:03:42 +02:00
var hasAlternateAudioSources : Bool {
2017-07-14 22:13:30 +02:00
Logger . info ( " \( TAG ) available audio sources: \( allAudioSources ) " )
2017-07-12 21:51:07 +02:00
// i n t e r n a l m i c a n d s p e a k e r p h o n e w i l l b e t h e f i r s t t w o , a n y m o r e t h a n o n e i n d i c a t e s e . g . a n a t t a c h e d b l u e t o o t h d e v i c e .
2017-07-13 21:03:42 +02:00
2017-07-12 21:51:07 +02:00
// T O D O i s t h i s s u f f i c i e n t ? A r e t h e i r d e v i c e s w / b l u e t o o t h b u t n o e x t e r n a l s p e a k e r ? e . g . i p o d ?
2017-07-13 21:03:42 +02:00
return allAudioSources . count > 2
2017-07-12 21:51:07 +02:00
}
2018-03-08 00:09:07 +01:00
var allAudioSources : Set < AudioSource > = Set ( )
2017-07-12 21:51:07 +02:00
2017-07-13 21:03:42 +02:00
var appropriateAudioSources : Set < AudioSource > {
2017-07-12 21:51:07 +02:00
if call . hasLocalVideo {
2017-07-13 21:03:42 +02:00
let appropriateForVideo = allAudioSources . filter { audioSource in
2017-07-12 21:51:07 +02:00
if audioSource . isBuiltInSpeaker {
return true
} else {
guard let portDescription = audioSource . portDescription else {
owsFail ( " Only built in speaker should be lacking a port description. " )
return false
}
2017-07-13 21:03:42 +02:00
// D o n ' t u s e r e c e i v e r w h e n v i d e o i s e n a b l e d . O n l y b l u e t o o t h o r s p e a k e r
2017-07-12 21:51:07 +02:00
return portDescription . portType != AVAudioSessionPortBuiltInMic
}
}
2017-07-13 21:03:42 +02:00
return Set ( appropriateForVideo )
2017-07-12 21:51:07 +02:00
} else {
2017-07-13 21:03:42 +02:00
return allAudioSources
2017-07-12 21:51:07 +02:00
}
}
2018-04-24 23:00:39 +02:00
// MARK: - I n i t i a l i z e r s
2016-11-12 18:22:29 +01:00
2017-07-13 16:41:39 +02:00
@ available ( * , unavailable , message : " use init(call:) constructor instead. " )
2016-11-12 18:22:29 +01:00
required init ? ( coder aDecoder : NSCoder ) {
2018-03-08 00:09:07 +01:00
fatalError ( " Unimplemented " )
2016-11-12 18:22:29 +01:00
}
2017-07-13 16:41:39 +02:00
required init ( call : SignalCall ) {
2017-12-04 16:35:47 +01:00
contactsManager = Environment . current ( ) . contactsManager
2017-07-13 16:41:39 +02:00
self . call = call
self . thread = TSContactThread . getOrCreateThread ( contactId : call . remotePhoneNumber )
2016-11-12 18:22:29 +01:00
super . init ( nibName : nil , bundle : nil )
2018-03-14 14:02:44 +01:00
2018-03-08 00:09:07 +01:00
allAudioSources = Set ( callUIAdapter . audioService . availableInputs )
2017-02-03 20:03:29 +01:00
}
deinit {
NotificationCenter . default . removeObserver ( self )
}
func didBecomeActive ( ) {
2017-08-31 18:46:19 +02:00
if ( self . isViewLoaded ) {
shouldRemoteVideoControlsBeHidden = false
}
2016-11-12 18:22:29 +01:00
}
2018-04-24 23:00:39 +02:00
// MARK: - V i e w L i f e c y c l e
2017-01-17 16:04:51 +01:00
override func viewDidDisappear ( _ animated : Bool ) {
super . viewDidDisappear ( animated )
2017-03-07 17:17:21 +01:00
UIDevice . current . isProximityMonitoringEnabled = false
2017-01-17 16:04:51 +01:00
callDurationTimer ? . invalidate ( )
callDurationTimer = nil
}
override func viewWillAppear ( _ animated : Bool ) {
super . viewWillAppear ( animated )
2017-03-07 17:17:21 +01:00
UIDevice . current . isProximityMonitoringEnabled = true
2017-01-17 16:04:51 +01:00
updateCallUI ( callState : call . state )
2018-04-17 01:01:00 +02:00
self . becomeFirstResponder ( )
}
override func viewDidAppear ( _ animated : Bool ) {
super . viewDidAppear ( animated )
self . becomeFirstResponder ( )
2017-01-17 16:04:51 +01:00
}
2017-09-01 20:27:50 +02:00
override func loadView ( ) {
self . view = UIView ( )
2018-04-23 22:12:23 +02:00
self . view . layoutMargins = UIEdgeInsets ( top : 16 , left : 20 , bottom : 16 , right : 20 )
2017-09-01 20:27:50 +02:00
createViews ( )
2018-04-23 22:12:23 +02:00
createViewConstraints ( )
2017-09-01 20:27:50 +02:00
}
2016-11-12 18:22:29 +01:00
override func viewDidLoad ( ) {
2017-01-12 21:55:14 +01:00
super . viewDidLoad ( )
2016-11-12 18:22:29 +01:00
2017-09-07 22:43:52 +02:00
contactNameLabel . text = contactsManager . stringForConversationTitle ( withPhoneIdentifier : thread . contactIdentifier ( ) )
2017-06-20 22:28:35 +02:00
updateAvatarImage ( )
2017-07-13 00:04:49 +02:00
NotificationCenter . default . addObserver ( forName : . OWSContactsManagerSignalAccountsDidChange , object : nil , queue : nil ) { [ weak self ] _ in
guard let strongSelf = self else { return }
Logger . info ( " \( strongSelf . TAG ) updating avatar image " )
strongSelf . updateAvatarImage ( )
2017-06-20 22:28:35 +02:00
}
2016-11-12 18:22:29 +01:00
2017-01-19 15:38:50 +01:00
// S u b s c r i b e f o r f u t u r e c a l l u p d a t e s
call . addObserverAndSyncState ( observer : self )
2017-01-26 16:05:41 +01:00
2017-12-04 16:35:47 +01:00
SignalApp . shared ( ) . callService . addObserverAndSyncState ( observer : self )
2018-04-18 21:39:35 +02:00
assert ( callUIAdapter . audioService . delegate = = nil )
callUIAdapter . audioService . delegate = self
NotificationCenter . default . addObserver ( self ,
selector : #selector ( didBecomeActive ) ,
name : NSNotification . Name . OWSApplicationDidBecomeActive ,
object : nil )
2016-11-12 18:22:29 +01:00
}
2017-01-26 16:05:41 +01:00
// MARK: - C r e a t e V i e w s
2017-01-12 21:55:14 +01:00
func createViews ( ) {
2017-01-27 21:26:31 +01:00
self . view . isUserInteractionEnabled = true
2018-03-14 14:02:44 +01:00
self . view . addGestureRecognizer ( OWSAnyTouchGestureRecognizer ( target : self ,
action : #selector ( didTouchRootView ) ) )
2017-01-27 21:26:31 +01:00
2017-01-12 21:55:14 +01:00
// D a r k b l u r r e d b a c k g r o u n d .
2018-04-18 20:39:50 +02:00
let blurEffect = UIBlurEffect ( style : . dark )
2018-04-18 20:39:59 +02:00
blurView = UIVisualEffectView ( effect : blurEffect )
2017-01-27 21:26:31 +01:00
blurView . isUserInteractionEnabled = false
2017-01-12 21:55:14 +01:00
self . view . addSubview ( blurView )
2017-07-11 19:07:24 +02:00
self . view . setHLayoutMargins ( 0 )
2017-01-26 16:05:41 +01:00
// C r e a t e t h e v i d e o v i e w s f i r s t , a s t h e y a r e u n d e r t h e o t h e r v i e w s .
createVideoViews ( )
2017-01-13 00:19:21 +01:00
createContactViews ( )
createOngoingCallControls ( )
createIncomingCallControls ( )
2017-02-27 20:37:42 +01:00
createSettingsNagViews ( )
2017-01-13 00:19:21 +01:00
}
2017-01-27 21:26:31 +01:00
func didTouchRootView ( sender : UIGestureRecognizer ) {
if ! remoteVideoView . isHidden {
2017-01-30 20:06:57 +01:00
shouldRemoteVideoControlsBeHidden = ! shouldRemoteVideoControlsBeHidden
2017-01-27 21:26:31 +01:00
}
}
2017-01-26 16:05:41 +01:00
func createVideoViews ( ) {
2018-04-18 20:39:59 +02:00
remoteVideoView = RemoteVideoView ( )
2017-10-03 23:26:14 +02:00
remoteVideoView . isUserInteractionEnabled = false
2018-04-18 20:39:59 +02:00
localVideoView = RTCCameraPreviewView ( )
2017-10-03 23:26:14 +02:00
2017-01-26 16:05:41 +01:00
remoteVideoView . isHidden = true
localVideoView . isHidden = true
self . view . addSubview ( remoteVideoView )
self . view . addSubview ( localVideoView )
}
2017-01-13 00:19:21 +01:00
func createContactViews ( ) {
2018-04-18 20:39:59 +02:00
contactNameLabel = MarqueeLabel ( )
2017-09-07 23:05:26 +02:00
// m a r q u e e c o n f i g
contactNameLabel . type = . continuous
// T h i s f e e l s p r e t t y s l o w w h e n y o u ' r e i n i t i a l l y w a i t i n g f o r i t , b u t w h e n y o u ' r e o v e r l a y i n g v i d e o c a l l s , a n y t h i n g f a s t e r i s d i s t r a c t i n g .
contactNameLabel . speed = . duration ( 30.0 )
2017-09-09 00:03:48 +02:00
contactNameLabel . animationCurve = . linear
2017-09-07 23:05:26 +02:00
contactNameLabel . fadeLength = 10.0
contactNameLabel . animationDelay = 5
// A d d t r a i l i n g s p a c e a f t e r t h e n a m e s c r o l l s b e f o r e i t w r a p s a r o u n d a n d s c r o l l s b a c k i n .
contactNameLabel . trailingBuffer = ScaleFromIPhone5 ( 80.0 )
// l a b e l c o n f i g
2018-03-14 14:02:44 +01:00
contactNameLabel . font = UIFont . ows_lightFont ( withSize : ScaleFromIPhone5To7Plus ( 32 , 40 ) )
2017-01-12 21:55:14 +01:00
contactNameLabel . textColor = UIColor . white
2017-01-31 00:27:52 +01:00
contactNameLabel . layer . shadowOffset = CGSize . zero
2017-01-31 00:16:54 +01:00
contactNameLabel . layer . shadowOpacity = 0.35
contactNameLabel . layer . shadowRadius = 4
2017-09-07 23:05:26 +02:00
2017-01-12 21:55:14 +01:00
self . view . addSubview ( contactNameLabel )
2018-04-18 20:39:59 +02:00
callStatusLabel = UILabel ( )
2018-03-14 14:02:44 +01:00
callStatusLabel . font = UIFont . ows_regularFont ( withSize : ScaleFromIPhone5To7Plus ( 19 , 25 ) )
2017-01-12 21:55:14 +01:00
callStatusLabel . textColor = UIColor . white
2017-01-31 00:27:52 +01:00
callStatusLabel . layer . shadowOffset = CGSize . zero
2017-01-31 00:16:54 +01:00
callStatusLabel . layer . shadowOpacity = 0.35
callStatusLabel . layer . shadowRadius = 4
2017-01-12 21:55:14 +01:00
self . view . addSubview ( callStatusLabel )
2018-04-18 20:39:59 +02:00
contactAvatarContainerView = UIView . container ( )
2017-11-08 00:36:29 +01:00
self . view . addSubview ( contactAvatarContainerView )
2018-04-18 20:39:59 +02:00
contactAvatarView = AvatarImageView ( )
2017-11-08 00:36:29 +01:00
contactAvatarContainerView . addSubview ( contactAvatarView )
2017-01-13 00:19:21 +01:00
}
2017-02-27 20:37:42 +01:00
func createSettingsNagViews ( ) {
2018-04-18 20:39:59 +02:00
settingsNagView = UIView ( )
2017-02-27 20:37:42 +01:00
settingsNagView . isHidden = true
self . view . addSubview ( settingsNagView )
let viewStack = UIView ( )
settingsNagView . addSubview ( viewStack )
viewStack . autoPinWidthToSuperview ( )
viewStack . autoVCenterInSuperview ( )
2018-04-18 20:39:59 +02:00
settingsNagDescriptionLabel = UILabel ( )
2017-02-27 20:37:42 +01:00
settingsNagDescriptionLabel . text = NSLocalizedString ( " CALL_VIEW_SETTINGS_NAG_DESCRIPTION_ALL " ,
comment : " Reminder to the user of the benefits of enabling CallKit and disabling CallKit privacy. " )
2018-03-14 14:02:44 +01:00
settingsNagDescriptionLabel . font = UIFont . ows_regularFont ( withSize : ScaleFromIPhone5To7Plus ( 16 , 18 ) )
2017-02-27 20:37:42 +01:00
settingsNagDescriptionLabel . textColor = UIColor . white
settingsNagDescriptionLabel . numberOfLines = 0
settingsNagDescriptionLabel . lineBreakMode = . byWordWrapping
viewStack . addSubview ( settingsNagDescriptionLabel )
settingsNagDescriptionLabel . autoPinWidthToSuperview ( )
2018-03-14 14:02:44 +01:00
settingsNagDescriptionLabel . autoPinEdge ( toSuperviewEdge : . top )
2017-02-27 20:37:42 +01:00
let buttonHeight = ScaleFromIPhone5To7Plus ( 35 , 45 )
let descriptionVSpacingHeight = ScaleFromIPhone5To7Plus ( 30 , 60 )
2018-03-14 14:02:44 +01:00
let callSettingsButton = OWSFlatButton . button ( title : NSLocalizedString ( " CALL_VIEW_SETTINGS_NAG_SHOW_CALL_SETTINGS " ,
2018-04-18 20:39:37 +02:00
comment : " Label for button that shows the privacy settings. " ) ,
2018-03-14 14:02:44 +01:00
font : OWSFlatButton . fontForHeight ( buttonHeight ) ,
titleColor : UIColor . white ,
backgroundColor : UIColor . ows_signalBrandBlue ,
target : self ,
selector : #selector ( didPressShowCallSettings ) )
2017-02-27 20:37:42 +01:00
viewStack . addSubview ( callSettingsButton )
2018-03-14 14:02:44 +01:00
callSettingsButton . autoSetDimension ( . height , toSize : buttonHeight )
2017-02-27 20:37:42 +01:00
callSettingsButton . autoPinWidthToSuperview ( )
2018-03-14 14:02:44 +01:00
callSettingsButton . autoPinEdge ( . top , to : . bottom , of : settingsNagDescriptionLabel , withOffset : descriptionVSpacingHeight )
2017-02-27 20:37:42 +01:00
2018-03-14 14:02:44 +01:00
let notNowButton = OWSFlatButton . button ( title : NSLocalizedString ( " CALL_VIEW_SETTINGS_NAG_NOT_NOW_BUTTON " ,
2018-04-18 20:39:37 +02:00
comment : " Label for button that dismiss the call view's settings nag. " ) ,
2018-03-14 14:02:44 +01:00
font : OWSFlatButton . fontForHeight ( buttonHeight ) ,
titleColor : UIColor . white ,
backgroundColor : UIColor . ows_signalBrandBlue ,
target : self ,
selector : #selector ( didPressDismissNag ) )
2017-02-27 20:37:42 +01:00
viewStack . addSubview ( notNowButton )
2018-03-14 14:02:44 +01:00
notNowButton . autoSetDimension ( . height , toSize : buttonHeight )
2017-02-27 20:37:42 +01:00
notNowButton . autoPinWidthToSuperview ( )
2018-03-14 14:02:44 +01:00
notNowButton . autoPinEdge ( toSuperviewEdge : . bottom )
notNowButton . autoPinEdge ( . top , to : . bottom , of : callSettingsButton , withOffset : 12 )
2017-02-27 20:37:42 +01:00
}
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
2018-04-18 20:39:37 +02:00
// t e x t M e s s a g e B u t t o n = c r e a t e B u t t o n ( i m a g e N a m e : " m e s s a g e - a c t i v e - w i d e " ,
// a c t i o n : # s e l e c t o r ( d i d P r e s s T e x t M e s s a g e ) )
2018-04-23 22:12:23 +02:00
audioSourceButton = createButton ( image : # imageLiteral ( resourceName : " audio-call-speaker-inactive " ) ,
2018-04-18 20:39:59 +02:00
action : #selector ( didPressAudioSource ) )
audioSourceButton . accessibilityLabel = NSLocalizedString ( " CALL_VIEW_AUDIO_SOURCE_LABEL " ,
comment : " Accessibility label for selection the audio source " )
2018-04-23 22:12:23 +02:00
hangUpButton = createButton ( image : # imageLiteral ( resourceName : " hangup-active-wide " ) ,
2018-04-18 20:39:59 +02:00
action : #selector ( didPressHangup ) )
hangUpButton . accessibilityLabel = NSLocalizedString ( " CALL_VIEW_HANGUP_LABEL " ,
comment : " Accessibility label for hang up call " )
2018-04-23 22:12:23 +02:00
audioModeMuteButton = createButton ( image : # imageLiteral ( resourceName : " audio-call-mute-inactive " ) ,
2018-04-18 20:39:59 +02:00
action : #selector ( didPressMute ) )
2018-04-23 22:12:23 +02:00
audioModeMuteButton . setImage ( # imageLiteral ( resourceName : " audio-call-mute-active " ) , for : . selected )
2018-04-18 20:39:59 +02:00
audioModeMuteButton . accessibilityLabel = NSLocalizedString ( " CALL_VIEW_MUTE_LABEL " ,
comment : " Accessibility label for muting the microphone " )
2018-04-23 22:12:23 +02:00
audioModeVideoButton = createButton ( image : # imageLiteral ( resourceName : " audio-call-video-inactive " ) ,
action : #selector ( didPressVideo ) )
audioModeVideoButton . setImage ( # imageLiteral ( resourceName : " audio-call-video-active " ) , for : . selected )
audioModeVideoButton . accessibilityLabel = NSLocalizedString ( " CALL_VIEW_SWITCH_TO_VIDEO_LABEL " , comment : " Accessibility label to switch to video call " )
videoModeMuteButton = createButton ( image : # imageLiteral ( resourceName : " video-mute-unselected " ) ,
2018-04-18 20:39:59 +02:00
action : #selector ( didPressMute ) )
2018-04-23 22:12:23 +02:00
videoModeMuteButton . setImage ( # imageLiteral ( resourceName : " video-mute-selected " ) , for : . selected )
2018-04-18 20:39:59 +02:00
videoModeMuteButton . accessibilityLabel = NSLocalizedString ( " CALL_VIEW_MUTE_LABEL " , comment : " Accessibility label for muting the microphone " )
2018-04-23 22:12:23 +02:00
videoModeMuteButton . alpha = 0.9
2018-04-18 20:39:59 +02:00
2018-05-01 23:01:38 +02:00
videoModeFlipCameraButton = createButton ( image : # imageLiteral ( resourceName : " video-switch-camera-unselected " ) ,
2018-04-23 22:12:23 +02:00
action : #selector ( didPressFlipCamera ) )
2018-05-01 23:01:38 +02:00
2018-04-23 22:12:23 +02:00
videoModeFlipCameraButton . accessibilityLabel = NSLocalizedString ( " CALL_VIEW_SWITCH_CAMERA_DIRECTION " , comment : " Accessibility label to toggle front vs. rear facing camera " )
videoModeFlipCameraButton . alpha = 0.9
2018-04-18 20:39:59 +02:00
2018-04-23 22:12:23 +02:00
videoModeVideoButton = createButton ( image : # imageLiteral ( resourceName : " video-video-unselected " ) ,
2018-04-18 20:39:59 +02:00
action : #selector ( didPressVideo ) )
2018-04-23 22:12:23 +02:00
videoModeVideoButton . setImage ( # imageLiteral ( resourceName : " video-video-selected " ) , for : . selected )
2018-04-18 20:39:59 +02:00
videoModeVideoButton . accessibilityLabel = NSLocalizedString ( " CALL_VIEW_SWITCH_TO_AUDIO_LABEL " , comment : " Accessibility label to switch to audio only " )
2018-04-23 22:12:23 +02:00
videoModeVideoButton . alpha = 0.9
2017-01-12 21:55:14 +01:00
2018-04-23 22:12:23 +02:00
ongoingCallControls = UIStackView ( arrangedSubviews : [ hangUpButton ] )
ongoingCallControls . axis = . vertical
ongoingCallControls . alignment = . center
view . addSubview ( ongoingCallControls )
2017-01-18 23:29:47 +01:00
2018-04-23 22:12:23 +02:00
ongoingAudioCallControls = UIStackView ( arrangedSubviews : [ audioModeMuteButton , audioSourceButton , audioModeVideoButton ] )
ongoingAudioCallControls . distribution = . equalSpacing
ongoingAudioCallControls . axis = . horizontal
ongoingVideoCallControls = UIStackView ( arrangedSubviews : [ videoModeMuteButton , videoModeFlipCameraButton , videoModeVideoButton ] )
ongoingAudioCallControls . distribution = . equalSpacing
ongoingVideoCallControls . axis = . horizontal
2017-07-12 21:51:07 +02:00
}
2017-07-13 21:03:42 +02:00
func presentAudioSourcePicker ( ) {
2018-04-11 21:17:34 +02:00
SwiftAssertIsOnMainThread ( #function )
2017-07-12 21:51:07 +02:00
let actionSheetController = UIAlertController ( title : nil , message : nil , preferredStyle : . actionSheet )
2018-03-14 14:02:44 +01:00
let dismissAction = UIAlertAction ( title : CommonStrings . dismissButton , style : . cancel , handler : nil )
2017-07-12 21:51:07 +02:00
actionSheetController . addAction ( dismissAction )
let currentAudioSource = callUIAdapter . audioService . currentAudioSource ( call : self . call )
2017-07-13 21:03:42 +02:00
for audioSource in self . appropriateAudioSources {
2017-07-12 21:51:07 +02:00
let routeAudioAction = UIAlertAction ( title : audioSource . localizedName , style : . default ) { _ in
2017-07-13 21:37:01 +02:00
self . callUIAdapter . setAudioSource ( call : self . call , audioSource : audioSource )
2017-07-12 21:51:07 +02:00
}
2017-07-13 20:38:32 +02:00
// H A C K : p r i v a t e A P I t o c r e a t e c h e c k m a r k f o r a c t i v e a u d i o s o u r c e .
2017-07-12 21:51:07 +02:00
routeAudioAction . setValue ( currentAudioSource = = audioSource , forKey : " checked " )
2017-07-13 20:38:32 +02:00
// TODO: p i c k s o m e i c o n s . L e a v i n g o u t f o r M V P
// H A C K : p r i v a t e A P I t o a d d i m a g e t o a c t i o n s h e e t
// r o u t e A u d i o A c t i o n . s e t V a l u e ( a u d i o S o u r c e . i m a g e , f o r K e y : " i m a g e " )
2017-07-12 21:51:07 +02:00
actionSheetController . addAction ( routeAudioAction )
}
2018-04-24 20:00:16 +02:00
// N o t e : I t ' s c r i t i c a l t h a t w e p r e s e n t f r o m t h i s v i e w a n d
// n o t t h e " f r o n t m o s t v i e w c o n t r o l l e r " s i n c e t h i s v i e w m a y
// r e s i d e o n a s e p a r a t e w i n d o w .
2017-07-12 21:51:07 +02:00
self . present ( actionSheetController , animated : true )
}
2017-06-20 22:28:35 +02:00
func updateAvatarImage ( ) {
2017-07-31 22:07:38 +02:00
contactAvatarView . image = OWSAvatarBuilder . buildImage ( thread : thread , diameter : 400 , contactsManager : contactsManager )
2017-06-20 22:28:35 +02:00
}
2017-01-13 00:19:21 +01:00
func createIncomingCallControls ( ) {
2017-01-12 21:55:14 +01:00
2018-04-23 22:12:23 +02:00
acceptIncomingButton = createButton ( image : # imageLiteral ( resourceName : " call-active-wide " ) ,
2018-04-18 20:39:59 +02:00
action : #selector ( didPressAnswerCall ) )
acceptIncomingButton . accessibilityLabel = NSLocalizedString ( " CALL_VIEW_ACCEPT_INCOMING_CALL_LABEL " ,
comment : " Accessibility label for accepting incoming calls " )
2018-04-23 22:12:23 +02:00
declineIncomingButton = createButton ( image : # imageLiteral ( resourceName : " hangup-active-wide " ) ,
2018-04-18 20:39:59 +02:00
action : #selector ( didPressDeclineCall ) )
declineIncomingButton . accessibilityLabel = NSLocalizedString ( " CALL_VIEW_DECLINE_INCOMING_CALL_LABEL " ,
comment : " Accessibility label for declining incoming calls " )
2017-01-13 00:19:21 +01:00
2018-04-23 22:12:23 +02:00
incomingCallControls = UIStackView ( arrangedSubviews : [ acceptIncomingButton , declineIncomingButton ] )
incomingCallControls . axis = . horizontal
incomingCallControls . alignment = . center
incomingCallControls . distribution = . equalSpacing
2017-01-13 00:19:21 +01:00
2018-04-23 22:12:23 +02:00
view . addSubview ( incomingCallControls )
2017-01-12 21:55:14 +01:00
}
2018-04-23 22:12:23 +02:00
func createButton ( image : UIImage , action : Selector ) -> UIButton {
2017-01-12 21:55:14 +01:00
let button = UIButton ( )
2018-03-14 14:02:44 +01:00
button . setImage ( image , for : . normal )
2017-01-13 22:32:44 +01:00
button . imageEdgeInsets = UIEdgeInsets ( top : buttonInset ( ) ,
left : buttonInset ( ) ,
bottom : buttonInset ( ) ,
right : buttonInset ( ) )
2018-03-14 14:02:44 +01:00
button . addTarget ( self , action : action , for : . touchUpInside )
button . autoSetDimension ( . width , toSize : buttonSize ( ) )
button . autoSetDimension ( . height , toSize : buttonSize ( ) )
2017-01-12 21:55:14 +01:00
return button
}
2017-01-26 16:05:41 +01:00
// MARK: - L a y o u t
2018-04-23 22:12:23 +02:00
func createViewConstraints ( ) {
let topMargin = CGFloat ( 40 )
let contactVSpacing = CGFloat ( 3 )
let settingsNagHMargin = CGFloat ( 30 )
let ongoingBottomMargin = ScaleFromIPhone5To7Plus ( 23 , 41 )
let incomingHMargin = ScaleFromIPhone5To7Plus ( 30 , 56 )
let incomingBottomMargin = CGFloat ( 41 )
let settingsNagBottomMargin = CGFloat ( 41 )
let avatarTopSpacing = ScaleFromIPhone5To7Plus ( 25 , 50 )
// T h e b u t t o n s h a v e b u i l t - i n 1 0 % m a r g i n s , s o t o a p p e a r c e n t e r e d
// t h e a v a t a r ' s b o t t o m s p a c i n g s h o u l d b e a b i t l e s s .
let avatarBottomSpacing = ScaleFromIPhone5To7Plus ( 18 , 41 )
// L a y o u t o f t h e l o c a l v i d e o v i e w i s a b i t u n u s u a l b e c a u s e
// a l t h o u g h t h e v i e w i s s q u a r e , i t w i l l b e u s e d
let videoPreviewHMargin = CGFloat ( 0 )
2017-01-12 21:55:14 +01:00
2018-04-23 22:12:23 +02:00
// D a r k b l u r r e d b a c k g r o u n d .
blurView . autoPinEdgesToSuperviewEdges ( )
localVideoView . autoPinTrailingToSuperviewMargin ( withInset : videoPreviewHMargin )
localVideoView . autoPinEdge ( toSuperviewEdge : . top , withInset : topMargin )
let localVideoSize = ScaleFromIPhone5To7Plus ( 80 , 100 )
localVideoView . autoSetDimension ( . width , toSize : localVideoSize )
localVideoView . autoSetDimension ( . height , toSize : localVideoSize )
remoteVideoView . autoPinEdgesToSuperviewEdges ( )
contactNameLabel . autoPinEdge ( toSuperviewEdge : . top , withInset : topMargin )
contactNameLabel . autoPinLeadingToSuperviewMargin ( )
contactNameLabel . setContentHuggingVerticalHigh ( )
contactNameLabel . setCompressionResistanceHigh ( )
callStatusLabel . autoPinEdge ( . top , to : . bottom , of : contactNameLabel , withOffset : contactVSpacing )
callStatusLabel . autoPinLeadingToSuperviewMargin ( )
callStatusLabel . setContentHuggingVerticalHigh ( )
callStatusLabel . setCompressionResistanceHigh ( )
contactAvatarContainerView . autoPinEdge ( . top , to : . bottom , of : callStatusLabel , withOffset : + avatarTopSpacing )
contactAvatarContainerView . autoPinEdge ( . bottom , to : . top , of : ongoingCallControls , withOffset : - avatarBottomSpacing )
contactAvatarContainerView . autoPinWidthToSuperview ( withMargin : avatarTopSpacing )
contactAvatarView . autoCenterInSuperview ( )
// E n s u r e C o n t a c A v a t a r V i e w g e t s a s c l o s e a s p o s s i b l e t o i t ' s s u p e r v i e w e d g e s w h i l e m a i n t a i n i n g
// a s p e c t r a t i o .
contactAvatarView . autoPinToSquareAspectRatio ( )
contactAvatarView . autoPinEdge ( toSuperviewEdge : . top , withInset : 0 , relation : . greaterThanOrEqual )
contactAvatarView . autoPinEdge ( toSuperviewEdge : . right , withInset : 0 , relation : . greaterThanOrEqual )
contactAvatarView . autoPinEdge ( toSuperviewEdge : . bottom , withInset : 0 , relation : . greaterThanOrEqual )
contactAvatarView . autoPinEdge ( toSuperviewEdge : . left , withInset : 0 , relation : . greaterThanOrEqual )
NSLayoutConstraint . autoSetPriority ( UILayoutPriorityDefaultLow ) {
contactAvatarView . autoPinEdgesToSuperviewMargins ( )
}
2017-01-12 21:55:14 +01:00
2018-04-23 22:12:23 +02:00
// O n g o i n g c a l l c o n t r o l s
ongoingCallControls . autoPinEdge ( toSuperviewEdge : . bottom , withInset : ongoingBottomMargin )
ongoingCallControls . autoPinLeadingToSuperviewMargin ( )
ongoingCallControls . autoPinTrailingToSuperviewMargin ( )
ongoingCallControls . setContentHuggingVerticalHigh ( )
2017-02-27 20:37:42 +01:00
2018-04-23 22:12:23 +02:00
// I n c o m i n g c a l l c o n t r o l s
incomingCallControls . autoPinEdge ( toSuperviewEdge : . bottom , withInset : incomingBottomMargin )
incomingCallControls . autoPinLeadingToSuperviewMargin ( withInset : incomingHMargin )
incomingCallControls . autoPinTrailingToSuperviewMargin ( withInset : incomingHMargin )
incomingCallControls . setContentHuggingVerticalHigh ( )
// S e t t i n g s n a g v i e w s
settingsNagView . autoPinEdge ( toSuperviewEdge : . bottom , withInset : settingsNagBottomMargin )
settingsNagView . autoPinWidthToSuperview ( withMargin : settingsNagHMargin )
settingsNagView . autoPinEdge ( . top , to : . bottom , of : callStatusLabel )
}
2017-01-12 21:55:14 +01:00
2018-04-23 22:12:23 +02:00
override func updateViewConstraints ( ) {
2017-01-26 17:33:42 +01:00
updateRemoteVideoLayout ( )
updateLocalVideoLayout ( )
2017-01-26 16:05:41 +01:00
2017-01-12 21:55:14 +01:00
super . updateViewConstraints ( )
}
2017-01-26 17:33:42 +01:00
internal func updateRemoteVideoLayout ( ) {
2017-10-04 17:00:59 +02:00
remoteVideoView . isHidden = ! self . hasRemoteVideoTrack
2017-01-27 17:11:33 +01:00
updateCallUI ( callState : call . state )
2017-01-26 17:33:42 +01:00
}
internal func updateLocalVideoLayout ( ) {
NSLayoutConstraint . deactivate ( self . localVideoConstraints )
var constraints : [ NSLayoutConstraint ] = [ ]
if localVideoView . isHidden {
2017-07-21 16:36:45 +02:00
let contactHMargin = CGFloat ( 5 )
2018-04-02 21:31:32 +02:00
constraints . append ( contactNameLabel . autoPinTrailingToSuperviewMargin ( withInset : contactHMargin ) )
constraints . append ( callStatusLabel . autoPinTrailingToSuperviewMargin ( withInset : contactHMargin ) )
2017-01-26 17:33:42 +01:00
} else {
let spacing = CGFloat ( 10 )
2018-04-02 21:31:32 +02:00
constraints . append ( localVideoView . autoPinLeading ( toTrailingEdgeOf : contactNameLabel , offset : spacing ) )
constraints . append ( localVideoView . autoPinLeading ( toTrailingEdgeOf : callStatusLabel , offset : spacing ) )
2017-01-26 17:33:42 +01:00
}
self . localVideoConstraints = constraints
2017-01-30 15:43:48 +01:00
updateCallUI ( callState : call . state )
2017-01-26 16:05:41 +01:00
}
// MARK: - M e t h o d s
2016-11-12 18:22:29 +01:00
func showCallFailed ( error : Error ) {
// T O D O S h o w s o m e t h i n g i n U I .
Logger . error ( " \( TAG ) call failed with error: \( error ) " )
}
2017-01-26 16:05:41 +01:00
// MARK: - V i e w S t a t e
2016-11-12 18:22:29 +01:00
func localizedTextForCallState ( _ callState : CallState ) -> String {
2017-01-18 01:31:25 +01:00
assert ( Thread . isMainThread )
2016-11-12 18:22:29 +01:00
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 :
2017-07-13 16:41:39 +02:00
let callDuration = call . connectionDuration ( )
2018-03-14 14:02:44 +01:00
let callDurationDate = Date ( timeIntervalSinceReferenceDate : callDuration )
2017-07-13 16:41:39 +02:00
if dateFormatter = = nil {
2018-04-18 20:39:59 +02:00
dateFormatter = DateFormatter ( )
dateFormatter ! . dateFormat = " HH:mm:ss "
dateFormatter ! . timeZone = TimeZone ( identifier : " UTC " ) !
2017-07-13 16:41:39 +02:00
}
var formattedDate = dateFormatter ! . string ( from : callDurationDate )
if formattedDate . hasPrefix ( " 00: " ) {
// D o n ' t s h o w t h e " h o u r s " p o r t i o n o f t h e d a t e f o r m a t u n l e s s t h e
// c a l l d u r a t i o n i s a t l e a s t 1 h o u r .
formattedDate = formattedDate . substring ( from : formattedDate . index ( formattedDate . startIndex , offsetBy : 3 ) )
2017-01-17 16:04:51 +01:00
} else {
2017-07-13 16:41:39 +02:00
// I f s h o w i n g t h e " h o u r s " p o r t i o n o f t h e d a t e f o r m a t , s t r i p a n y l e a d i n g
// z e r o e s .
if formattedDate . hasPrefix ( " 0 " ) {
formattedDate = formattedDate . substring ( from : formattedDate . index ( formattedDate . startIndex , offsetBy : 1 ) )
}
2017-01-17 16:04:51 +01:00
}
2017-07-13 16:41:39 +02:00
return formattedDate
2018-04-19 15:56:09 +02:00
case . reconnecting :
return NSLocalizedString ( " IN_CALL_RECONNECTING " , comment : " Call setup status label " )
2016-11-12 18:22:29 +01:00
case . remoteBusy :
return NSLocalizedString ( " END_CALL_RESPONDER_IS_BUSY " , comment : " Call setup status label " )
case . localFailure :
2017-02-08 01:37:05 +01:00
if let error = call . error {
2017-02-27 20:37:42 +01:00
switch error {
2017-02-08 01:37:05 +01:00
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
}
}
2016-11-12 18:22:29 +01:00
return NSLocalizedString ( " END_CALL_UNCATEGORIZED_FAILURE " , comment : " Call setup status label " )
}
}
2018-04-19 15:56:09 +02:00
var isBlinkingReconnectLabel = false
2017-01-18 01:31:25 +01:00
func updateCallStatusLabel ( callState : CallState ) {
assert ( Thread . isMainThread )
2017-02-06 18:36:55 +01:00
2017-02-07 17:26:33 +01:00
let text = String ( format : CallStrings . callStatusFormat ,
localizedTextForCallState ( callState ) )
2017-02-06 18:36:55 +01:00
self . callStatusLabel . text = text
2018-04-19 15:56:09 +02:00
// H a n d l e r e c o n n e c t i n g b l i n k i n g
if case . reconnecting = callState {
if ! isBlinkingReconnectLabel {
isBlinkingReconnectLabel = true
UIView . animate ( withDuration : 0.7 , delay : 0 , options : [ . autoreverse , . repeat ] ,
animations : {
self . callStatusLabel . alpha = 0.2
} , completion : nil )
} else {
// a l r e a d y b l i n k i n g
}
} else {
// W e ' r e n o l o n g e r i n a r e c o n n e c t i n g s t a t e , e i t h e r t h e c a l l f a i l e d o r w e r e c o n n e c t e d .
// S t o p t h e b l i n k i n g a n i m a t i o n
if isBlinkingReconnectLabel {
self . callStatusLabel . layer . removeAllAnimations ( )
self . callStatusLabel . alpha = 1
isBlinkingReconnectLabel = false
}
}
2017-01-18 01:31:25 +01:00
}
2016-11-12 18:22:29 +01:00
2017-01-18 01:31:25 +01:00
func updateCallUI ( callState : CallState ) {
assert ( Thread . isMainThread )
updateCallStatusLabel ( callState : callState )
2017-02-27 20:37:42 +01:00
if isShowingSettingsNag {
settingsNagView . isHidden = false
contactAvatarView . isHidden = true
2018-04-23 22:12:23 +02:00
ongoingCallControls . isHidden = true
2017-02-27 20:37:42 +01:00
return
}
2018-02-03 00:35:32 +01:00
// M a r q u e e s c r o l l i n g i s d i s t r a c t i n g d u r i n g a v i d e o c a l l , d i s a b l e i t .
2017-09-07 23:34:54 +02:00
contactNameLabel . labelize = call . hasLocalVideo
2018-04-18 20:39:59 +02:00
audioModeMuteButton . isSelected = call . isMuted
videoModeMuteButton . isSelected = call . isMuted
audioModeVideoButton . isSelected = call . hasLocalVideo
videoModeVideoButton . isSelected = call . hasLocalVideo
2017-01-18 23:29:47 +01:00
2017-01-13 22:32:44 +01:00
// S h o w I n c o m i n g v s . O n g o i n g c a l l c o n t r o l s
2017-01-12 21:55:14 +01:00
let isRinging = callState = = . localRinging
2018-04-23 22:12:23 +02:00
incomingCallControls . isHidden = ! isRinging
incomingCallControls . isUserInteractionEnabled = isRinging
ongoingCallControls . isHidden = isRinging
ongoingCallControls . isUserInteractionEnabled = ! isRinging
2016-11-12 18:22:29 +01:00
2017-01-27 17:17:35 +01:00
// R e w o r k c o n t r o l s t a t e i f r e m o t e v i d e o i s a v a i l a b l e .
2017-01-30 15:43:48 +01:00
let hasRemoteVideo = ! remoteVideoView . isHidden
contactAvatarView . isHidden = hasRemoteVideo
// R e w o r k c o n t r o l s t a t e i f l o c a l v i d e o i s a v a i l a b l e .
let hasLocalVideo = ! localVideoView . isHidden
2017-07-12 21:51:07 +02:00
2018-04-23 22:12:23 +02:00
if hasLocalVideo {
ongoingAudioCallControls . removeFromSuperview ( )
ongoingCallControls . insertArrangedSubview ( ongoingVideoCallControls , at : 0 )
} else {
ongoingVideoCallControls . removeFromSuperview ( )
ongoingCallControls . insertArrangedSubview ( ongoingAudioCallControls , at : 0 )
2017-01-30 15:43:48 +01:00
}
2017-01-27 17:11:33 +01:00
2017-01-27 21:26:31 +01:00
// A l s o h i d e o t h e r c o n t r o l s i f u s e r h a s t a p p e d t o h i d e t h e m .
2017-01-30 20:06:57 +01:00
if shouldRemoteVideoControlsBeHidden && ! remoteVideoView . isHidden {
2017-01-27 21:26:31 +01:00
contactNameLabel . isHidden = true
callStatusLabel . isHidden = true
2018-04-23 22:12:23 +02:00
ongoingCallControls . isHidden = true
2017-01-27 21:26:31 +01:00
} else {
contactNameLabel . isHidden = false
callStatusLabel . isHidden = false
}
2017-07-13 21:03:42 +02:00
// A u d i o S o u r c e H a n d l i n g ( b l u e t o o t h )
if self . hasAlternateAudioSources {
2017-07-12 21:51:07 +02:00
// W i t h b l u e t o o t h , b u t t o n d o e s n o t s t a y s e l e c t e d . P r e s s i n g i t p o p s a n a c t i o n s h e e t
// a n d t h e b u t t o n s h o u l d i m m e d i a t e l y " u n s e l e c t " .
2018-04-18 20:39:59 +02:00
audioSourceButton . isSelected = false
2017-07-12 21:51:07 +02:00
if hasLocalVideo {
2018-04-18 20:39:59 +02:00
audioSourceButton . setImage ( # imageLiteral ( resourceName : " ic_speaker_bluetooth_inactive_video_mode " ) , for : . normal )
audioSourceButton . setImage ( # imageLiteral ( resourceName : " ic_speaker_bluetooth_inactive_video_mode " ) , for : . selected )
2017-07-12 21:51:07 +02:00
} else {
2018-04-18 20:39:59 +02:00
audioSourceButton . setImage ( # imageLiteral ( resourceName : " ic_speaker_bluetooth_inactive_audio_mode " ) , for : . normal )
audioSourceButton . setImage ( # imageLiteral ( resourceName : " ic_speaker_bluetooth_inactive_audio_mode " ) , for : . selected )
2017-07-12 21:51:07 +02:00
}
2018-04-18 20:39:59 +02:00
audioSourceButton . isHidden = false
2017-07-12 21:51:07 +02:00
} else {
// N o b l u e t o o t h a u d i o d e t e c t e d
2018-04-18 20:39:59 +02:00
audioSourceButton . setImage ( # imageLiteral ( resourceName : " audio-call-speaker-inactive " ) , for : . normal )
audioSourceButton . setImage ( # imageLiteral ( resourceName : " audio-call-speaker-active " ) , for : . selected )
2017-07-12 21:51:07 +02:00
// I f t h e r e ' s n o b l u e t o o t h , w e a l w a y s u s e s p e a k e r p h o n e , s o n o n e e d f o r
// a b u t t o n , g i v i n g m o r e s c r e e n b a c k f o r t h e v i d e o .
2018-04-18 20:39:59 +02:00
audioSourceButton . isHidden = hasLocalVideo
2017-07-12 21:51:07 +02:00
}
2016-11-12 18:22:29 +01:00
// D i s m i s s H a n d l i n g
switch callState {
case . remoteHangup , . remoteBusy , . localFailure :
2017-01-18 01:31:25 +01:00
Logger . debug ( " \( TAG ) dismissing after delay because new state is \( callState ) " )
2018-03-14 14:02:44 +01:00
dismissIfPossible ( shouldDelay : true )
2016-11-12 18:22:29 +01:00
case . localHangup :
Logger . debug ( " \( TAG ) dismissing immediately from local hangup " )
2018-03-14 14:02:44 +01:00
dismissIfPossible ( shouldDelay : false )
2016-11-12 18:22:29 +01:00
default : break
}
2017-01-17 16:04:51 +01:00
if callState = = . connected {
if callDurationTimer = = nil {
let kDurationUpdateFrequencySeconds = 1 / 20.0
2017-07-07 18:26:12 +02:00
callDurationTimer = WeakTimer . scheduledTimer ( timeInterval : TimeInterval ( kDurationUpdateFrequencySeconds ) ,
2018-04-18 20:39:37 +02:00
target : self ,
userInfo : nil ,
repeats : true ) { [ weak self ] _ in
self ? . updateCallDuration ( )
2017-07-07 18:26:12 +02:00
}
2017-01-17 16:04:51 +01:00
}
} else {
callDurationTimer ? . invalidate ( )
callDurationTimer = nil
}
}
2017-07-07 18:26:12 +02:00
func updateCallDuration ( ) {
2017-01-18 01:31:25 +01:00
updateCallStatusLabel ( callState : call . state )
2016-11-12 18:22:29 +01:00
}
2018-02-03 00:35:32 +01:00
// W e u p d a t e t h e a u d i o S o u r c e B u t t o n o u t s i d e o f t h e m a i n ` u p d a t e C a l l U I `
// b e c a u s e ` u p d a t e C a l l U I ` i s i n t e n d e d t o b e i d e m p o t e n t , w h i c h i s n ' t p o s s i b l e
// w i t h e x t e r n a l s p e a k e r s t a t e b e c a u s e :
// - t h e s y s t e m A P I w h i c h e n a b l e s t h e e x t e r n a l s p e a k e r i s a ( s o m e w h a t s l o w ) a s y n c r o n o u s
// o p e r a t i o n
// - w e w a n t t o g i v e i m m e d i a t e U I f e e d b a c k b y m a r k i n g t h e p r e s s e d b u t t o n a s s e l e c t e d
// b e f o r e t h e o p e r a t i o n c o m p l e t e s .
func updateAudioSourceButtonIsSelected ( ) {
guard callUIAdapter . audioService . isSpeakerphoneEnabled else {
2018-04-18 20:39:59 +02:00
self . audioSourceButton . isSelected = false
2018-02-03 00:35:32 +01:00
return
}
// V i d e o C h a t m o d e e n a b l e s t h e o u t p u t s p e a k e r , b u t w e d o n ' t
// w a n t t o h i g h l i g h t t h e s p e a k e r b u t t o n i n t h a t c a s e .
guard ! call . hasLocalVideo else {
2018-04-18 20:39:59 +02:00
self . audioSourceButton . isSelected = false
2018-02-03 00:35:32 +01:00
return
}
2018-04-18 20:39:59 +02:00
self . audioSourceButton . isSelected = true
2018-02-03 00:35:32 +01:00
}
2016-11-12 18:22:29 +01:00
// MARK: - A c t i o n s
/* *
* 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 ) " )
2017-07-13 16:41:39 +02:00
callUIAdapter . localHangupCall ( call )
2016-11-12 18:22:29 +01:00
2018-03-14 14:02:44 +01:00
dismissIfPossible ( shouldDelay : false )
2016-11-12 18:22:29 +01:00
}
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-07-13 16:41:39 +02:00
callUIAdapter . setIsMuted ( call : call , isMuted : muteButton . isSelected )
2016-11-12 18:22:29 +01:00
}
2017-07-13 21:03:42 +02:00
func didPressAudioSource ( sender button : UIButton ) {
2017-07-12 21:51:07 +02:00
Logger . info ( " \( TAG ) called \( #function ) " )
2017-07-13 21:03:42 +02:00
if self . hasAlternateAudioSources {
presentAudioSourcePicker ( )
2017-07-12 21:51:07 +02:00
} else {
didPressSpeakerphone ( sender : button )
}
}
2017-07-12 21:15:59 +02:00
func didPressSpeakerphone ( sender button : UIButton ) {
2016-11-12 18:22:29 +01:00
Logger . info ( " \( TAG ) called \( #function ) " )
2018-02-03 00:35:32 +01:00
2017-07-12 21:15:59 +02:00
button . isSelected = ! button . isSelected
2018-02-03 00:35:32 +01:00
callUIAdapter . audioService . requestSpeakerphone ( isEnabled : button . isSelected )
2016-11-12 18:22:29 +01:00
}
2017-07-12 21:15:59 +02:00
func didPressTextMessage ( sender button : UIButton ) {
2017-01-12 21:55:14 +01:00
Logger . info ( " \( TAG ) called \( #function ) " )
2017-01-13 22:32:44 +01:00
2018-03-14 14:02:44 +01:00
dismissIfPossible ( shouldDelay : false )
2017-01-12 21:55:14 +01:00
}
func didPressAnswerCall ( sender : UIButton ) {
2016-11-12 18:22:29 +01:00
Logger . info ( " \( TAG ) called \( #function ) " )
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 ) " )
2017-01-27 17:11:33 +01:00
let hasLocalVideo = ! sender . isSelected
2017-07-13 16:41:39 +02:00
callUIAdapter . setHasLocalVideo ( call : call , hasLocalVideo : hasLocalVideo )
2017-01-12 21:55:14 +01:00
}
2018-04-23 22:12:23 +02:00
func didPressFlipCamera ( sender : UIButton ) {
2018-04-23 22:40:56 +02:00
// t o g g l e v a l u e
sender . isSelected = ! sender . isSelected
2018-04-23 22:12:23 +02:00
2018-04-23 22:40:56 +02:00
let useBackCamera = sender . isSelected
Logger . info ( " \( TAG ) in \( #function ) with useBackCamera: \( useBackCamera ) " )
callUIAdapter . setCameraSource ( call : call , useBackCamera : useBackCamera )
2018-04-23 22:12:23 +02:00
}
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 ) " )
2017-07-13 16:41:39 +02:00
callUIAdapter . declineCall ( call )
2016-11-12 18:22:29 +01:00
2018-03-14 14:02:44 +01:00
dismissIfPossible ( shouldDelay : false )
2017-02-27 20:37:42 +01:00
}
func didPressShowCallSettings ( sender : UIButton ) {
Logger . info ( " \( TAG ) called \( #function ) " )
markSettingsNagAsComplete ( )
dismissIfPossible ( shouldDelay : false , ignoreNag : true , completion : {
// F i n d t h e f r o n t m o s t p r e s e n t e d U I V i e w C o n t r o l l e r f r o m w h i c h t o p r e s e n t t h e
// s e t t i n g s v i e w s .
2018-04-24 20:00:16 +02:00
let fromViewController = UIApplication . shared . findFrontmostViewController ( ignoringAlerts : true )
2017-02-27 20:37:42 +01:00
assert ( fromViewController != nil )
// C o n s t r u c t t h e " s e t t i n g s " v i e w & p u s h t h e " p r i v a c y s e t t i n g s " v i e w .
2017-09-07 17:03:54 +02:00
let navigationController = AppSettingsViewController . inModalNavigationController ( )
2018-03-14 14:02:44 +01:00
navigationController . pushViewController ( PrivacySettingsTableViewController ( ) , animated : false )
2017-02-27 20:37:42 +01:00
fromViewController ? . present ( navigationController , animated : true , completion : nil )
} )
}
func didPressDismissNag ( sender : UIButton ) {
Logger . info ( " \( TAG ) called \( #function ) " )
markSettingsNagAsComplete ( )
dismissIfPossible ( shouldDelay : false , ignoreNag : true )
}
// W e o n l y s h o w t h e " b l o c k i n g " s e t t i n g s n a g u n t i l t h e u s e r h a s c h o s e n
// t o v i e w t h e p r i v a c y s e t t i n g s _ o r _ d i s m i s s e d t h e n a g a t l e a s t o n c e .
2018-04-18 20:39:37 +02:00
//
// I n e i t h e r c a s e , w e s e t t h e " C a l l K i t e n a b l e d " a n d " C a l l K i t p r i v a c y e n a b l e d "
2017-02-27 20:37:42 +01:00
// s e t t i n g s t o t h e i r d e f a u l t v a l u e s t o i n d i c a t e t h a t t h e u s e r h a s r e v i e w e d
// t h e m .
private func markSettingsNagAsComplete ( ) {
Logger . info ( " \( TAG ) called \( #function ) " )
2018-04-18 20:39:59 +02:00
let preferences = Environment . current ( ) . preferences !
2017-02-27 20:37:42 +01:00
preferences . setIsCallKitEnabled ( preferences . isCallKitEnabled ( ) )
preferences . setIsCallKitPrivacyEnabled ( preferences . isCallKitPrivacyEnabled ( ) )
2016-11-12 18:22:29 +01:00
}
2017-01-09 15:28:04 +01:00
2018-04-24 23:00:39 +02:00
func didTapLeaveCall ( sender : UIGestureRecognizer ) {
guard sender . state = = . recognized else {
return
}
OWSWindowManager . shared ( ) . leaveCallView ( )
}
2017-01-19 15:38:50 +01:00
// MARK: - C a l l O b s e r v e r
2017-01-09 15:28:04 +01:00
internal func stateDidChange ( call : SignalCall , state : CallState ) {
2018-04-11 21:17:34 +02:00
SwiftAssertIsOnMainThread ( #function )
2017-01-26 16:05:41 +01:00
Logger . info ( " \( self . TAG ) new call status: \( state ) " )
self . updateCallUI ( callState : state )
2017-01-09 15:28:04 +01:00
}
2017-01-27 17:11:33 +01:00
internal func hasLocalVideoDidChange ( call : SignalCall , hasLocalVideo : Bool ) {
2018-04-11 21:17:34 +02:00
SwiftAssertIsOnMainThread ( #function )
2017-01-26 16:05:41 +01:00
self . updateCallUI ( callState : call . state )
2017-01-18 23:29:47 +01:00
}
2017-01-09 15:28:04 +01:00
internal func muteDidChange ( call : SignalCall , isMuted : Bool ) {
2018-04-11 21:17:34 +02:00
SwiftAssertIsOnMainThread ( #function )
2017-01-26 16:05:41 +01:00
self . updateCallUI ( callState : call . state )
2017-01-09 15:28:04 +01:00
}
2017-01-19 15:38:50 +01:00
2017-10-28 20:44:29 +02:00
func holdDidChange ( call : SignalCall , isOnHold : Bool ) {
2018-04-11 21:17:34 +02:00
SwiftAssertIsOnMainThread ( #function )
2017-10-28 20:44:29 +02:00
self . updateCallUI ( callState : call . state )
}
2017-07-13 21:37:01 +02:00
internal func audioSourceDidChange ( call : SignalCall , audioSource : AudioSource ? ) {
2018-04-11 21:17:34 +02:00
SwiftAssertIsOnMainThread ( #function )
2017-01-26 16:05:41 +01:00
self . updateCallUI ( callState : call . state )
}
2018-04-24 23:00:39 +02:00
// MARK: - C a l l A u d i o S e r v i c e D e l e g a t e
2018-02-03 00:35:32 +01:00
func callAudioService ( _ callAudioService : CallAudioService , didUpdateIsSpeakerphoneEnabled isSpeakerphoneEnabled : Bool ) {
2018-04-11 21:17:34 +02:00
SwiftAssertIsOnMainThread ( #function )
2018-02-03 00:35:32 +01:00
updateAudioSourceButtonIsSelected ( )
}
func callAudioServiceDidChangeAudioSession ( _ callAudioService : CallAudioService ) {
2018-04-11 21:17:34 +02:00
SwiftAssertIsOnMainThread ( #function )
2018-02-03 00:35:32 +01:00
// W h i c h s o u r c e s a r e a v a i l a b l e d e p e n d s o n t h e s t a t e o f y o u r S e s s i o n .
// W h e n t h e a u d i o s e s s i o n i s n o t y e t i n P l a y A n d R e c o r d n o n e a r e a v a i l a b l e
// T h e n i f w e ' r e i n s p e a k e r p h o n e , b l u e t o o t h i s n ' t a v a i l a b l e .
// S o w e a c c r u e a l l p o s s i b l e a u d i o s o u r c e s i n a s e t , a n d t h a t l i s t l i v e s a s l o n g s a s t h e C a l l V i e w C o n t r o l l e r
// T h e d o w n s i d e o f t h i s i s t h a t i f y o u e . g . u n p a i r y o u r b l u e t o o t h m i d c a l l , i t w i l l s t i l l a p p e a r a s a n o p t i o n
// u n t i l y o u r n e x t c a l l .
// FIXME: T h e r e ' s g o t t o b e a b e t t e r w a y , b u t t h i s i s w h e r e I l a n d e d a f t e r a b i t o f w o r k , a n d s e e m s t o w o r k
// p r e t t y w e l l i n p r a c t i c e .
let availableInputs = callAudioService . availableInputs
self . allAudioSources . formUnion ( availableInputs )
}
2017-01-26 16:05:41 +01:00
// MARK: - V i d e o
internal func updateLocalVideoTrack ( localVideoTrack : RTCVideoTrack ? ) {
2018-04-11 21:17:34 +02:00
SwiftAssertIsOnMainThread ( #function )
2017-01-26 23:42:32 +01:00
guard self . localVideoTrack != localVideoTrack else {
2017-01-26 16:05:41 +01:00
return
2017-01-19 15:38:50 +01:00
}
2017-01-26 16:05:41 +01:00
self . localVideoTrack = localVideoTrack
2017-05-05 00:17:18 +02:00
let source = localVideoTrack ? . source as ? RTCAVFoundationVideoSource
2017-01-26 16:05:41 +01:00
localVideoView . captureSession = source ? . captureSession
let isHidden = source = = nil
Logger . info ( " \( TAG ) \( #function ) isHidden: \( isHidden ) " )
2017-01-26 19:39:53 +01:00
localVideoView . isHidden = isHidden
2017-01-26 16:05:41 +01:00
2017-01-26 17:33:42 +01:00
updateLocalVideoLayout ( )
2018-02-03 00:35:32 +01:00
updateAudioSourceButtonIsSelected ( )
2017-01-26 16:05:41 +01:00
}
2017-10-03 23:26:14 +02:00
var hasRemoteVideoTrack : Bool {
return self . remoteVideoTrack != nil
}
2017-10-04 17:00:59 +02:00
2017-01-26 16:05:41 +01:00
internal func updateRemoteVideoTrack ( remoteVideoTrack : RTCVideoTrack ? ) {
2018-04-11 21:17:34 +02:00
SwiftAssertIsOnMainThread ( #function )
2017-01-26 23:42:32 +01:00
guard self . remoteVideoTrack != remoteVideoTrack else {
2017-01-26 16:05:41 +01:00
return
}
self . remoteVideoTrack ? . remove ( remoteVideoView )
self . remoteVideoTrack = nil
remoteVideoView . renderFrame ( nil )
self . remoteVideoTrack = remoteVideoTrack
self . remoteVideoTrack ? . add ( remoteVideoView )
2017-01-30 20:06:57 +01:00
shouldRemoteVideoControlsBeHidden = false
2017-01-26 16:05:41 +01:00
2017-01-26 17:33:42 +01:00
updateRemoteVideoLayout ( )
2017-01-26 16:05:41 +01:00
}
2018-03-14 14:02:44 +01:00
internal func dismissIfPossible ( shouldDelay : Bool , ignoreNag ignoreNagParam : Bool = false , completion : ( ( ) -> Void ) ? = nil ) {
2018-02-03 00:35:32 +01:00
callUIAdapter . audioService . delegate = nil
2018-02-23 23:08:21 +01:00
let ignoreNag : Bool = {
// N o t h i n g t o n a g a b o u t o n i O S 1 1
if #available ( iOS 11 , * ) {
return true
} else {
// o t h e r w i s e o n i O S 1 0 , n a g a s s p e c i f i e d
return ignoreNagParam
}
} ( )
2017-02-27 20:37:42 +01:00
if hasDismissed {
// D o n ' t d i s m i s s t w i c e .
return
} else if ! ignoreNag &&
call . direction = = . incoming &&
2017-02-27 17:04:14 +01:00
UIDevice . current . supportsCallKit &&
2017-12-04 16:35:47 +01:00
( ! Environment . current ( ) . preferences . isCallKitEnabled ( ) ||
Environment . current ( ) . preferences . isCallKitPrivacyEnabled ( ) ) {
2017-02-27 20:37:42 +01:00
isShowingSettingsNag = true
// U p d a t e t h e n a g v i e w ' s c o p y t o r e f l e c t t h e s e t t i n g s s t a t e .
2017-12-04 16:35:47 +01:00
if Environment . current ( ) . preferences . isCallKitEnabled ( ) {
2017-02-27 20:37:42 +01:00
settingsNagDescriptionLabel . text = NSLocalizedString ( " CALL_VIEW_SETTINGS_NAG_DESCRIPTION_PRIVACY " ,
comment : " Reminder to the user of the benefits of disabling CallKit privacy. " )
} else {
settingsNagDescriptionLabel . text = NSLocalizedString ( " CALL_VIEW_SETTINGS_NAG_DESCRIPTION_ALL " ,
comment : " Reminder to the user of the benefits of enabling CallKit and disabling CallKit privacy. " )
}
settingsNagDescriptionLabel . superview ? . setNeedsLayout ( )
2017-12-04 16:35:47 +01:00
if Environment . current ( ) . preferences . isCallKitEnabledSet ( ) ||
Environment . current ( ) . preferences . isCallKitPrivacySet ( ) {
2017-02-27 20:37:42 +01:00
// U s e r h a s a l r e a d y t o u c h e d t h e s e p r e f e r e n c e s , o n l y s h o w
// t h e " f l e e t i n g " n a g , n o t t h e " b l o c k i n g " n a g .
// S h o w n a g f o r N s e c o n d s .
DispatchQueue . main . asyncAfter ( deadline : . now ( ) + 5 ) { [ weak self ] in
guard let strongSelf = self else { return }
strongSelf . dismissIfPossible ( shouldDelay : false , ignoreNag : true )
}
}
} else if shouldDelay {
hasDismissed = true
2017-02-27 23:28:25 +01:00
DispatchQueue . main . asyncAfter ( deadline : . now ( ) + 1.5 ) { [ weak self ] in
guard let strongSelf = self else { return }
2018-04-18 17:23:57 +02:00
strongSelf . dismissImmediately ( completion : completion )
2017-02-27 20:37:42 +01:00
}
} else {
hasDismissed = true
2018-04-18 17:23:57 +02:00
dismissImmediately ( completion : completion )
}
}
internal func dismissImmediately ( completion : ( ( ) -> Void ) ? ) {
if CallViewController . kShowCallViewOnSeparateWindow {
OWSWindowManager . shared ( ) . endCall ( self )
completion ? ( )
} else {
self . dismiss ( animated : true , completion : completion )
2017-02-27 20:37:42 +01:00
}
}
2017-05-03 15:34:13 +02:00
2017-01-26 16:05:41 +01:00
// MARK: - C a l l S e r v i c e O b s e r v e r
2017-02-03 21:37:16 +01:00
internal func didUpdateCall ( call : SignalCall ? ) {
// D o n o t h i n g .
}
2017-05-03 15:34:13 +02:00
internal func didUpdateVideoTracks ( call : SignalCall ? ,
localVideoTrack : RTCVideoTrack ? ,
2017-01-26 16:05:41 +01:00
remoteVideoTrack : RTCVideoTrack ? ) {
2018-04-11 21:17:34 +02:00
SwiftAssertIsOnMainThread ( #function )
2017-01-26 16:05:41 +01:00
2017-10-03 23:26:14 +02:00
updateLocalVideoTrack ( localVideoTrack : localVideoTrack )
updateRemoteVideoTrack ( remoteVideoTrack : remoteVideoTrack )
2017-01-26 16:05:41 +01:00
}
2016-11-12 18:22:29 +01:00
}