//
// C o p y r i g h t ( c ) 2 0 1 7 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 .
//
import Foundation
/* *
* Creates an outbound call via either Redphone or WebRTC depending on participant preferences .
*/
@objc class OutboundCallInitiator : NSObject {
let TAG = " [OutboundCallInitiator] "
let redphoneManager : PhoneManager
let contactsManager : OWSContactsManager
let contactsUpdater : ContactsUpdater
var cancelledCallTokens : [ String ] = [ ]
init ( redphoneManager : PhoneManager , contactsManager : OWSContactsManager , contactsUpdater : ContactsUpdater ) {
self . redphoneManager = redphoneManager
self . contactsManager = contactsManager
self . contactsUpdater = contactsUpdater
super . init ( )
NotificationCenter . default . addObserver ( self ,
selector : #selector ( callWasCancelledByInterstitial ) ,
name : Notification . Name ( rawValue : CallService . callWasCancelledByInterstitialNotificationName ( ) ) ,
object : nil )
}
deinit {
NotificationCenter . default . removeObserver ( self )
}
func callWasCancelledByInterstitial ( notification : NSNotification ) {
AssertIsOnMainThread ( )
let callToken = notification . object as ! String
cancelCallToken ( callToken : callToken )
}
func cancelCallToken ( callToken : String ) {
AssertIsOnMainThread ( )
cancelledCallTokens . append ( callToken )
}
/* *
* | handle | is a user formatted phone number , e . g . from a system contacts entry
*/
public func initiateCall ( handle : String ) -> Bool {
Logger . info ( " \( TAG ) in \( #function ) with handle: \( handle ) " )
guard let recipientId = PhoneNumber ( fromUserSpecifiedText : handle ) ? . toE164 ( ) else {
Logger . warn ( " \( TAG ) unable to parse signalId from phone number: \( handle ) " )
return false
}
return initiateCall ( recipientId : recipientId )
}
/* *
* | recipientId | is a e164 formatted phone number .
*/
public func initiateCall ( recipientId : String ) -> Bool {
// A t e m p o r a r y u n i q u e i d u s e d t o i d e n t i f y t h i s c a l l d u r i n g t h e
let callToken = NSUUID ( ) . uuidString
presentCallInterstitial ( callToken : callToken )
// S i n c e u s e r s c a n t o g g l e t h i s s e t t i n g , w h i c h i s o n l y c o m m u n i c a t e d d u r i n g c o n t a c t s y n c , i t ' s e a s y t o i m a g i n e t h e
// p r e f e r e n c e g e t t i n g s t a l e . E s p e c i a l l y a s u s e r s a r e t o g g l i n g t h e f e a t u r e t o t e s t c a l l s . S o h e r e , w e o p t f o r a
// b l o c k i n g n e t w o r k r e q u e s t * e v e r y * t i m e w e p l a c e a c a l l t o m a k e s u r e w e ' v e g o t u p t o d a t e p r e f e r e n c e s .
//
// e . g . T h e f o l l o w i n g w o u l d s u f f i c e i f w e w e r e n ' t w o r r i e d a b o u t s t a l e p r e f e r e n c e s .
// S i g n a l R e c i p i e n t * r e c i p i e n t = [ S i g n a l R e c i p i e n t r e c i p i e n t W i t h T e x t S e c u r e I d e n t i f i e r : s e l f . t h r e a d . c o n t a c t I d e n t i f i e r ] ;
self . contactsUpdater . lookupIdentifier ( recipientId ,
success : { recipient in
guard ! self . cancelledCallTokens . contains ( callToken ) else {
Logger . info ( " \( self . TAG ) OutboundCallInitiator aborting due to cancelled call. " )
return
}
guard ! Environment . getCurrent ( ) . phoneManager . hasOngoingRedphoneCall ( ) else {
Logger . error ( " \( self . TAG ) OutboundCallInitiator aborting due to ongoing RedPhone call. " )
return
}
guard Environment . getCurrent ( ) . callService . call = = nil else {
Logger . error ( " \( self . TAG ) OutboundCallInitiator aborting due to ongoing WebRTC call. " )
return
}
let remoteWantsWebRTC = recipient . supportsWebRTC
Logger . debug ( " \( self . TAG ) remoteWantsWebRTC: \( remoteWantsWebRTC ) " )
if remoteWantsWebRTC {
_ = self . initiateWebRTCAudioCall ( recipientId : recipientId )
} else {
_ = self . initiateRedphoneCall ( recipientId : recipientId )
}
} ,
failure : { error in
Logger . warn ( " \( self . TAG ) looking up recipientId: \( recipientId ) failed with error \( error ) " )
self . cancelCallToken ( callToken : callToken )
self . dismissCallInterstitial ( callToken : callToken )
let alertTitle = NSLocalizedString ( " UNABLE_TO_PLACE_CALL " , comment : " Alert Title " )
let alertController = UIAlertController ( title : alertTitle , message : error . localizedDescription , preferredStyle : . alert )
let dismissAction = UIAlertAction ( title : NSLocalizedString ( " DISMISS_BUTTON_TEXT " , comment : " Generic short text for button to dismiss a dialog " ) , style : . default )
alertController . addAction ( dismissAction )
UIApplication . shared . keyWindow ? . rootViewController ? . present ( alertController , animated : true , completion : nil )
} )
// S i n c e w e ' v e a l r e a d y d i s p a t c h e d a s y n c t o m a k e s u r e w e h a v e f r e s h w e b r t c p r e f e r e n c e d a t a
// w e d o n ' t h a v e a m e a n i n g f u l v a l u e t o r e t u r n h e r e - b u t w e ' r e n o t u s i n g i t a n w a y . = /
return true
}
private func initiateRedphoneCall ( recipientId : String ) -> Bool {
Logger . info ( " \( TAG ) Placing redphone call to: \( recipientId ) " )
let number = PhoneNumber . tryParsePhoneNumber ( fromUserSpecifiedText : recipientId )
assert ( number != nil )
let contact : Contact ? = self . contactsManager . latestContact ( for : number )
redphoneManager . initiateOutgoingCall ( to : contact , atRemoteNumber : number )
return true
}
private func initiateWebRTCAudioCall ( recipientId : String ) -> Bool {
// R a t h e r t h a n a n i n i t - a s s i g n e d d e p e n d e n c y p r o p e r t y , w e a c c e s s ` c a l l U I A d a p t e r ` v i a E n v i r o n m e n t
// b e c a u s e i t c a n c h a n g e a f t e r a p p l a u n c h d u e t o u s e r s e t t i n g s
guard let callUIAdapter = Environment . getCurrent ( ) . callUIAdapter else {
assertionFailure ( )
Logger . error ( " \( TAG ) can't initiate call because callUIAdapter is nil " )
return false
}
callUIAdapter . startAndShowOutgoingCall ( recipientId : recipientId )
return true
}
private func presentCallInterstitial ( callToken : String ) {
AssertIsOnMainThread ( )
let notificationName = CallService . presentCallInterstitialNotificationName ( )
NotificationCenter . default . post ( name : NSNotification . Name ( rawValue : notificationName ) , object : callToken )
}
private func dismissCallInterstitial ( callToken : String ) {
AssertIsOnMainThread ( )
let notificationName = CallService . dismissCallInterstitialNotificationName ( )
NotificationCenter . default . post ( name : NSNotification . Name ( rawValue : notificationName ) , object : callToken )
}
}