2020-04-02 09:30:16 +00:00
package eu.siacs.conversations.xmpp.jingle ;
import android.util.Log ;
2020-04-02 19:12:38 +00:00
import com.google.common.collect.ImmutableList ;
import com.google.common.collect.ImmutableMap ;
2020-04-03 08:46:42 +00:00
2020-04-04 13:30:13 +00:00
import org.webrtc.IceCandidate ;
2020-04-07 11:15:24 +00:00
import org.webrtc.PeerConnection ;
2020-04-02 19:12:38 +00:00
2020-04-06 08:26:29 +00:00
import java.util.ArrayDeque ;
import java.util.Arrays ;
2020-04-02 19:12:38 +00:00
import java.util.Collection ;
2020-04-04 13:30:13 +00:00
import java.util.Collections ;
2020-04-06 08:26:29 +00:00
import java.util.List ;
2020-04-02 19:12:38 +00:00
import java.util.Map ;
2020-04-02 09:30:16 +00:00
import eu.siacs.conversations.Config ;
2020-04-08 15:52:47 +00:00
import eu.siacs.conversations.entities.Account ;
2020-04-02 14:29:33 +00:00
import eu.siacs.conversations.xml.Element ;
2020-04-02 19:12:38 +00:00
import eu.siacs.conversations.xml.Namespace ;
2020-04-08 15:52:47 +00:00
import eu.siacs.conversations.xmpp.OnIqPacketReceived ;
2020-04-06 08:26:29 +00:00
import eu.siacs.conversations.xmpp.jingle.stanzas.Group ;
2020-04-03 08:46:42 +00:00
import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo ;
2020-04-02 09:30:16 +00:00
import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket ;
2020-04-07 19:26:51 +00:00
import eu.siacs.conversations.xmpp.jingle.stanzas.Reason ;
2020-04-08 15:52:47 +00:00
import eu.siacs.conversations.xmpp.stanzas.IqPacket ;
2020-04-02 19:12:38 +00:00
import eu.siacs.conversations.xmpp.stanzas.MessagePacket ;
2020-04-02 14:29:33 +00:00
import rocks.xmpp.addr.Jid ;
2020-04-02 09:30:16 +00:00
2020-04-06 11:01:17 +00:00
public class JingleRtpConnection extends AbstractJingleConnection implements WebRTCWrapper . EventCallback {
2020-04-02 09:30:16 +00:00
2020-04-02 19:12:38 +00:00
private static final Map < State , Collection < State > > VALID_TRANSITIONS ;
static {
final ImmutableMap . Builder < State , Collection < State > > transitionBuilder = new ImmutableMap . Builder < > ( ) ;
transitionBuilder . put ( State . NULL , ImmutableList . of ( State . PROPOSED , State . SESSION_INITIALIZED ) ) ;
2020-04-08 07:42:06 +00:00
transitionBuilder . put ( State . PROPOSED , ImmutableList . of ( State . ACCEPTED , State . PROCEED , State . REJECTED , State . RETRACTED ) ) ;
2020-04-08 13:27:17 +00:00
transitionBuilder . put ( State . PROCEED , ImmutableList . of ( State . SESSION_INITIALIZED_PRE_APPROVED ) ) ;
transitionBuilder . put ( State . SESSION_INITIALIZED , ImmutableList . of ( State . SESSION_ACCEPTED , State . TERMINATED_CANCEL_OR_TIMEOUT ) ) ;
transitionBuilder . put ( State . SESSION_INITIALIZED_PRE_APPROVED , ImmutableList . of ( State . SESSION_ACCEPTED , State . TERMINATED_CANCEL_OR_TIMEOUT ) ) ;
transitionBuilder . put ( State . SESSION_ACCEPTED , ImmutableList . of ( State . TERMINATED_SUCCESS , State . TERMINATED_CONNECTIVITY_ERROR ) ) ;
2020-04-02 19:12:38 +00:00
VALID_TRANSITIONS = transitionBuilder . build ( ) ;
}
2020-04-06 11:01:17 +00:00
private final WebRTCWrapper webRTCWrapper = new WebRTCWrapper ( this ) ;
2020-04-06 08:26:29 +00:00
private final ArrayDeque < IceCandidate > pendingIceCandidates = new ArrayDeque < > ( ) ;
2020-04-06 11:01:17 +00:00
private State state = State . NULL ;
private RtpContentMap initiatorRtpContentMap ;
private RtpContentMap responderRtpContentMap ;
2020-04-06 08:26:29 +00:00
2020-04-02 09:30:16 +00:00
2020-04-03 06:16:55 +00:00
public JingleRtpConnection ( JingleConnectionManager jingleConnectionManager , Id id , Jid initiator ) {
super ( jingleConnectionManager , id , initiator ) ;
2020-04-02 09:30:16 +00:00
}
2020-04-08 15:52:47 +00:00
private static State reasonToState ( Reason reason ) {
switch ( reason ) {
case SUCCESS :
return State . TERMINATED_SUCCESS ;
case DECLINE :
case BUSY :
return State . TERMINATED_DECLINED_OR_BUSY ;
case CANCEL :
case TIMEOUT :
return State . TERMINATED_CANCEL_OR_TIMEOUT ;
default :
return State . TERMINATED_CONNECTIVITY_ERROR ;
}
}
2020-04-02 09:30:16 +00:00
@Override
void deliverPacket ( final JinglePacket jinglePacket ) {
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : packet delivered to JingleRtpConnection " ) ;
2020-04-03 08:46:42 +00:00
switch ( jinglePacket . getAction ( ) ) {
case SESSION_INITIATE :
receiveSessionInitiate ( jinglePacket ) ;
break ;
2020-04-06 08:26:29 +00:00
case TRANSPORT_INFO :
receiveTransportInfo ( jinglePacket ) ;
break ;
2020-04-06 13:45:06 +00:00
case SESSION_ACCEPT :
receiveSessionAccept ( jinglePacket ) ;
break ;
2020-04-07 19:26:51 +00:00
case SESSION_TERMINATE :
receiveSessionTerminate ( jinglePacket ) ;
break ;
2020-04-03 08:46:42 +00:00
default :
Log . d ( Config . LOGTAG , String . format ( " %s: received unhandled jingle action %s " , id . account . getJid ( ) . asBareJid ( ) , jinglePacket . getAction ( ) ) ) ;
break ;
}
}
2020-04-07 19:26:51 +00:00
private void receiveSessionTerminate ( final JinglePacket jinglePacket ) {
final Reason reason = jinglePacket . getReason ( ) ;
2020-04-08 13:27:17 +00:00
final State previous = this . state ;
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : received session terminate reason= " + reason + " while in state " + previous ) ;
webRTCWrapper . close ( ) ;
transitionOrThrow ( reasonToState ( reason ) ) ;
if ( previous = = State . PROPOSED | | previous = = State . SESSION_INITIALIZED ) {
xmppConnectionService . getNotificationService ( ) . cancelIncomingCallNotification ( ) ;
}
jingleConnectionManager . finishConnection ( this ) ;
}
2020-04-06 08:26:29 +00:00
private void receiveTransportInfo ( final JinglePacket jinglePacket ) {
2020-04-08 13:27:17 +00:00
if ( isInState ( State . SESSION_INITIALIZED , State . SESSION_INITIALIZED_PRE_APPROVED , State . SESSION_ACCEPTED ) ) {
2020-04-06 08:26:29 +00:00
final RtpContentMap contentMap ;
try {
contentMap = RtpContentMap . of ( jinglePacket ) ;
} catch ( IllegalArgumentException | NullPointerException e ) {
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : improperly formatted contents " , e ) ;
return ;
}
2020-04-06 13:45:06 +00:00
final RtpContentMap rtpContentMap = isInitiator ( ) ? this . initiatorRtpContentMap : this . responderRtpContentMap ;
final Group originalGroup = rtpContentMap ! = null ? rtpContentMap . group : null ;
2020-04-06 08:26:29 +00:00
final List < String > identificationTags = originalGroup = = null ? Collections . emptyList ( ) : originalGroup . getIdentificationTags ( ) ;
if ( identificationTags . size ( ) = = 0 ) {
2020-04-06 11:01:17 +00:00
Log . w ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : no identification tags found in initial offer. we won't be able to calculate mLineIndices " ) ;
2020-04-06 08:26:29 +00:00
}
2020-04-06 11:01:17 +00:00
for ( final Map . Entry < String , RtpContentMap . DescriptionTransport > content : contentMap . contents . entrySet ( ) ) {
2020-04-06 08:26:29 +00:00
final String ufrag = content . getValue ( ) . transport . getAttribute ( " ufrag " ) ;
2020-04-06 11:01:17 +00:00
for ( final IceUdpTransportInfo . Candidate candidate : content . getValue ( ) . transport . getCandidates ( ) ) {
2020-04-06 08:26:29 +00:00
final String sdp = candidate . toSdpAttribute ( ufrag ) ;
final String sdpMid = content . getKey ( ) ;
final int mLineIndex = identificationTags . indexOf ( sdpMid ) ;
final IceCandidate iceCandidate = new IceCandidate ( sdpMid , mLineIndex , sdp ) ;
2020-04-06 11:01:17 +00:00
Log . d ( Config . LOGTAG , " received candidate: " + iceCandidate ) ;
2020-04-06 08:26:29 +00:00
if ( isInState ( State . SESSION_ACCEPTED ) ) {
2020-04-06 11:01:17 +00:00
this . webRTCWrapper . addIceCandidate ( iceCandidate ) ;
2020-04-06 08:26:29 +00:00
} else {
this . pendingIceCandidates . push ( iceCandidate ) ;
}
}
}
} else {
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : received transport info while in state= " + this . state ) ;
}
}
2020-04-03 08:46:42 +00:00
private void receiveSessionInitiate ( final JinglePacket jinglePacket ) {
if ( isInitiator ( ) ) {
Log . d ( Config . LOGTAG , String . format ( " %s: received session-initiate even though we were initiating " , id . account . getJid ( ) . asBareJid ( ) ) ) ;
//TODO respond with out-of-order
return ;
}
2020-04-05 08:20:34 +00:00
final RtpContentMap contentMap ;
2020-04-03 08:46:42 +00:00
try {
2020-04-05 08:20:34 +00:00
contentMap = RtpContentMap . of ( jinglePacket ) ;
2020-04-06 08:26:29 +00:00
contentMap . requireContentDescriptions ( ) ;
} catch ( IllegalArgumentException | IllegalStateException | NullPointerException e ) {
2020-04-04 13:30:13 +00:00
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : improperly formatted contents " , e ) ;
2020-04-03 08:46:42 +00:00
return ;
}
2020-04-05 08:20:34 +00:00
Log . d ( Config . LOGTAG , " processing session-init with " + contentMap . contents . size ( ) + " contents " ) ;
2020-04-08 13:27:17 +00:00
final State target ;
if ( this . state = = State . PROCEED ) {
target = State . SESSION_INITIALIZED_PRE_APPROVED ;
} else {
target = State . SESSION_INITIALIZED ;
}
if ( transition ( target ) ) {
2020-04-06 11:01:17 +00:00
this . initiatorRtpContentMap = contentMap ;
2020-04-08 13:27:17 +00:00
if ( target = = State . SESSION_INITIALIZED_PRE_APPROVED ) {
2020-04-06 11:01:17 +00:00
Log . d ( Config . LOGTAG , " automatically accepting " ) ;
2020-04-03 08:46:42 +00:00
sendSessionAccept ( ) ;
} else {
2020-04-06 11:01:17 +00:00
Log . d ( Config . LOGTAG , " start ringing " ) ;
2020-04-03 08:46:42 +00:00
//TODO start ringing
}
} else {
Log . d ( Config . LOGTAG , String . format ( " %s: received session-initiate while in state %s " , id . account . getJid ( ) . asBareJid ( ) , state ) ) ;
}
2020-04-02 09:30:16 +00:00
}
2020-04-02 14:29:33 +00:00
2020-04-06 13:45:06 +00:00
private void receiveSessionAccept ( final JinglePacket jinglePacket ) {
if ( ! isInitiator ( ) ) {
Log . d ( Config . LOGTAG , String . format ( " %s: received session-accept even though we were responding " , id . account . getJid ( ) . asBareJid ( ) ) ) ;
//TODO respond with out-of-order
return ;
}
final RtpContentMap contentMap ;
try {
contentMap = RtpContentMap . of ( jinglePacket ) ;
contentMap . requireContentDescriptions ( ) ;
} catch ( IllegalArgumentException | IllegalStateException | NullPointerException e ) {
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : improperly formatted contents " , e ) ;
return ;
}
Log . d ( Config . LOGTAG , " processing session-accept with " + contentMap . contents . size ( ) + " contents " ) ;
if ( transition ( State . SESSION_ACCEPTED ) ) {
receiveSessionAccept ( contentMap ) ;
} else {
Log . d ( Config . LOGTAG , String . format ( " %s: received session-accept while in state %s " , id . account . getJid ( ) . asBareJid ( ) , state ) ) ;
//TODO out-of-order
}
}
private void receiveSessionAccept ( final RtpContentMap contentMap ) {
this . responderRtpContentMap = contentMap ;
org . webrtc . SessionDescription answer = new org . webrtc . SessionDescription (
org . webrtc . SessionDescription . Type . ANSWER ,
SessionDescription . of ( contentMap ) . toString ( )
) ;
try {
this . webRTCWrapper . setRemoteDescription ( answer ) . get ( ) ;
} catch ( Exception e ) {
Log . d ( Config . LOGTAG , " unable to receive session accept " , e ) ;
}
}
2020-04-06 11:01:17 +00:00
private void sendSessionAccept ( ) {
final RtpContentMap rtpContentMap = this . initiatorRtpContentMap ;
if ( rtpContentMap = = null ) {
2020-04-06 13:45:06 +00:00
throw new IllegalStateException ( " initiator RTP Content Map has not been set " ) ;
2020-04-06 11:01:17 +00:00
}
2020-04-09 05:38:12 +00:00
final SessionDescription offer ;
try {
offer = SessionDescription . of ( rtpContentMap ) ;
} catch ( final IllegalArgumentException e ) {
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : unable to process offer " , e ) ;
//TODO terminate session with application error
return ;
}
sendSessionAccept ( offer ) ;
}
private void sendSessionAccept ( SessionDescription offer ) {
2020-04-08 15:52:47 +00:00
discoverIceServers ( iceServers - > {
setupWebRTC ( iceServers ) ;
2020-04-09 05:38:12 +00:00
final org . webrtc . SessionDescription sdp = new org . webrtc . SessionDescription (
2020-04-08 15:52:47 +00:00
org . webrtc . SessionDescription . Type . OFFER ,
2020-04-09 05:38:12 +00:00
offer . toString ( )
2020-04-08 15:52:47 +00:00
) ;
try {
2020-04-09 05:38:12 +00:00
this . webRTCWrapper . setRemoteDescription ( sdp ) . get ( ) ;
2020-04-08 15:52:47 +00:00
org . webrtc . SessionDescription webRTCSessionDescription = this . webRTCWrapper . createAnswer ( ) . get ( ) ;
final SessionDescription sessionDescription = SessionDescription . parse ( webRTCSessionDescription . description ) ;
final RtpContentMap respondingRtpContentMap = RtpContentMap . of ( sessionDescription ) ;
sendSessionAccept ( respondingRtpContentMap ) ;
this . webRTCWrapper . setLocalDescription ( webRTCSessionDescription ) ;
} catch ( Exception e ) {
Log . d ( Config . LOGTAG , " unable to send session accept " , e ) ;
2020-04-05 14:12:44 +00:00
2020-04-08 15:52:47 +00:00
}
} ) ;
2020-04-06 11:01:17 +00:00
}
2020-04-05 14:12:44 +00:00
2020-04-06 11:01:17 +00:00
private void sendSessionAccept ( final RtpContentMap rtpContentMap ) {
this . responderRtpContentMap = rtpContentMap ;
this . transitionOrThrow ( State . SESSION_ACCEPTED ) ;
final JinglePacket sessionAccept = rtpContentMap . toJinglePacket ( JinglePacket . Action . SESSION_ACCEPT , id . sessionId ) ;
Log . d ( Config . LOGTAG , sessionAccept . toString ( ) ) ;
send ( sessionAccept ) ;
2020-04-03 13:25:19 +00:00
}
2020-04-03 08:46:42 +00:00
void deliveryMessage ( final Jid from , final Element message ) {
2020-04-02 14:29:33 +00:00
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : delivered message to JingleRtpConnection " + message ) ;
2020-04-02 19:12:38 +00:00
switch ( message . getName ( ) ) {
case " propose " :
2020-04-03 08:46:42 +00:00
receivePropose ( from , message ) ;
2020-04-02 19:12:38 +00:00
break ;
2020-04-03 08:46:42 +00:00
case " proceed " :
receiveProceed ( from , message ) ;
2020-04-08 07:42:06 +00:00
break ;
case " retract " :
receiveRetract ( from , message ) ;
break ;
2020-04-08 09:29:01 +00:00
case " reject " :
receiveReject ( from , message ) ;
break ;
2020-04-08 10:17:46 +00:00
case " accept " :
receiveAccept ( from , message ) ;
break ;
2020-04-02 19:12:38 +00:00
default :
break ;
}
}
2020-04-08 10:17:46 +00:00
private void receiveAccept ( Jid from , Element message ) {
final boolean originatedFromMyself = from . asBareJid ( ) . equals ( id . account . getJid ( ) . asBareJid ( ) ) ;
if ( originatedFromMyself ) {
if ( transition ( State . ACCEPTED ) ) {
this . xmppConnectionService . getNotificationService ( ) . cancelIncomingCallNotification ( ) ;
this . jingleConnectionManager . finishConnection ( this ) ;
} else {
2020-04-08 13:27:17 +00:00
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : unable to transition to accept because already in state= " + this . state ) ;
2020-04-08 10:17:46 +00:00
}
} else {
2020-04-08 13:27:17 +00:00
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : ignoring 'accept' from " + from ) ;
2020-04-08 10:17:46 +00:00
}
}
2020-04-08 09:29:01 +00:00
private void receiveReject ( Jid from , Element message ) {
final boolean originatedFromMyself = from . asBareJid ( ) . equals ( id . account . getJid ( ) . asBareJid ( ) ) ;
//reject from another one of my clients
if ( originatedFromMyself ) {
if ( transition ( State . REJECTED ) ) {
this . xmppConnectionService . getNotificationService ( ) . cancelIncomingCallNotification ( ) ;
this . jingleConnectionManager . finishConnection ( this ) ;
} else {
2020-04-08 10:17:46 +00:00
Log . d ( Config . LOGTAG , " not able to transition into REJECTED because already in " + this . state ) ;
2020-04-08 09:29:01 +00:00
}
} else {
2020-04-08 10:17:46 +00:00
Log . d ( Config . LOGTAG , id . account . getJid ( ) + " : ignoring reject from " + from + " for session with " + id . with ) ;
2020-04-08 09:29:01 +00:00
}
}
2020-04-03 08:46:42 +00:00
private void receivePropose ( final Jid from , final Element propose ) {
final boolean originatedFromMyself = from . asBareJid ( ) . equals ( id . account . getJid ( ) . asBareJid ( ) ) ;
2020-04-06 11:01:17 +00:00
//TODO we can use initiator logic here
2020-04-03 08:46:42 +00:00
if ( originatedFromMyself ) {
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : saw proposal from mysql. ignoring " ) ;
} else if ( transition ( State . PROPOSED ) ) {
2020-04-07 09:36:28 +00:00
startRinging ( ) ;
2020-04-03 08:46:42 +00:00
} else {
Log . d ( Config . LOGTAG , id . account . getJid ( ) + " : ignoring session proposal because already in " + state ) ;
}
}
2020-04-07 09:36:28 +00:00
private void startRinging ( ) {
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : received call from " + id . with + " . start ringing " ) ;
2020-04-07 16:50:39 +00:00
xmppConnectionService . getNotificationService ( ) . showIncomingCallNotification ( id ) ;
2020-04-07 09:36:28 +00:00
}
2020-04-03 08:46:42 +00:00
private void receiveProceed ( final Jid from , final Element proceed ) {
if ( from . equals ( id . with ) ) {
if ( isInitiator ( ) ) {
2020-04-04 09:31:53 +00:00
if ( transition ( State . PROCEED ) ) {
2020-04-08 13:27:17 +00:00
this . sendSessionInitiate ( State . SESSION_INITIALIZED_PRE_APPROVED ) ;
2020-04-03 08:46:42 +00:00
} else {
Log . d ( Config . LOGTAG , String . format ( " %s: ignoring proceed because already in %s " , id . account . getJid ( ) . asBareJid ( ) , this . state ) ) ;
}
} else {
Log . d ( Config . LOGTAG , String . format ( " %s: ignoring proceed because we were not initializing " , id . account . getJid ( ) . asBareJid ( ) ) ) ;
}
2020-04-08 09:29:01 +00:00
} else if ( from . asBareJid ( ) . equals ( id . account . getJid ( ) . asBareJid ( ) ) ) {
if ( transition ( State . ACCEPTED ) ) {
2020-04-08 10:17:46 +00:00
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : moved session with " + id . with + " into state accepted after received carbon copied procced " ) ;
2020-04-08 09:29:01 +00:00
this . xmppConnectionService . getNotificationService ( ) . cancelIncomingCallNotification ( ) ;
this . jingleConnectionManager . finishConnection ( this ) ;
}
2020-04-03 08:46:42 +00:00
} else {
2020-04-08 09:29:01 +00:00
//TODO a carbon copied proceed from another client of mine has the same logic as `accept`
2020-04-03 08:46:42 +00:00
Log . d ( Config . LOGTAG , String . format ( " %s: ignoring proceed from %s. was expected from %s " , id . account . getJid ( ) . asBareJid ( ) , from , id . with ) ) ;
}
}
2020-04-08 07:42:06 +00:00
private void receiveRetract ( final Jid from , final Element retract ) {
if ( from . equals ( id . with ) ) {
if ( transition ( State . RETRACTED ) ) {
xmppConnectionService . getNotificationService ( ) . cancelIncomingCallNotification ( ) ;
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : session with " + id . with + " has been retracted " ) ;
//TODO create missed call notification/message
jingleConnectionManager . finishConnection ( this ) ;
} else {
Log . d ( Config . LOGTAG , " ignoring retract because already in " + this . state ) ;
}
} else {
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : received retract from " + from + " . expected retract from " + id . with + " . ignoring " ) ;
}
}
2020-04-08 13:27:17 +00:00
private void sendSessionInitiate ( final State targetState ) {
2020-04-05 08:20:34 +00:00
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : prepare session-initiate " ) ;
2020-04-08 15:52:47 +00:00
discoverIceServers ( iceServers - > {
setupWebRTC ( iceServers ) ;
try {
org . webrtc . SessionDescription webRTCSessionDescription = this . webRTCWrapper . createOffer ( ) . get ( ) ;
final SessionDescription sessionDescription = SessionDescription . parse ( webRTCSessionDescription . description ) ;
Log . d ( Config . LOGTAG , " description: " + webRTCSessionDescription . description ) ;
final RtpContentMap rtpContentMap = RtpContentMap . of ( sessionDescription ) ;
sendSessionInitiate ( rtpContentMap , targetState ) ;
this . webRTCWrapper . setLocalDescription ( webRTCSessionDescription ) . get ( ) ;
} catch ( Exception e ) {
Log . d ( Config . LOGTAG , " unable to sendSessionInitiate " , e ) ;
}
} ) ;
2020-04-05 08:20:34 +00:00
}
2020-04-08 13:27:17 +00:00
private void sendSessionInitiate ( RtpContentMap rtpContentMap , final State targetState ) {
2020-04-06 11:01:17 +00:00
this . initiatorRtpContentMap = rtpContentMap ;
2020-04-08 13:27:17 +00:00
this . transitionOrThrow ( targetState ) ;
2020-04-05 11:58:05 +00:00
final JinglePacket sessionInitiate = rtpContentMap . toJinglePacket ( JinglePacket . Action . SESSION_INITIATE , id . sessionId ) ;
Log . d ( Config . LOGTAG , sessionInitiate . toString ( ) ) ;
send ( sessionInitiate ) ;
2020-04-03 08:46:42 +00:00
}
2020-04-08 13:27:17 +00:00
private void sendSessionTerminate ( final Reason reason ) {
final State target = reasonToState ( reason ) ;
transitionOrThrow ( target ) ;
final JinglePacket jinglePacket = new JinglePacket ( JinglePacket . Action . SESSION_TERMINATE , id . sessionId ) ;
jinglePacket . setReason ( reason ) ;
send ( jinglePacket ) ;
Log . d ( Config . LOGTAG , jinglePacket . toString ( ) ) ;
jingleConnectionManager . finishConnection ( this ) ;
}
2020-04-05 11:58:05 +00:00
private void sendTransportInfo ( final String contentName , IceUdpTransportInfo . Candidate candidate ) {
final RtpContentMap transportInfo ;
try {
2020-04-06 13:45:06 +00:00
final RtpContentMap rtpContentMap = isInitiator ( ) ? this . initiatorRtpContentMap : this . responderRtpContentMap ;
transportInfo = rtpContentMap . transportInfo ( contentName , candidate ) ;
2020-04-05 11:58:05 +00:00
} catch ( Exception e ) {
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : unable to prepare transport-info from candidate for content= " + contentName ) ;
return ;
}
final JinglePacket jinglePacket = transportInfo . toJinglePacket ( JinglePacket . Action . TRANSPORT_INFO , id . sessionId ) ;
Log . d ( Config . LOGTAG , jinglePacket . toString ( ) ) ;
send ( jinglePacket ) ;
}
private void send ( final JinglePacket jinglePacket ) {
jinglePacket . setTo ( id . with ) ;
//TODO track errors
xmppConnectionService . sendIqPacket ( id . account , jinglePacket , null ) ;
}
2020-04-07 11:15:24 +00:00
public RtpEndUserState getEndUserState ( ) {
switch ( this . state ) {
case PROPOSED :
2020-04-08 13:27:17 +00:00
case SESSION_INITIALIZED :
2020-04-07 11:15:24 +00:00
if ( isInitiator ( ) ) {
return RtpEndUserState . RINGING ;
} else {
return RtpEndUserState . INCOMING_CALL ;
}
case PROCEED :
if ( isInitiator ( ) ) {
return RtpEndUserState . CONNECTING ;
} else {
return RtpEndUserState . ACCEPTING_CALL ;
}
2020-04-08 13:27:17 +00:00
case SESSION_INITIALIZED_PRE_APPROVED :
2020-04-07 11:15:24 +00:00
return RtpEndUserState . CONNECTING ;
case SESSION_ACCEPTED :
final PeerConnection . PeerConnectionState state = webRTCWrapper . getState ( ) ;
if ( state = = PeerConnection . PeerConnectionState . CONNECTED ) {
return RtpEndUserState . CONNECTED ;
} else if ( state = = PeerConnection . PeerConnectionState . NEW | | state = = PeerConnection . PeerConnectionState . CONNECTING ) {
return RtpEndUserState . CONNECTING ;
2020-04-07 12:22:12 +00:00
} else if ( state = = PeerConnection . PeerConnectionState . CLOSED ) {
return RtpEndUserState . ENDING_CALL ;
2020-04-08 10:17:46 +00:00
} else if ( state = = PeerConnection . PeerConnectionState . FAILED ) {
return RtpEndUserState . CONNECTIVITY_ERROR ;
2020-04-07 11:15:24 +00:00
} else {
2020-04-07 19:26:51 +00:00
return RtpEndUserState . ENDING_CALL ;
}
case REJECTED :
case TERMINATED_DECLINED_OR_BUSY :
if ( isInitiator ( ) ) {
return RtpEndUserState . DECLINED_OR_BUSY ;
} else {
return RtpEndUserState . ENDED ;
2020-04-07 11:15:24 +00:00
}
2020-04-07 19:26:51 +00:00
case TERMINATED_SUCCESS :
case ACCEPTED :
case RETRACTED :
case TERMINATED_CANCEL_OR_TIMEOUT :
return RtpEndUserState . ENDED ;
case TERMINATED_CONNECTIVITY_ERROR :
return RtpEndUserState . CONNECTIVITY_ERROR ;
2020-04-07 11:15:24 +00:00
}
2020-04-07 19:26:51 +00:00
throw new IllegalStateException ( String . format ( " %s has no equivalent EndUserState " , this . state ) ) ;
2020-04-07 11:15:24 +00:00
}
2020-04-05 11:58:05 +00:00
2020-04-07 11:15:24 +00:00
public void acceptCall ( ) {
2020-04-02 19:12:38 +00:00
switch ( this . state ) {
case PROPOSED :
2020-04-07 11:15:24 +00:00
acceptCallFromProposed ( ) ;
2020-04-02 19:12:38 +00:00
break ;
case SESSION_INITIALIZED :
2020-04-07 11:15:24 +00:00
acceptCallFromSessionInitialized ( ) ;
2020-04-02 19:12:38 +00:00
break ;
default :
2020-04-07 19:26:51 +00:00
throw new IllegalStateException ( " Can not accept call from " + this . state ) ;
2020-04-02 19:12:38 +00:00
}
}
2020-04-07 11:15:24 +00:00
public void rejectCall ( ) {
2020-04-07 19:26:51 +00:00
switch ( this . state ) {
case PROPOSED :
rejectCallFromProposed ( ) ;
break ;
default :
throw new IllegalStateException ( " Can not reject call from " + this . state ) ;
}
2020-04-07 11:15:24 +00:00
}
2020-04-07 12:22:12 +00:00
public void endCall ( ) {
2020-04-08 13:27:17 +00:00
if ( isInitiator ( ) & & isInState ( State . SESSION_INITIALIZED ) ) {
webRTCWrapper . close ( ) ;
sendSessionTerminate ( Reason . CANCEL ) ;
} else if ( isInState ( State . SESSION_INITIALIZED , State . SESSION_INITIALIZED_PRE_APPROVED , State . SESSION_ACCEPTED ) ) {
2020-04-07 12:22:12 +00:00
webRTCWrapper . close ( ) ;
2020-04-08 13:27:17 +00:00
sendSessionTerminate ( Reason . SUCCESS ) ;
2020-04-07 12:22:12 +00:00
} else {
2020-04-08 13:27:17 +00:00
throw new IllegalStateException ( " called 'endCall' while in state " + this . state ) ;
2020-04-07 12:22:12 +00:00
}
}
2020-04-08 15:52:47 +00:00
private void setupWebRTC ( final List < PeerConnection . IceServer > iceServers ) {
2020-04-06 11:01:17 +00:00
this . webRTCWrapper . setup ( this . xmppConnectionService ) ;
2020-04-08 15:52:47 +00:00
this . webRTCWrapper . initializePeerConnection ( iceServers ) ;
2020-04-04 13:30:13 +00:00
}
2020-04-07 11:15:24 +00:00
private void acceptCallFromProposed ( ) {
2020-04-02 19:12:38 +00:00
transitionOrThrow ( State . PROCEED ) ;
2020-04-07 16:50:39 +00:00
xmppConnectionService . getNotificationService ( ) . cancelIncomingCallNotification ( ) ;
2020-04-08 10:17:46 +00:00
this . sendJingleMessage ( " accept " , id . account . getJid ( ) . asBareJid ( ) ) ;
2020-04-07 19:26:51 +00:00
this . sendJingleMessage ( " proceed " ) ;
}
private void rejectCallFromProposed ( ) {
transitionOrThrow ( State . REJECTED ) ;
xmppConnectionService . getNotificationService ( ) . cancelIncomingCallNotification ( ) ;
this . sendJingleMessage ( " reject " ) ;
jingleConnectionManager . finishConnection ( this ) ;
}
private void sendJingleMessage ( final String action ) {
2020-04-08 10:17:46 +00:00
sendJingleMessage ( action , id . with ) ;
}
private void sendJingleMessage ( final String action , final Jid to ) {
2020-04-02 19:12:38 +00:00
final MessagePacket messagePacket = new MessagePacket ( ) ;
2020-04-08 09:29:01 +00:00
messagePacket . setType ( MessagePacket . TYPE_CHAT ) ; //we want to carbon copy those
2020-04-08 10:17:46 +00:00
messagePacket . setTo ( to ) ;
2020-04-07 19:26:51 +00:00
messagePacket . addChild ( action , Namespace . JINGLE_MESSAGE ) . setAttribute ( " id " , id . sessionId ) ;
2020-04-02 19:12:38 +00:00
Log . d ( Config . LOGTAG , messagePacket . toString ( ) ) ;
xmppConnectionService . sendMessagePacket ( id . account , messagePacket ) ;
2020-04-02 14:29:33 +00:00
}
2020-04-02 19:12:38 +00:00
2020-04-07 11:15:24 +00:00
private void acceptCallFromSessionInitialized ( ) {
2020-04-07 16:50:39 +00:00
xmppConnectionService . getNotificationService ( ) . cancelIncomingCallNotification ( ) ;
throw new IllegalStateException ( " accepting from this state has not been implemented yet " ) ;
2020-04-02 19:12:38 +00:00
}
2020-04-06 08:26:29 +00:00
private synchronized boolean isInState ( State . . . state ) {
return Arrays . asList ( state ) . contains ( this . state ) ;
}
2020-04-02 19:12:38 +00:00
private synchronized boolean transition ( final State target ) {
final Collection < State > validTransitions = VALID_TRANSITIONS . get ( this . state ) ;
if ( validTransitions ! = null & & validTransitions . contains ( target ) ) {
this . state = target ;
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : transitioned into " + target ) ;
2020-04-07 11:15:24 +00:00
updateEndUserState ( ) ;
2020-04-02 19:12:38 +00:00
return true ;
} else {
return false ;
}
}
2020-04-04 09:31:53 +00:00
public void transitionOrThrow ( final State target ) {
2020-04-02 19:12:38 +00:00
if ( ! transition ( target ) ) {
throw new IllegalStateException ( String . format ( " Unable to transition from %s to %s " , this . state , target ) ) ;
}
}
2020-04-06 11:01:17 +00:00
@Override
public void onIceCandidate ( final IceCandidate iceCandidate ) {
final IceUdpTransportInfo . Candidate candidate = IceUdpTransportInfo . Candidate . fromSdpAttribute ( iceCandidate . sdp ) ;
2020-04-06 13:45:06 +00:00
Log . d ( Config . LOGTAG , " sending candidate: " + iceCandidate . toString ( ) ) ;
2020-04-06 11:01:17 +00:00
sendTransportInfo ( iceCandidate . sdpMid , candidate ) ;
}
2020-04-07 11:15:24 +00:00
@Override
2020-04-08 13:27:17 +00:00
public void onConnectionChange ( final PeerConnection . PeerConnectionState newState ) {
2020-04-07 19:26:51 +00:00
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : PeerConnectionState changed to " + newState ) ;
2020-04-07 11:15:24 +00:00
updateEndUserState ( ) ;
2020-04-08 13:27:17 +00:00
if ( newState = = PeerConnection . PeerConnectionState . FAILED ) { //TODO guard this in isState(initiated,initated_approved,accepted) otherwise it might fire too late
sendSessionTerminate ( Reason . CONNECTIVITY_ERROR ) ;
}
2020-04-07 11:15:24 +00:00
}
private void updateEndUserState ( ) {
2020-04-08 07:42:06 +00:00
xmppConnectionService . notifyJingleRtpConnectionUpdate ( id . account , id . with , id . sessionId , getEndUserState ( ) ) ;
2020-04-07 11:15:24 +00:00
}
2020-04-08 15:52:47 +00:00
private void discoverIceServers ( final OnIceServersDiscovered onIceServersDiscovered ) {
if ( id . account . getXmppConnection ( ) . getFeatures ( ) . extendedServiceDiscovery ( ) ) {
final IqPacket request = new IqPacket ( IqPacket . TYPE . GET ) ;
request . setTo ( Jid . of ( id . account . getJid ( ) . getDomain ( ) ) ) ;
request . addChild ( " services " , Namespace . EXTERNAL_SERVICE_DISCOVERY ) ;
xmppConnectionService . sendIqPacket ( id . account , request , ( account , response ) - > {
ImmutableList . Builder < PeerConnection . IceServer > listBuilder = new ImmutableList . Builder < > ( ) ;
if ( response . getType ( ) = = IqPacket . TYPE . RESULT ) {
final Element services = response . findChild ( " services " , Namespace . EXTERNAL_SERVICE_DISCOVERY ) ;
final List < Element > children = services = = null ? Collections . emptyList ( ) : services . getChildren ( ) ;
for ( final Element child : children ) {
if ( " service " . equals ( child . getName ( ) ) ) {
final String type = child . getAttribute ( " type " ) ;
final String host = child . getAttribute ( " host " ) ;
final String port = child . getAttribute ( " port " ) ;
final String transport = child . getAttribute ( " transport " ) ;
final String username = child . getAttribute ( " username " ) ;
final String password = child . getAttribute ( " password " ) ;
if ( Arrays . asList ( " stun " , " type " ) . contains ( type ) & & host ! = null & & port ! = null & & " udp " . equals ( transport ) ) {
PeerConnection . IceServer . Builder iceServerBuilder = PeerConnection . IceServer . builder ( String . format ( " %s:%s:%s " , type , host , port ) ) ;
if ( username ! = null & & password ! = null ) {
iceServerBuilder . setUsername ( username ) ;
iceServerBuilder . setPassword ( password ) ;
}
final PeerConnection . IceServer iceServer = iceServerBuilder . createIceServer ( ) ;
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : discovered ICE Server: " + iceServer ) ;
listBuilder . add ( iceServer ) ;
}
}
}
}
List < PeerConnection . IceServer > iceServers = listBuilder . build ( ) ;
if ( iceServers . size ( ) = = 0 ) {
Log . w ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : no ICE server found " + response ) ;
}
onIceServersDiscovered . onIceServersDiscovered ( iceServers ) ;
} ) ;
} else {
Log . w ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : has no external service discovery " ) ;
onIceServersDiscovered . onIceServersDiscovered ( Collections . emptyList ( ) ) ;
}
}
private interface OnIceServersDiscovered {
void onIceServersDiscovered ( List < PeerConnection . IceServer > iceServers ) ;
}
2020-04-02 09:30:16 +00:00
}