2020-04-02 09:30:16 +00:00
package eu.siacs.conversations.xmpp.jingle ;
2020-04-12 15:12:59 +00:00
import android.os.SystemClock ;
2020-04-02 09:30:16 +00:00
import android.util.Log ;
2020-04-14 17:06:39 +00:00
import com.google.common.base.Optional ;
2020-04-15 08:49:38 +00:00
import com.google.common.base.Preconditions ;
2020-04-09 11:27:13 +00:00
import com.google.common.base.Strings ;
2020-04-15 08:49:38 +00:00
import com.google.common.collect.Collections2 ;
2020-04-02 19:12:38 +00:00
import com.google.common.collect.ImmutableList ;
import com.google.common.collect.ImmutableMap ;
2020-04-15 08:49:38 +00:00
import com.google.common.collect.Sets ;
2020-04-09 11:27:13 +00:00
import com.google.common.primitives.Ints ;
2020-04-03 08:46:42 +00:00
2020-04-14 17:06:39 +00:00
import org.webrtc.EglBase ;
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-14 17:06:39 +00:00
import org.webrtc.VideoTrack ;
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-13 10:02:34 +00:00
import java.util.Set ;
2020-04-02 19:12:38 +00:00
2020-04-02 09:30:16 +00:00
import eu.siacs.conversations.Config ;
2020-04-12 15:12:59 +00:00
import eu.siacs.conversations.entities.Conversation ;
import eu.siacs.conversations.entities.Conversational ;
import eu.siacs.conversations.entities.Message ;
import eu.siacs.conversations.entities.RtpSessionStatus ;
2020-04-13 10:02:34 +00:00
import eu.siacs.conversations.services.AppRTCAudioManager ;
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-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-15 08:49:38 +00:00
import eu.siacs.conversations.xmpp.jingle.stanzas.Propose ;
2020-04-07 19:26:51 +00:00
import eu.siacs.conversations.xmpp.jingle.stanzas.Reason ;
2020-04-15 08:49:38 +00:00
import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription ;
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-10 17:22:29 +00:00
public static final List < State > STATES_SHOWING_ONGOING_CALL = Arrays . asList (
State . PROCEED ,
State . SESSION_INITIALIZED ,
State . SESSION_INITIALIZED_PRE_APPROVED ,
State . SESSION_ACCEPTED
) ;
2020-04-12 07:59:32 +00:00
private static final List < State > TERMINATED = Arrays . asList (
2020-04-14 07:53:01 +00:00
State . TERMINATED_SUCCESS ,
2020-04-12 07:59:32 +00:00
State . TERMINATED_DECLINED_OR_BUSY ,
State . TERMINATED_CONNECTIVITY_ERROR ,
State . TERMINATED_CANCEL_OR_TIMEOUT ,
State . TERMINATED_APPLICATION_FAILURE
) ;
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 < > ( ) ;
2020-04-09 13:22:03 +00:00
transitionBuilder . put ( State . NULL , ImmutableList . of (
State . PROPOSED ,
State . SESSION_INITIALIZED ,
State . TERMINATED_APPLICATION_FAILURE
) ) ;
transitionBuilder . put ( State . PROPOSED , ImmutableList . of (
State . ACCEPTED ,
State . PROCEED ,
State . REJECTED ,
State . RETRACTED ,
2020-04-13 16:30:12 +00:00
State . TERMINATED_APPLICATION_FAILURE ,
State . TERMINATED_CONNECTIVITY_ERROR //only used when the xmpp connection rebinds
2020-04-09 13:22:03 +00:00
) ) ;
transitionBuilder . put ( State . PROCEED , ImmutableList . of (
State . SESSION_INITIALIZED_PRE_APPROVED ,
State . TERMINATED_SUCCESS ,
2020-04-10 05:07:22 +00:00
State . TERMINATED_APPLICATION_FAILURE ,
State . TERMINATED_CONNECTIVITY_ERROR //at this state used for error bounces of the proceed message
2020-04-09 13:22:03 +00:00
) ) ;
transitionBuilder . put ( State . SESSION_INITIALIZED , ImmutableList . of (
State . SESSION_ACCEPTED ,
2020-04-10 19:33:08 +00:00
State . TERMINATED_SUCCESS ,
2020-04-09 13:22:03 +00:00
State . TERMINATED_DECLINED_OR_BUSY ,
2020-04-10 19:33:08 +00:00
State . TERMINATED_CONNECTIVITY_ERROR , //at this state used for IQ errors and IQ timeouts
State . TERMINATED_CANCEL_OR_TIMEOUT ,
State . TERMINATED_APPLICATION_FAILURE
2020-04-09 13:22:03 +00:00
) ) ;
transitionBuilder . put ( State . SESSION_INITIALIZED_PRE_APPROVED , ImmutableList . of (
State . SESSION_ACCEPTED ,
2020-04-10 19:33:08 +00:00
State . TERMINATED_SUCCESS ,
2020-04-09 13:22:03 +00:00
State . TERMINATED_DECLINED_OR_BUSY ,
2020-04-10 19:33:08 +00:00
State . TERMINATED_CONNECTIVITY_ERROR , //at this state used for IQ errors and IQ timeouts
State . TERMINATED_CANCEL_OR_TIMEOUT ,
State . TERMINATED_APPLICATION_FAILURE
2020-04-09 13:22:03 +00:00
) ) ;
transitionBuilder . put ( State . SESSION_ACCEPTED , ImmutableList . of (
State . TERMINATED_SUCCESS ,
2020-04-10 19:33:08 +00:00
State . TERMINATED_DECLINED_OR_BUSY ,
2020-04-09 13:22:03 +00:00
State . TERMINATED_CONNECTIVITY_ERROR ,
2020-04-10 19:33:08 +00:00
State . TERMINATED_CANCEL_OR_TIMEOUT ,
2020-04-09 13:22:03 +00:00
State . TERMINATED_APPLICATION_FAILURE
) ) ;
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-12 15:12:59 +00:00
private final Message message ;
2020-04-06 11:01:17 +00:00
private State state = State . NULL ;
2020-04-15 08:49:38 +00:00
private Set < Media > proposedMedia ;
2020-04-06 11:01:17 +00:00
private RtpContentMap initiatorRtpContentMap ;
private RtpContentMap responderRtpContentMap ;
2020-04-12 15:12:59 +00:00
private long rtpConnectionStarted = 0 ; //time of 'connected'
2020-04-06 08:26:29 +00:00
2020-04-02 09:30:16 +00:00
2020-04-10 05:45:23 +00:00
JingleRtpConnection ( JingleConnectionManager jingleConnectionManager , Id id , Jid initiator ) {
2020-04-03 06:16:55 +00:00
super ( jingleConnectionManager , id , initiator ) ;
2020-04-12 15:12:59 +00:00
final Conversation conversation = jingleConnectionManager . getXmppConnectionService ( ) . findOrCreateConversation (
id . account ,
id . with . asBareJid ( ) ,
false ,
false
) ;
this . message = new Message (
conversation ,
isInitiator ( ) ? Message . STATUS_SEND : Message . STATUS_RECEIVED ,
Message . TYPE_RTP_SESSION ,
id . sessionId
) ;
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 ;
2020-04-09 13:22:03 +00:00
case FAILED_APPLICATION :
2020-04-15 10:07:19 +00:00
case SECURITY_ERROR :
case UNSUPPORTED_TRANSPORTS :
case UNSUPPORTED_APPLICATIONS :
2020-04-09 13:22:03 +00:00
return State . TERMINATED_APPLICATION_FAILURE ;
2020-04-08 15:52:47 +00:00
default :
return State . TERMINATED_CONNECTIVITY_ERROR ;
}
}
2020-04-02 09:30:16 +00:00
@Override
2020-04-14 07:53:01 +00:00
synchronized void deliverPacket ( final JinglePacket jinglePacket ) {
2020-04-02 09:30:16 +00:00
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 :
2020-04-09 13:22:03 +00:00
respondOk ( jinglePacket ) ;
2020-04-03 08:46:42 +00:00
Log . d ( Config . LOGTAG , String . format ( " %s: received unhandled jingle action %s " , id . account . getJid ( ) . asBareJid ( ) , jinglePacket . getAction ( ) ) ) ;
break ;
}
}
2020-04-13 16:30:12 +00:00
@Override
2020-04-14 07:53:01 +00:00
synchronized void notifyRebound ( ) {
2020-04-13 16:30:12 +00:00
if ( TERMINATED . contains ( this . state ) ) {
return ;
}
webRTCWrapper . close ( ) ;
2020-04-15 10:07:19 +00:00
if ( ! isInitiator ( ) & & isInState ( State . PROPOSED , State . SESSION_INITIALIZED ) ) {
2020-04-13 16:30:12 +00:00
xmppConnectionService . getNotificationService ( ) . cancelIncomingCallNotification ( ) ;
}
if ( isInState ( State . SESSION_INITIALIZED , State . SESSION_INITIALIZED_PRE_APPROVED , State . SESSION_ACCEPTED ) ) {
//we might have already changed resources (full jid) at this point; so this might not even reach the other party
sendSessionTerminate ( Reason . CONNECTIVITY_ERROR ) ;
} else {
transitionOrThrow ( State . TERMINATED_CONNECTIVITY_ERROR ) ;
jingleConnectionManager . finishConnection ( this ) ;
}
}
2020-04-07 19:26:51 +00:00
private void receiveSessionTerminate ( final JinglePacket jinglePacket ) {
2020-04-09 13:22:03 +00:00
respondOk ( jinglePacket ) ;
2020-04-12 07:59:32 +00:00
final JinglePacket . ReasonWrapper wrapper = jinglePacket . getReason ( ) ;
2020-04-08 13:27:17 +00:00
final State previous = this . state ;
2020-04-12 07:59:32 +00:00
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : received session terminate reason= " + wrapper . reason + " ( " + Strings . nullToEmpty ( wrapper . text ) + " ) while in state " + previous ) ;
if ( TERMINATED . contains ( previous ) ) {
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : ignoring session terminate because already in " + previous ) ;
return ;
}
2020-04-08 13:27:17 +00:00
webRTCWrapper . close ( ) ;
2020-04-12 15:12:59 +00:00
final State target = reasonToState ( wrapper . reason ) ;
transitionOrThrow ( target ) ;
writeLogMessage ( target ) ;
2020-04-08 13:27:17 +00:00
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-09 13:22:03 +00:00
respondOk ( jinglePacket ) ;
2020-04-06 08:26:29 +00:00
final RtpContentMap contentMap ;
try {
contentMap = RtpContentMap . of ( jinglePacket ) ;
} catch ( IllegalArgumentException | NullPointerException e ) {
2020-04-09 13:22:03 +00:00
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : improperly formatted contents; ignoring " , e ) ;
2020-04-06 08:26:29 +00:00
return ;
}
2020-04-09 07:29:07 +00:00
final RtpContentMap rtpContentMap = isInitiator ( ) ? this . responderRtpContentMap : this . initiatorRtpContentMap ;
2020-04-06 13:45:06 +00:00
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 ) ;
if ( isInState ( State . SESSION_ACCEPTED ) ) {
2020-04-09 07:29:07 +00:00
Log . d ( Config . LOGTAG , " received candidate: " + iceCandidate ) ;
2020-04-06 11:01:17 +00:00
this . webRTCWrapper . addIceCandidate ( iceCandidate ) ;
2020-04-06 08:26:29 +00:00
} else {
2020-04-09 07:29:07 +00:00
this . pendingIceCandidates . offer ( iceCandidate ) ;
2020-04-09 07:04:59 +00:00
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : put ICE candidate on backlog " ) ;
2020-04-06 08:26:29 +00:00
}
}
}
} else {
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : received transport info while in state= " + this . state ) ;
2020-04-10 05:07:22 +00:00
terminateWithOutOfOrder ( jinglePacket ) ;
2020-04-06 08:26:29 +00:00
}
}
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 ( ) ) ) ;
2020-04-10 05:07:22 +00:00
terminateWithOutOfOrder ( jinglePacket ) ;
2020-04-03 08:46:42 +00:00
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 ( ) ;
2020-04-10 05:45:23 +00:00
contentMap . requireDTLSFingerprint ( ) ;
} catch ( final IllegalArgumentException | IllegalStateException | NullPointerException e ) {
2020-04-09 13:22:03 +00:00
respondOk ( jinglePacket ) ;
2020-04-16 06:20:13 +00:00
sendSessionTerminate ( Reason . of ( e ) , e . getMessage ( ) ) ;
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 ) {
2020-04-15 10:07:19 +00:00
Preconditions . checkState (
proposedMedia ! = null & & proposedMedia . size ( ) > 0 ,
" proposed media must be set when processing pre-approved session-initiate "
) ;
if ( ! this . proposedMedia . equals ( contentMap . getMedia ( ) ) ) {
2020-04-15 16:28:04 +00:00
sendSessionTerminate ( Reason . SECURITY_ERROR , String . format (
2020-04-15 10:07:19 +00:00
" Your session proposal (Jingle Message Initiation) included media %s but your session-initiate was %s " ,
this . proposedMedia ,
contentMap . getMedia ( )
) ) ;
return ;
}
2020-04-08 13:27:17 +00:00
target = State . SESSION_INITIALIZED_PRE_APPROVED ;
} else {
target = State . SESSION_INITIALIZED ;
}
2020-04-15 20:40:37 +00:00
if ( transition ( target , ( ) - > this . initiatorRtpContentMap = contentMap ) ) {
2020-04-09 13:22:03 +00:00
respondOk ( jinglePacket ) ;
2020-04-08 13:27:17 +00:00
if ( target = = State . SESSION_INITIALIZED_PRE_APPROVED ) {
2020-04-09 07:04:59 +00:00
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : automatically accepting session-initiate " ) ;
2020-04-03 08:46:42 +00:00
sendSessionAccept ( ) ;
} else {
2020-04-09 07:04:59 +00:00
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : received not pre-approved session-initiate. start ringing " ) ;
startRinging ( ) ;
2020-04-03 08:46:42 +00:00
}
} else {
Log . d ( Config . LOGTAG , String . format ( " %s: received session-initiate while in state %s " , id . account . getJid ( ) . asBareJid ( ) , state ) ) ;
2020-04-10 05:07:22 +00:00
terminateWithOutOfOrder ( jinglePacket ) ;
2020-04-03 08:46:42 +00:00
}
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 ( ) ) ) ;
2020-04-10 05:07:22 +00:00
terminateWithOutOfOrder ( jinglePacket ) ;
2020-04-06 13:45:06 +00:00
return ;
}
final RtpContentMap contentMap ;
try {
contentMap = RtpContentMap . of ( jinglePacket ) ;
contentMap . requireContentDescriptions ( ) ;
2020-04-10 05:45:23 +00:00
contentMap . requireDTLSFingerprint ( ) ;
2020-04-16 06:20:13 +00:00
} catch ( final IllegalArgumentException | IllegalStateException | NullPointerException e ) {
2020-04-09 13:22:03 +00:00
respondOk ( jinglePacket ) ;
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : improperly formatted contents in session-accept " , e ) ;
webRTCWrapper . close ( ) ;
2020-04-16 06:20:13 +00:00
sendSessionTerminate ( Reason . of ( e ) , e . getMessage ( ) ) ;
return ;
}
final Set < Media > initiatorMedia = this . initiatorRtpContentMap . getMedia ( ) ;
if ( ! initiatorMedia . equals ( contentMap . getMedia ( ) ) ) {
sendSessionTerminate ( Reason . SECURITY_ERROR , String . format (
" Your session-included included media %s but our session-initiate was %s " ,
this . proposedMedia ,
contentMap . getMedia ( )
) ) ;
2020-04-06 13:45:06 +00:00
return ;
}
Log . d ( Config . LOGTAG , " processing session-accept with " + contentMap . contents . size ( ) + " contents " ) ;
if ( transition ( State . SESSION_ACCEPTED ) ) {
2020-04-09 13:22:03 +00:00
respondOk ( jinglePacket ) ;
2020-04-06 13:45:06 +00:00
receiveSessionAccept ( contentMap ) ;
} else {
Log . d ( Config . LOGTAG , String . format ( " %s: received session-accept while in state %s " , id . account . getJid ( ) . asBareJid ( ) , state ) ) ;
2020-04-09 13:22:03 +00:00
respondOk ( jinglePacket ) ;
2020-04-06 13:45:06 +00:00
}
}
private void receiveSessionAccept ( final RtpContentMap contentMap ) {
this . responderRtpContentMap = contentMap ;
2020-04-09 13:22:03 +00:00
final SessionDescription sessionDescription ;
try {
sessionDescription = SessionDescription . of ( contentMap ) ;
2020-04-10 05:53:29 +00:00
} catch ( final IllegalArgumentException | NullPointerException e ) {
2020-04-09 13:22:03 +00:00
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : unable convert offer from session-accept to SDP " , e ) ;
webRTCWrapper . close ( ) ;
2020-04-09 18:35:44 +00:00
sendSessionTerminate ( Reason . FAILED_APPLICATION , e . getMessage ( ) ) ;
2020-04-09 13:22:03 +00:00
return ;
}
2020-04-06 13:45:06 +00:00
org . webrtc . SessionDescription answer = new org . webrtc . SessionDescription (
org . webrtc . SessionDescription . Type . ANSWER ,
2020-04-09 13:22:03 +00:00
sessionDescription . toString ( )
2020-04-06 13:45:06 +00:00
) ;
try {
this . webRTCWrapper . setRemoteDescription ( answer ) . get ( ) ;
} catch ( Exception e ) {
2020-04-09 13:22:03 +00:00
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : unable to set remote description after receiving session-accept " , e ) ;
webRTCWrapper . close ( ) ;
sendSessionTerminate ( Reason . FAILED_APPLICATION ) ;
2020-04-06 13:45:06 +00:00
}
}
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 ) ;
2020-04-10 05:53:29 +00:00
} catch ( final IllegalArgumentException | NullPointerException e ) {
2020-04-09 13:22:03 +00:00
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : unable convert offer from session-initiate to SDP " , e ) ;
webRTCWrapper . close ( ) ;
2020-04-09 18:35:44 +00:00
sendSessionTerminate ( Reason . FAILED_APPLICATION , e . getMessage ( ) ) ;
2020-04-09 05:38:12 +00:00
return ;
}
2020-04-15 10:07:19 +00:00
sendSessionAccept ( rtpContentMap . getMedia ( ) , offer ) ;
2020-04-09 05:38:12 +00:00
}
2020-04-15 10:07:19 +00:00
private void sendSessionAccept ( final Set < Media > media , final SessionDescription offer ) {
discoverIceServers ( iceServers - > sendSessionAccept ( media , offer , iceServers ) ) ;
2020-04-14 07:06:07 +00:00
}
2020-04-05 14:12:44 +00:00
2020-04-15 10:07:19 +00:00
private synchronized void sendSessionAccept ( final Set < Media > media , final SessionDescription offer , final List < PeerConnection . IceServer > iceServers ) {
2020-04-14 07:53:01 +00:00
if ( TERMINATED . contains ( this . state ) ) {
2020-04-15 10:07:19 +00:00
Log . w ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : ICE servers got discovered when session was already terminated. nothing to do. " ) ;
2020-04-14 07:53:01 +00:00
return ;
}
2020-04-14 07:06:07 +00:00
try {
2020-04-15 10:07:19 +00:00
setupWebRTC ( media , iceServers ) ;
2020-04-14 07:06:07 +00:00
} catch ( WebRTCWrapper . InitializationException e ) {
sendSessionTerminate ( Reason . FAILED_APPLICATION ) ;
return ;
}
final org . webrtc . SessionDescription sdp = new org . webrtc . SessionDescription (
org . webrtc . SessionDescription . Type . OFFER ,
offer . toString ( )
) ;
try {
this . webRTCWrapper . setRemoteDescription ( sdp ) . get ( ) ;
addIceCandidatesFromBlackLog ( ) ;
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-06 11:01:17 +00:00
}
2020-04-05 14:12:44 +00:00
2020-04-09 07:29:07 +00:00
private void addIceCandidatesFromBlackLog ( ) {
while ( ! this . pendingIceCandidates . isEmpty ( ) ) {
final IceCandidate iceCandidate = this . pendingIceCandidates . poll ( ) ;
this . webRTCWrapper . addIceCandidate ( iceCandidate ) ;
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : added ICE candidate from back log " + iceCandidate ) ;
}
}
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-14 07:53:01 +00:00
synchronized void deliveryMessage ( final Jid from , final Element message , final String serverMessageId , final long timestamp ) {
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-15 08:49:38 +00:00
receivePropose ( from , Propose . upgrade ( message ) , serverMessageId , timestamp ) ;
2020-04-02 19:12:38 +00:00
break ;
2020-04-03 08:46:42 +00:00
case " proceed " :
2020-04-12 16:07:31 +00:00
receiveProceed ( from , serverMessageId , timestamp ) ;
2020-04-08 07:42:06 +00:00
break ;
case " retract " :
2020-04-12 16:07:31 +00:00
receiveRetract ( from , serverMessageId , timestamp ) ;
2020-04-08 07:42:06 +00:00
break ;
2020-04-08 09:29:01 +00:00
case " reject " :
2020-04-12 16:07:31 +00:00
receiveReject ( from , serverMessageId , timestamp ) ;
2020-04-08 09:29:01 +00:00
break ;
2020-04-08 10:17:46 +00:00
case " accept " :
2020-04-12 16:07:31 +00:00
receiveAccept ( from , serverMessageId , timestamp ) ;
2020-04-08 10:17:46 +00:00
break ;
2020-04-02 19:12:38 +00:00
default :
break ;
}
}
2020-04-10 05:45:23 +00:00
void deliverFailedProceed ( ) {
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : receive message error for proceed message " ) ;
if ( transition ( State . TERMINATED_CONNECTIVITY_ERROR ) ) {
webRTCWrapper . close ( ) ;
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : transitioned into connectivity error " ) ;
this . jingleConnectionManager . finishConnection ( this ) ;
}
}
2020-04-12 16:07:31 +00:00
private void receiveAccept ( final Jid from , final String serverMsgId , final long timestamp ) {
2020-04-08 10:17:46 +00:00
final boolean originatedFromMyself = from . asBareJid ( ) . equals ( id . account . getJid ( ) . asBareJid ( ) ) ;
if ( originatedFromMyself ) {
if ( transition ( State . ACCEPTED ) ) {
2020-04-12 16:07:31 +00:00
if ( serverMsgId ! = null ) {
this . message . setServerMsgId ( serverMsgId ) ;
}
this . message . setTime ( timestamp ) ;
this . message . setCarbon ( true ) ; //indicate that call was accepted on other device
this . writeLogMessageSuccess ( 0 ) ;
2020-04-08 10:17:46 +00:00
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-12 16:07:31 +00:00
private void receiveReject ( Jid from , String serverMsgId , long timestamp ) {
2020-04-08 09:29:01 +00:00
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 ) ;
2020-04-12 16:07:31 +00:00
if ( serverMsgId ! = null ) {
this . message . setServerMsgId ( serverMsgId ) ;
}
this . message . setTime ( timestamp ) ;
this . message . setCarbon ( true ) ; //indicate that call was rejected on other device
writeLogMessageMissed ( ) ;
2020-04-08 09:29:01 +00:00
} 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-15 08:49:38 +00:00
private void receivePropose ( final Jid from , final Propose propose , final String serverMsgId , final long timestamp ) {
2020-04-03 08:46:42 +00:00
final boolean originatedFromMyself = from . asBareJid ( ) . equals ( id . account . getJid ( ) . asBareJid ( ) ) ;
if ( originatedFromMyself ) {
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : saw proposal from mysql. ignoring " ) ;
2020-04-15 20:40:37 +00:00
} else if ( transition ( State . PROPOSED , ( ) - > {
2020-04-15 08:49:38 +00:00
final Collection < RtpDescription > descriptions = Collections2 . transform (
Collections2 . filter ( propose . getDescriptions ( ) , d - > d instanceof RtpDescription ) ,
input - > ( RtpDescription ) input
) ;
final Collection < Media > media = Collections2 . transform ( descriptions , RtpDescription : : getMedia ) ;
2020-04-15 10:07:19 +00:00
Preconditions . checkState ( ! media . contains ( Media . UNKNOWN ) , " RTP descriptions contain unknown media " ) ;
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : received session proposal from " + from + " for " + media ) ;
2020-04-15 08:49:38 +00:00
this . proposedMedia = Sets . newHashSet ( media ) ;
2020-04-15 20:40:37 +00:00
} ) ) {
2020-04-12 16:07:31 +00:00
if ( serverMsgId ! = null ) {
this . message . setServerMsgId ( serverMsgId ) ;
}
this . message . setTime ( timestamp ) ;
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-15 16:28:04 +00:00
xmppConnectionService . getNotificationService ( ) . showIncomingCallNotification ( id , getMedia ( ) ) ;
2020-04-07 09:36:28 +00:00
}
2020-04-12 16:07:31 +00:00
private void receiveProceed ( final Jid from , final String serverMsgId , final long timestamp ) {
2020-04-15 10:07:19 +00:00
final Set < Media > media = Preconditions . checkNotNull ( this . proposedMedia , " Proposed media has to be set before handling proceed " ) ;
Preconditions . checkState ( media . size ( ) > 0 , " Proposed media should not be empty " ) ;
2020-04-03 08:46:42 +00:00
if ( from . equals ( id . with ) ) {
if ( isInitiator ( ) ) {
2020-04-04 09:31:53 +00:00
if ( transition ( State . PROCEED ) ) {
2020-04-12 16:07:31 +00:00
if ( serverMsgId ! = null ) {
this . message . setServerMsgId ( serverMsgId ) ;
}
this . message . setTime ( timestamp ) ;
2020-04-15 10:07:19 +00:00
this . sendSessionInitiate ( media , 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 {
Log . d ( Config . LOGTAG , String . format ( " %s: ignoring proceed from %s. was expected from %s " , id . account . getJid ( ) . asBareJid ( ) , from , id . with ) ) ;
}
}
2020-04-12 16:07:31 +00:00
private void receiveRetract ( final Jid from , final String serverMsgId , final long timestamp ) {
2020-04-08 07:42:06 +00:00
if ( from . equals ( id . with ) ) {
if ( transition ( State . RETRACTED ) ) {
xmppConnectionService . getNotificationService ( ) . cancelIncomingCallNotification ( ) ;
2020-04-13 16:30:12 +00:00
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : session with " + id . with + " has been retracted (serverMsgId= " + serverMsgId + " ) " ) ;
2020-04-12 16:07:31 +00:00
if ( serverMsgId ! = null ) {
this . message . setServerMsgId ( serverMsgId ) ;
}
this . message . setTime ( timestamp ) ;
2020-04-12 15:12:59 +00:00
writeLogMessageMissed ( ) ;
2020-04-08 07:42:06 +00:00
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-15 10:07:19 +00:00
private void sendSessionInitiate ( final Set < Media > media , final State targetState ) {
2020-04-05 08:20:34 +00:00
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : prepare session-initiate " ) ;
2020-04-15 10:07:19 +00:00
discoverIceServers ( iceServers - > sendSessionInitiate ( media , targetState , iceServers ) ) ;
2020-04-14 07:06:07 +00:00
}
2020-04-15 10:07:19 +00:00
private synchronized void sendSessionInitiate ( final Set < Media > media , final State targetState , final List < PeerConnection . IceServer > iceServers ) {
2020-04-14 07:53:01 +00:00
if ( TERMINATED . contains ( this . state ) ) {
2020-04-15 10:07:19 +00:00
Log . w ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : ICE servers got discovered when session was already terminated. nothing to do. " ) ;
2020-04-14 07:53:01 +00:00
return ;
}
2020-04-14 07:06:07 +00:00
try {
2020-04-15 10:07:19 +00:00
setupWebRTC ( media , iceServers ) ;
2020-04-14 07:06:07 +00:00
} catch ( WebRTCWrapper . InitializationException e ) {
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : unable to initialize webrtc " ) ;
transitionOrThrow ( State . TERMINATED_APPLICATION_FAILURE ) ;
return ;
}
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 ( final Exception e ) {
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : unable to sendSessionInitiate " , e ) ;
webRTCWrapper . close ( ) ;
if ( isInState ( targetState ) ) {
sendSessionTerminate ( Reason . FAILED_APPLICATION ) ;
} else {
2020-04-09 13:22:03 +00:00
transitionOrThrow ( State . TERMINATED_APPLICATION_FAILURE ) ;
2020-04-08 15:52:47 +00:00
}
2020-04-14 07:06:07 +00:00
}
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 ) ;
send ( sessionInitiate ) ;
2020-04-03 08:46:42 +00:00
}
2020-04-08 13:27:17 +00:00
private void sendSessionTerminate ( final Reason reason ) {
2020-04-09 18:35:44 +00:00
sendSessionTerminate ( reason , null ) ;
}
private void sendSessionTerminate ( final Reason reason , final String text ) {
2020-04-08 13:27:17 +00:00
final State target = reasonToState ( reason ) ;
transitionOrThrow ( target ) ;
2020-04-12 15:12:59 +00:00
writeLogMessage ( target ) ;
2020-04-08 13:27:17 +00:00
final JinglePacket jinglePacket = new JinglePacket ( JinglePacket . Action . SESSION_TERMINATE , id . sessionId ) ;
2020-04-09 18:35:44 +00:00
jinglePacket . setReason ( reason , text ) ;
2020-04-15 16:28:04 +00:00
Log . d ( Config . LOGTAG , jinglePacket . toString ( ) ) ;
2020-04-08 13:27:17 +00:00
send ( jinglePacket ) ;
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 ) ;
send ( jinglePacket ) ;
}
private void send ( final JinglePacket jinglePacket ) {
jinglePacket . setTo ( id . with ) ;
2020-04-09 13:22:03 +00:00
xmppConnectionService . sendIqPacket ( id . account , jinglePacket , ( account , response ) - > {
if ( response . getType ( ) = = IqPacket . TYPE . ERROR ) {
final String errorCondition = response . getErrorCondition ( ) ;
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : received IQ-error from " + response . getFrom ( ) + " in RTP session. " + errorCondition ) ;
this . webRTCWrapper . close ( ) ;
final State target ;
if ( Arrays . asList (
" service-unavailable " ,
" recipient-unavailable " ,
" remote-server-not-found " ,
" remote-server-timeout "
) . contains ( errorCondition ) ) {
target = State . TERMINATED_CONNECTIVITY_ERROR ;
} else {
target = State . TERMINATED_APPLICATION_FAILURE ;
}
if ( transition ( target ) ) {
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : terminated session with " + id . with ) ;
} else {
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : not transitioning because already at state= " + this . state ) ;
}
} else if ( response . getType ( ) = = IqPacket . TYPE . TIMEOUT ) {
this . webRTCWrapper . close ( ) ;
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : received IQ timeout in RTP session with " + id . with + " . terminating with connectivity error " ) ;
transition ( State . TERMINATED_CONNECTIVITY_ERROR ) ;
this . jingleConnectionManager . finishConnection ( this ) ;
}
} ) ;
}
2020-04-10 05:07:22 +00:00
private void terminateWithOutOfOrder ( final JinglePacket jinglePacket ) {
2020-04-10 05:45:23 +00:00
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : terminating session with out-of-order " ) ;
2020-04-10 05:07:22 +00:00
webRTCWrapper . close ( ) ;
transitionOrThrow ( State . TERMINATED_APPLICATION_FAILURE ) ;
respondWithOutOfOrder ( jinglePacket ) ;
jingleConnectionManager . finishConnection ( this ) ;
}
2020-04-09 13:22:03 +00:00
private void respondWithOutOfOrder ( final JinglePacket jinglePacket ) {
jingleConnectionManager . respondWithJingleError ( id . account , jinglePacket , " out-of-order " , " unexpected-request " , " wait " ) ;
}
private void respondOk ( final JinglePacket jinglePacket ) {
xmppConnectionService . sendIqPacket ( id . account , jinglePacket . generateResponse ( IqPacket . TYPE . RESULT ) , null ) ;
2020-04-05 11:58:05 +00:00
}
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 ( ) ) {
2020-04-09 07:04:59 +00:00
return RtpEndUserState . RINGING ;
2020-04-07 11:15:24 +00:00
} else {
return RtpEndUserState . ACCEPTING_CALL ;
}
2020-04-08 13:27:17 +00:00
case SESSION_INITIALIZED_PRE_APPROVED :
2020-04-09 07:04:59 +00:00
if ( isInitiator ( ) ) {
return RtpEndUserState . RINGING ;
} else {
return RtpEndUserState . CONNECTING ;
}
2020-04-07 11:15:24 +00:00
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-07 11:15:24 +00:00
} else {
2020-04-15 20:40:37 +00:00
return RtpEndUserState . CONNECTIVITY_ERROR ;
2020-04-07 19:26:51 +00:00
}
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-09 13:22:03 +00:00
case TERMINATED_APPLICATION_FAILURE :
return RtpEndUserState . APPLICATION_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-15 16:28:04 +00:00
public Set < Media > getMedia ( ) {
if ( isInState ( State . NULL ) ) {
throw new IllegalStateException ( " RTP connection has not been initialized yet " ) ;
}
if ( isInState ( State . PROPOSED , State . PROCEED ) ) {
2020-04-16 17:49:34 +00:00
return Preconditions . checkNotNull ( this . proposedMedia , " RTP connection has not been initialized properly " ) ;
}
final RtpContentMap initiatorContentMap = initiatorRtpContentMap ;
if ( initiatorContentMap ! = null ) {
return initiatorContentMap . getMedia ( ) ;
} else {
return Preconditions . checkNotNull ( this . proposedMedia , " RTP connection has not been initialized properly " ) ;
2020-04-15 16:28:04 +00:00
}
}
2020-04-05 11:58:05 +00:00
2020-04-14 07:53:01 +00:00
public synchronized 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-14 07:53:01 +00:00
public synchronized void rejectCall ( ) {
2020-04-07 19:26:51 +00:00
switch ( this . state ) {
case PROPOSED :
rejectCallFromProposed ( ) ;
break ;
2020-04-09 07:04:59 +00:00
case SESSION_INITIALIZED :
rejectCallFromSessionInitiate ( ) ;
break ;
2020-04-07 19:26:51 +00:00
default :
throw new IllegalStateException ( " Can not reject call from " + this . state ) ;
}
2020-04-07 11:15:24 +00:00
}
2020-04-14 07:53:01 +00:00
public synchronized void endCall ( ) {
2020-04-14 09:56:02 +00:00
if ( TERMINATED . contains ( this . state ) ) {
2020-04-15 10:07:19 +00:00
Log . w ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : received endCall() when session has already been terminated. nothing to do " ) ;
2020-04-14 09:56:02 +00:00
return ;
}
2020-04-10 08:35:00 +00:00
if ( isInState ( State . PROPOSED ) & & ! isInitiator ( ) ) {
rejectCallFromProposed ( ) ;
return ;
}
2020-04-09 07:04:59 +00:00
if ( isInState ( State . PROCEED ) ) {
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : ending call while in state PROCEED just means ending the connection " ) ;
webRTCWrapper . close ( ) ;
jingleConnectionManager . finishConnection ( this ) ;
transitionOrThrow ( State . TERMINATED_SUCCESS ) ; //arguably this wasn't success; but not a real failure either
return ;
}
if ( isInitiator ( ) & & isInState ( State . SESSION_INITIALIZED , State . SESSION_INITIALIZED_PRE_APPROVED ) ) {
2020-04-08 13:27:17 +00:00
webRTCWrapper . close ( ) ;
sendSessionTerminate ( Reason . CANCEL ) ;
2020-04-09 07:04:59 +00:00
return ;
}
2020-04-10 08:35:00 +00:00
if ( isInState ( State . SESSION_INITIALIZED ) ) {
rejectCallFromSessionInitiate ( ) ;
return ;
}
if ( isInState ( 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-09 07:04:59 +00:00
return ;
2020-04-07 12:22:12 +00:00
}
2020-04-10 11:13:20 +00:00
if ( isInState ( State . TERMINATED_APPLICATION_FAILURE , State . TERMINATED_CONNECTIVITY_ERROR , State . TERMINATED_DECLINED_OR_BUSY ) ) {
Log . d ( Config . LOGTAG , " ignoring request to end call because already in state " + this . state ) ;
return ;
}
2020-04-10 08:35:00 +00:00
throw new IllegalStateException ( " called 'endCall' while in state " + this . state + " . isInitiator= " + isInitiator ( ) ) ;
2020-04-07 12:22:12 +00:00
}
2020-04-15 10:07:19 +00:00
private void setupWebRTC ( final Set < Media > media , final List < PeerConnection . IceServer > iceServers ) throws WebRTCWrapper . InitializationException {
2020-04-15 16:47:15 +00:00
final AppRTCAudioManager . SpeakerPhonePreference speakerPhonePreference ;
if ( media . contains ( Media . VIDEO ) ) {
speakerPhonePreference = AppRTCAudioManager . SpeakerPhonePreference . SPEAKER ;
} else {
speakerPhonePreference = AppRTCAudioManager . SpeakerPhonePreference . EARPIECE ;
}
this . webRTCWrapper . setup ( this . xmppConnectionService , speakerPhonePreference ) ;
2020-04-15 10:07:19 +00:00
this . webRTCWrapper . initializePeerConnection ( media , 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 ) ;
2020-04-12 16:07:31 +00:00
writeLogMessageMissed ( ) ;
2020-04-07 19:26:51 +00:00
xmppConnectionService . getNotificationService ( ) . cancelIncomingCallNotification ( ) ;
this . sendJingleMessage ( " reject " ) ;
jingleConnectionManager . finishConnection ( this ) ;
}
2020-04-09 07:04:59 +00:00
private void rejectCallFromSessionInitiate ( ) {
webRTCWrapper . close ( ) ;
sendSessionTerminate ( Reason . DECLINE ) ;
xmppConnectionService . getNotificationService ( ) . cancelIncomingCallNotification ( ) ;
}
2020-04-07 19:26:51 +00:00
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-10 05:45:23 +00:00
if ( " proceed " . equals ( action ) ) {
messagePacket . setId ( JINGLE_MESSAGE_PROCEED_ID_PREFIX + id . sessionId ) ;
}
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-13 07:00:25 +00:00
messagePacket . addChild ( " store " , " urn:xmpp:hints " ) ;
2020-04-02 19:12:38 +00:00
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 ( ) ;
2020-04-09 07:04:59 +00:00
sendSessionAccept ( ) ;
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-15 20:40:37 +00:00
private boolean transition ( final State target ) {
return transition ( target , null ) ;
}
private synchronized boolean transition ( final State target , final Runnable runnable ) {
2020-04-02 19:12:38 +00:00
final Collection < State > validTransitions = VALID_TRANSITIONS . get ( this . state ) ;
if ( validTransitions ! = null & & validTransitions . contains ( target ) ) {
this . state = target ;
2020-04-15 20:40:37 +00:00
if ( runnable ! = null ) {
runnable . run ( ) ;
}
2020-04-02 19:12:38 +00:00
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : transitioned into " + target ) ;
2020-04-07 11:15:24 +00:00
updateEndUserState ( ) ;
2020-04-10 13:19:56 +00:00
updateOngoingCallNotification ( ) ;
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-12 15:12:59 +00:00
if ( newState = = PeerConnection . PeerConnectionState . CONNECTED & & this . rtpConnectionStarted = = 0 ) {
this . rtpConnectionStarted = SystemClock . elapsedRealtime ( ) ;
}
2020-04-16 06:20:13 +00:00
//TODO 'DISCONNECTED' might be an opportunity to renew the offer and send a transport-replace
//TODO exact syntax is yet to be determined but transport-replace sounds like the most reasonable
//as there is no content-replace
2020-04-15 20:40:37 +00:00
if ( Arrays . asList ( PeerConnection . PeerConnectionState . FAILED , PeerConnection . PeerConnectionState . DISCONNECTED ) . contains ( newState ) ) {
2020-04-12 07:59:32 +00:00
if ( TERMINATED . contains ( this . state ) ) {
2020-04-12 15:12:59 +00:00
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : not sending session-terminate after connectivity error because session is already in state " + this . state ) ;
2020-04-12 07:59:32 +00:00
return ;
}
2020-04-08 13:27:17 +00:00
sendSessionTerminate ( Reason . CONNECTIVITY_ERROR ) ;
2020-04-15 22:08:58 +00:00
} else {
updateEndUserState ( ) ;
2020-04-08 13:27:17 +00:00
}
2020-04-07 11:15:24 +00:00
}
2020-04-13 10:53:23 +00:00
public AppRTCAudioManager getAudioManager ( ) {
return webRTCWrapper . getAudioManager ( ) ;
}
public boolean isMicrophoneEnabled ( ) {
return webRTCWrapper . isMicrophoneEnabled ( ) ;
}
2020-04-13 16:30:12 +00:00
public void setMicrophoneEnabled ( final boolean enabled ) {
webRTCWrapper . setMicrophoneEnabled ( enabled ) ;
}
2020-04-15 17:16:47 +00:00
public boolean isVideoEnabled ( ) {
return webRTCWrapper . isVideoEnabled ( ) ;
}
public void setVideoEnabled ( final boolean enabled ) {
webRTCWrapper . setVideoEnabled ( enabled ) ;
}
2020-04-13 10:02:34 +00:00
@Override
public void onAudioDeviceChanged ( AppRTCAudioManager . AudioDevice selectedAudioDevice , Set < AppRTCAudioManager . AudioDevice > availableAudioDevices ) {
xmppConnectionService . notifyJingleRtpConnectionUpdate ( selectedAudioDevice , availableAudioDevices ) ;
}
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
2020-04-10 13:19:56 +00:00
private void updateOngoingCallNotification ( ) {
if ( STATES_SHOWING_ONGOING_CALL . contains ( this . state ) ) {
2020-04-15 20:40:37 +00:00
xmppConnectionService . setOngoingCall ( id , getMedia ( ) ) ;
2020-04-10 13:19:56 +00:00
} else {
2020-04-15 20:40:37 +00:00
xmppConnectionService . removeOngoingCall ( ) ;
2020-04-10 13:19:56 +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 " ) ;
2020-04-09 11:27:13 +00:00
final String sport = child . getAttribute ( " port " ) ;
final Integer port = sport = = null ? null : Ints . tryParse ( sport ) ;
2020-04-08 15:52:47 +00:00
final String transport = child . getAttribute ( " transport " ) ;
final String username = child . getAttribute ( " username " ) ;
final String password = child . getAttribute ( " password " ) ;
2020-04-09 11:27:13 +00:00
if ( Strings . isNullOrEmpty ( host ) | | port = = null ) {
continue ;
}
if ( port < 0 | | port > 65535 ) {
continue ;
}
if ( Arrays . asList ( " stun " , " turn " ) . contains ( type ) | | Arrays . asList ( " udp " , " tcp " ) . contains ( transport ) ) {
//TODO wrap ipv6 addresses
PeerConnection . IceServer . Builder iceServerBuilder = PeerConnection . IceServer . builder ( String . format ( " %s:%s:%s?transport=%s " , type , host , port , transport ) ) ;
2020-04-08 15:52:47 +00:00
if ( username ! = null & & password ! = null ) {
iceServerBuilder . setUsername ( username ) ;
iceServerBuilder . setPassword ( password ) ;
2020-04-09 11:27:13 +00:00
} else if ( Arrays . asList ( " turn " , " turns " ) . contains ( type ) ) {
//The WebRTC spec requires throwing an InvalidAccessError when username (from libwebrtc source coder)
//https://chromium.googlesource.com/external/webrtc/+/master/pc/ice_server_parsing.cc
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : skipping " + type + " / " + transport + " without username and password " ) ;
continue ;
2020-04-08 15:52:47 +00:00
}
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 ( ) ) ;
}
}
2020-04-12 15:12:59 +00:00
private void writeLogMessage ( final State state ) {
final long started = this . rtpConnectionStarted ;
long duration = started < = 0 ? 0 : SystemClock . elapsedRealtime ( ) - started ;
if ( state = = State . TERMINATED_SUCCESS | | ( state = = State . TERMINATED_CONNECTIVITY_ERROR & & duration > 0 ) ) {
writeLogMessageSuccess ( duration ) ;
} else {
writeLogMessageMissed ( ) ;
}
}
private void writeLogMessageSuccess ( final long duration ) {
this . message . setBody ( new RtpSessionStatus ( true , duration ) . toString ( ) ) ;
this . writeMessage ( ) ;
}
private void writeLogMessageMissed ( ) {
2020-04-13 16:30:12 +00:00
this . message . setBody ( new RtpSessionStatus ( false , 0 ) . toString ( ) ) ;
2020-04-12 15:12:59 +00:00
this . writeMessage ( ) ;
}
private void writeMessage ( ) {
final Conversational conversational = message . getConversation ( ) ;
if ( conversational instanceof Conversation ) {
( ( Conversation ) conversational ) . add ( this . message ) ;
xmppConnectionService . databaseBackend . createMessage ( message ) ;
xmppConnectionService . updateConversationUi ( ) ;
} else {
throw new IllegalStateException ( " Somehow the conversation in a message was a stub " ) ;
}
}
2020-04-10 13:19:56 +00:00
public State getState ( ) {
return this . state ;
}
2020-04-14 17:06:39 +00:00
public Optional < VideoTrack > geLocalVideoTrack ( ) {
return webRTCWrapper . getLocalVideoTrack ( ) ;
}
public Optional < VideoTrack > getRemoteVideoTrack ( ) {
return webRTCWrapper . getRemoteVideoTrack ( ) ;
}
public EglBase . Context getEglBaseContext ( ) {
return webRTCWrapper . getEglBaseContext ( ) ;
}
2020-04-15 08:49:38 +00:00
public void setProposedMedia ( final Set < Media > media ) {
2020-04-15 10:07:19 +00:00
this . proposedMedia = media ;
2020-04-15 08:49:38 +00:00
}
2020-04-08 15:52:47 +00:00
private interface OnIceServersDiscovered {
void onIceServersDiscovered ( List < PeerConnection . IceServer > iceServers ) ;
}
2020-04-02 09:30:16 +00:00
}