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 ;
2021-05-06 16:40:35 +00:00
import androidx.annotation.NonNull ;
import androidx.annotation.Nullable ;
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-29 06:51:38 +00:00
import com.google.common.base.Throwables ;
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 ;
2021-05-06 16:40:35 +00:00
import com.google.common.util.concurrent.FutureCallback ;
import com.google.common.util.concurrent.Futures ;
2020-05-02 15:15:46 +00:00
import com.google.common.util.concurrent.ListenableFuture ;
2021-05-06 16:40:35 +00:00
import com.google.common.util.concurrent.MoreExecutors ;
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-22 19:59:20 +00:00
import java.util.concurrent.ScheduledFuture ;
import java.util.concurrent.TimeUnit ;
2020-04-02 19:12:38 +00:00
2020-04-02 09:30:16 +00:00
import eu.siacs.conversations.Config ;
2021-03-02 20:13:49 +00:00
import eu.siacs.conversations.crypto.axolotl.AxolotlService ;
import eu.siacs.conversations.crypto.axolotl.CryptoFailedException ;
2021-03-03 08:41:05 +00:00
import eu.siacs.conversations.crypto.axolotl.FingerprintStatus ;
2020-04-17 08:56:27 +00:00
import eu.siacs.conversations.entities.Account ;
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-05-08 15:52:41 +00:00
import eu.siacs.conversations.utils.IP ;
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-06-22 16:07:27 +00:00
import eu.siacs.conversations.xmpp.Jid ;
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 ;
2021-03-02 20:13:49 +00:00
import eu.siacs.conversations.xmpp.jingle.stanzas.Proceed ;
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 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_PRE_APPROVED ,
State . SESSION_ACCEPTED
) ;
2020-04-29 08:36:54 +00:00
private static final long BUSY_TIME_OUT = 30 ;
2020-04-12 07:59:32 +00:00
private static final List < State > TERMINATED = Arrays . asList (
2020-05-08 16:36:52 +00:00
State . ACCEPTED ,
State . REJECTED ,
2021-02-11 15:56:57 +00:00
State . REJECTED_RACED ,
2020-05-08 16:36:52 +00:00
State . RETRACTED ,
2020-05-09 19:35:21 +00:00
State . RETRACTED_RACED ,
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 ,
2021-05-04 08:10:30 +00:00
State . TERMINATED_APPLICATION_FAILURE ,
State . TERMINATED_SECURITY_ERROR
2020-04-12 07:59:32 +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 < > ( ) ;
2020-04-09 13:22:03 +00:00
transitionBuilder . put ( State . NULL , ImmutableList . of (
State . PROPOSED ,
State . SESSION_INITIALIZED ,
2021-05-04 08:10:30 +00:00
State . TERMINATED_APPLICATION_FAILURE ,
State . TERMINATED_SECURITY_ERROR
2020-04-09 13:22:03 +00:00
) ) ;
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 ,
2021-05-04 08:10:30 +00:00
State . TERMINATED_SECURITY_ERROR ,
2020-04-13 16:30:12 +00:00
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 (
2021-02-11 15:56:57 +00:00
State . REJECTED_RACED ,
2020-05-09 19:35:21 +00:00
State . RETRACTED_RACED ,
2020-04-09 13:22:03 +00:00
State . SESSION_INITIALIZED_PRE_APPROVED ,
State . TERMINATED_SUCCESS ,
2020-04-10 05:07:22 +00:00
State . TERMINATED_APPLICATION_FAILURE ,
2021-05-04 08:10:30 +00:00
State . TERMINATED_SECURITY_ERROR ,
2020-04-10 05:07:22 +00:00
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 ,
2021-05-04 08:10:30 +00:00
State . TERMINATED_APPLICATION_FAILURE ,
State . TERMINATED_SECURITY_ERROR
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 ,
2021-05-04 08:10:30 +00:00
State . TERMINATED_APPLICATION_FAILURE ,
State . TERMINATED_SECURITY_ERROR
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 ,
2021-05-04 08:10:30 +00:00
State . TERMINATED_APPLICATION_FAILURE ,
State . TERMINATED_SECURITY_ERROR
2020-04-09 13:22:03 +00:00
) ) ;
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-06-22 16:07:27 +00:00
private final ArrayDeque < Set < Map . Entry < String , RtpContentMap . DescriptionTransport > > > pendingIceCandidates = new ArrayDeque < > ( ) ;
2021-03-02 20:13:49 +00:00
private final OmemoVerification omemoVerification = new OmemoVerification ( ) ;
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-05-28 07:22:58 +00:00
private StateTransitionException stateTransitionException ;
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-05-09 09:14:39 +00:00
private long rtpConnectionEnded = 0 ;
2020-04-22 19:59:20 +00:00
private ScheduledFuture < ? > ringingTimeoutFuture ;
2020-04-06 08:26:29 +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-15 10:07:19 +00:00
case SECURITY_ERROR :
2021-05-04 08:10:30 +00:00
return State . TERMINATED_SECURITY_ERROR ;
case FAILED_APPLICATION :
2020-04-15 10:07:19 +00:00
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-24 07:41:54 +00:00
if ( isTerminated ( ) ) {
2020-04-13 16:30:12 +00:00
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 ) ;
2020-04-18 16:22:10 +00:00
finish ( ) ;
2020-04-13 16:30:12 +00:00
}
}
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 ( ) ;
}
2020-04-18 16:22:10 +00:00
finish ( ) ;
2020-04-08 13:27:17 +00:00
}
2020-04-06 08:26:29 +00:00
private void receiveTransportInfo ( final JinglePacket jinglePacket ) {
2021-05-06 16:40:35 +00:00
//Due to the asynchronicity of processing session-init we might move from NULL|PROCEED to INITIALIZED only after transport-info has been received
if ( isInState ( State . NULL , State . PROCEED , 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-06-22 16:07:27 +00:00
final Set < Map . Entry < String , RtpContentMap . DescriptionTransport > > candidates = contentMap . contents . entrySet ( ) ;
if ( this . state = = State . SESSION_ACCEPTED ) {
2020-08-01 12:18:00 +00:00
try {
processCandidates ( candidates ) ;
} catch ( final WebRTCWrapper . PeerConnectionNotInitialized e ) {
Log . w ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : PeerConnection was not initialized when processing transport info. this usually indicates a race condition that can be ignored " ) ;
}
2020-06-22 16:07:27 +00:00
} else {
pendingIceCandidates . push ( candidates ) ;
2020-04-06 08:26:29 +00:00
}
2020-04-29 08:36:54 +00:00
} else {
if ( isTerminated ( ) ) {
respondOk ( jinglePacket ) ;
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : ignoring out-of-order transport info; we where already terminated " ) ;
} else {
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : received transport info while in state= " + this . state ) ;
terminateWithOutOfOrder ( jinglePacket ) ;
}
}
}
2020-06-22 16:07:27 +00:00
private void processCandidates ( final Set < Map . Entry < String , RtpContentMap . DescriptionTransport > > contents ) {
final RtpContentMap rtpContentMap = isInitiator ( ) ? this . responderRtpContentMap : this . initiatorRtpContentMap ;
final Group originalGroup = rtpContentMap . group ;
final List < String > identificationTags = originalGroup = = null ? rtpContentMap . getNames ( ) : originalGroup . getIdentificationTags ( ) ;
if ( identificationTags . size ( ) = = 0 ) {
Log . w ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : no identification tags found in initial offer. we won't be able to calculate mLineIndices " ) ;
}
processCandidates ( identificationTags , contents ) ;
}
private void processCandidates ( final List < String > indices , final Set < Map . Entry < String , RtpContentMap . DescriptionTransport > > contents ) {
2020-04-29 08:36:54 +00:00
for ( final Map . Entry < String , RtpContentMap . DescriptionTransport > content : contents ) {
final String ufrag = content . getValue ( ) . transport . getAttribute ( " ufrag " ) ;
for ( final IceUdpTransportInfo . Candidate candidate : content . getValue ( ) . transport . getCandidates ( ) ) {
final String sdp ;
try {
sdp = candidate . toSdpAttribute ( ufrag ) ;
} catch ( IllegalArgumentException e ) {
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : ignoring invalid ICE candidate " + e . getMessage ( ) ) ;
continue ;
}
final String sdpMid = content . getKey ( ) ;
2020-06-22 16:07:27 +00:00
final int mLineIndex = indices . indexOf ( sdpMid ) ;
2021-03-16 17:52:25 +00:00
if ( mLineIndex < 0 ) {
Log . w ( Config . LOGTAG , " mLineIndex not found for " + sdpMid + " . available indices " + indices ) ;
}
2020-04-29 08:36:54 +00:00
final IceCandidate iceCandidate = new IceCandidate ( sdpMid , mLineIndex , sdp ) ;
2020-06-22 16:07:27 +00:00
Log . d ( Config . LOGTAG , " received candidate: " + iceCandidate ) ;
this . webRTCWrapper . addIceCandidate ( iceCandidate ) ;
2020-04-06 08:26:29 +00:00
}
}
}
2021-05-06 16:40:35 +00:00
private ListenableFuture < RtpContentMap > receiveRtpContentMap ( final JinglePacket jinglePacket , final boolean expectVerification ) {
2021-03-02 20:13:49 +00:00
final RtpContentMap receivedContentMap = RtpContentMap . of ( jinglePacket ) ;
if ( receivedContentMap instanceof OmemoVerifiedRtpContentMap ) {
2021-05-06 16:40:35 +00:00
final ListenableFuture < AxolotlService . OmemoVerifiedPayload < RtpContentMap > > future = id . account . getAxolotlService ( ) . decrypt ( ( OmemoVerifiedRtpContentMap ) receivedContentMap , id . with ) ;
return Futures . transform ( future , omemoVerifiedPayload - > {
omemoVerification . setOrEnsureEqual ( omemoVerifiedPayload ) ;
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : received verifiable DTLS fingerprint via " + omemoVerification ) ;
return omemoVerifiedPayload . getPayload ( ) ;
} , MoreExecutors . directExecutor ( ) ) ;
2021-03-02 20:13:49 +00:00
} else if ( expectVerification ) {
throw new SecurityException ( " DTLS fingerprint was unexpectedly not verifiable " ) ;
} else {
2021-05-06 16:40:35 +00:00
return Futures . immediateFuture ( receivedContentMap ) ;
2021-03-02 20:13:49 +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 ( ) ) ) ;
2021-04-08 08:23:37 +00:00
if ( isTerminated ( ) ) {
Log . d ( Config . LOGTAG , String . format (
" %s: got a reason to terminate with out-of-order. but already in state %s " ,
id . account . getJid ( ) . asBareJid ( ) ,
getState ( )
) ) ;
respondWithOutOfOrder ( jinglePacket ) ;
} else {
terminateWithOutOfOrder ( jinglePacket ) ;
}
2020-04-03 08:46:42 +00:00
return ;
}
2021-05-06 16:40:35 +00:00
final ListenableFuture < RtpContentMap > future = receiveRtpContentMap ( jinglePacket , false ) ;
Futures . addCallback ( future , new FutureCallback < RtpContentMap > ( ) {
@Override
public void onSuccess ( @Nullable RtpContentMap rtpContentMap ) {
receiveSessionInitiate ( jinglePacket , rtpContentMap ) ;
}
@Override
public void onFailure ( @NonNull final Throwable throwable ) {
respondOk ( jinglePacket ) ;
sendSessionTerminate ( Reason . ofThrowable ( throwable ) , throwable . getMessage ( ) ) ;
}
} , MoreExecutors . directExecutor ( ) ) ;
}
private void receiveSessionInitiate ( final JinglePacket jinglePacket , final RtpContentMap contentMap ) {
2020-04-03 08:46:42 +00:00
try {
2020-04-06 08:26:29 +00:00
contentMap . requireContentDescriptions ( ) ;
2020-04-10 05:45:23 +00:00
contentMap . requireDTLSFingerprint ( ) ;
2020-04-22 14:35:08 +00:00
} catch ( final RuntimeException e ) {
2020-04-29 13:54:02 +00:00
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : improperly formatted contents " , Throwables . getRootCause ( 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-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 ) ;
2021-03-03 12:12:10 +00:00
final Set < Map . Entry < String , RtpContentMap . DescriptionTransport > > candidates = contentMap . contents . entrySet ( ) ;
if ( candidates . size ( ) > 0 ) {
pendingIceCandidates . push ( candidates ) ;
}
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 ;
}
2021-05-06 16:40:35 +00:00
final ListenableFuture < RtpContentMap > future = receiveRtpContentMap ( jinglePacket , this . omemoVerification . hasFingerprint ( ) ) ;
Futures . addCallback ( future , new FutureCallback < RtpContentMap > ( ) {
@Override
public void onSuccess ( @Nullable RtpContentMap rtpContentMap ) {
receiveSessionAccept ( jinglePacket , rtpContentMap ) ;
}
@Override
public void onFailure ( @NonNull final Throwable throwable ) {
respondOk ( jinglePacket ) ;
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : improperly formatted contents in session-accept " , throwable ) ;
webRTCWrapper . close ( ) ;
sendSessionTerminate ( Reason . ofThrowable ( throwable ) , throwable . getMessage ( ) ) ;
}
} , MoreExecutors . directExecutor ( ) ) ;
}
private void receiveSessionAccept ( final JinglePacket jinglePacket , final RtpContentMap contentMap ) {
2020-04-06 13:45:06 +00:00
try {
contentMap . requireContentDescriptions ( ) ;
2020-04-10 05:45:23 +00:00
contentMap . requireDTLSFingerprint ( ) ;
2020-04-22 14:35:08 +00:00
} catch ( final RuntimeException 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-08-22 06:11:21 +00:00
final org . webrtc . SessionDescription answer = new org . webrtc . SessionDescription (
2020-04-06 13:45:06 +00:00
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 ( ) ;
2020-04-29 06:51:38 +00:00
} catch ( final Exception e ) {
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : unable to set remote description after receiving session-accept " , Throwables . getRootCause ( e ) ) ;
2020-04-09 13:22:03 +00:00
webRTCWrapper . close ( ) ;
sendSessionTerminate ( Reason . FAILED_APPLICATION ) ;
2020-08-22 06:11:21 +00:00
return ;
2020-04-06 13:45:06 +00:00
}
2020-08-22 06:11:21 +00:00
final List < String > identificationTags = contentMap . group = = null ? contentMap . getNames ( ) : contentMap . group . getIdentificationTags ( ) ;
processCandidates ( identificationTags , contentMap . contents . entrySet ( ) ) ;
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-24 07:41:54 +00:00
if ( isTerminated ( ) ) {
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-05-01 11:56:24 +00:00
} catch ( final WebRTCWrapper . InitializationException e ) {
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : unable to initialize WebRTC " ) ;
webRTCWrapper . close ( ) ;
2020-04-14 07:06:07 +00:00
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 ) ;
2020-04-29 13:32:27 +00:00
this . webRTCWrapper . setLocalDescription ( webRTCSessionDescription ) . get ( ) ;
2020-04-29 06:51:38 +00:00
} catch ( final Exception e ) {
Log . d ( Config . LOGTAG , " unable to send session accept " , Throwables . getRootCause ( e ) ) ;
webRTCWrapper . close ( ) ;
sendSessionTerminate ( Reason . FAILED_APPLICATION ) ;
2020-04-14 07:06:07 +00:00
}
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 ( ) ) {
2020-06-22 16:07:27 +00:00
processCandidates ( this . pendingIceCandidates . poll ( ) ) ;
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : added candidates from back log " ) ;
2020-04-09 07:29:07 +00:00
}
}
2020-04-06 11:01:17 +00:00
private void sendSessionAccept ( final RtpContentMap rtpContentMap ) {
this . responderRtpContentMap = rtpContentMap ;
this . transitionOrThrow ( State . SESSION_ACCEPTED ) ;
2021-03-02 20:13:49 +00:00
final RtpContentMap outgoingContentMap ;
if ( this . omemoVerification . hasDeviceId ( ) ) {
2021-03-03 07:22:21 +00:00
final AxolotlService . OmemoVerifiedPayload < OmemoVerifiedRtpContentMap > verifiedPayload ;
2021-03-02 20:13:49 +00:00
try {
2021-03-03 07:22:21 +00:00
verifiedPayload = id . account . getAxolotlService ( ) . encrypt ( rtpContentMap , id . with , omemoVerification . getDeviceId ( ) ) ;
2021-03-02 20:13:49 +00:00
outgoingContentMap = verifiedPayload . getPayload ( ) ;
this . omemoVerification . setOrEnsureEqual ( verifiedPayload ) ;
} catch ( final Exception e ) {
2021-03-03 07:22:21 +00:00
throw new SecurityException ( " Unable to verify DTLS Fingerprint with OMEMO " , e ) ;
2021-03-02 20:13:49 +00:00
}
} else {
outgoingContentMap = rtpContentMap ;
}
final JinglePacket sessionAccept = outgoingContentMap . toJinglePacket ( JinglePacket . Action . SESSION_ACCEPT , id . sessionId ) ;
2020-04-06 11:01:17 +00:00
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 " :
2021-03-02 20:13:49 +00:00
receiveProceed ( from , Proceed . upgrade ( message ) , 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 " ) ;
2020-04-18 16:22:10 +00:00
this . finish ( ) ;
2020-04-10 05:45:23 +00:00
}
}
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 ( ) ;
2020-04-18 16:22:10 +00:00
this . finish ( ) ;
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 ( ) + " : 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
}
}
2021-02-11 15:56:57 +00:00
private void receiveReject ( final Jid from , final String serverMsgId , final 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 ) {
2021-02-11 15:56:57 +00:00
receiveRejectFromMyself ( serverMsgId , timestamp ) ;
} else if ( isInitiator ( ) ) {
if ( from . equals ( id . with ) ) {
receiveRejectFromResponder ( ) ;
2020-04-08 09:29:01 +00:00
} else {
2021-02-11 15:56:57 +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
}
} 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
}
}
2021-02-11 15:56:57 +00:00
private void receiveRejectFromMyself ( String serverMsgId , long timestamp ) {
if ( transition ( State . REJECTED ) ) {
this . xmppConnectionService . getNotificationService ( ) . cancelIncomingCallNotification ( ) ;
this . finish ( ) ;
if ( serverMsgId ! = null ) {
this . message . setServerMsgId ( serverMsgId ) ;
}
this . message . setTime ( timestamp ) ;
this . message . setCarbon ( true ) ; //indicate that call was rejected on other device
writeLogMessageMissed ( ) ;
} else {
Log . d ( Config . LOGTAG , " not able to transition into REJECTED because already in " + this . state ) ;
}
}
private void receiveRejectFromResponder ( ) {
if ( isInState ( State . PROCEED ) ) {
Log . d ( Config . LOGTAG , id . account . getJid ( ) + " : received reject while still in proceed. callee reconsidered " ) ;
closeTransitionLogFinish ( State . REJECTED_RACED ) ;
return ;
}
if ( isInState ( State . SESSION_INITIALIZED_PRE_APPROVED ) ) {
Log . d ( Config . LOGTAG , id . account . getJid ( ) + " : received reject while in SESSION_INITIATED_PRE_APPROVED. callee reconsidered before receiving session-init " ) ;
closeTransitionLogFinish ( State . TERMINATED_DECLINED_OR_BUSY ) ;
return ;
}
Log . d ( Config . LOGTAG , id . account . getJid ( ) + " : ignoring reject from responder because already in state " + this . state ) ;
}
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 ) {
2021-02-11 15:56:57 +00:00
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : saw proposal from myself. 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-22 19:59:20 +00:00
ringingTimeoutFuture = jingleConnectionManager . schedule ( this : : ringingTimeout , BUSY_TIME_OUT , TimeUnit . SECONDS ) ;
2021-02-18 19:55:29 +00:00
xmppConnectionService . getNotificationService ( ) . startRinging ( id , getMedia ( ) ) ;
2020-04-07 09:36:28 +00:00
}
2020-04-22 19:59:20 +00:00
private synchronized void ringingTimeout ( ) {
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : timeout reached for ringing " ) ;
switch ( this . state ) {
case PROPOSED :
2020-05-03 16:07:00 +00:00
message . markUnread ( ) ;
2020-04-22 19:59:20 +00:00
rejectCallFromProposed ( ) ;
break ;
case SESSION_INITIALIZED :
2020-05-03 16:07:00 +00:00
message . markUnread ( ) ;
2020-04-22 19:59:20 +00:00
rejectCallFromSessionInitiate ( ) ;
break ;
}
}
private void cancelRingingTimeout ( ) {
final ScheduledFuture < ? > future = this . ringingTimeoutFuture ;
if ( future ! = null & & ! future . isCancelled ( ) ) {
future . cancel ( false ) ;
}
}
2021-03-02 20:13:49 +00:00
private void receiveProceed ( final Jid from , final Proceed proceed , 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 ) ;
2021-03-03 11:55:27 +00:00
final Integer remoteDeviceId = proceed . getDeviceId ( ) ;
if ( isOmemoEnabled ( ) ) {
this . omemoVerification . setDeviceId ( remoteDeviceId ) ;
} else {
if ( remoteDeviceId ! = null ) {
2021-03-16 17:52:25 +00:00
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : remote party signaled support for OMEMO verification but we have OMEMO disabled " ) ;
2021-03-03 11:55:27 +00:00
}
this . omemoVerification . setDeviceId ( null ) ;
}
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 ( ) ;
2020-04-18 16:22:10 +00:00
this . finish ( ) ;
2020-04-08 09:29:01 +00:00
}
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 ) ) {
2020-05-09 19:35:21 +00:00
final State target = this . state = = State . PROCEED ? State . RETRACTED_RACED : State . RETRACTED ;
if ( transition ( target ) ) {
2020-04-08 07:42:06 +00:00
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-05-09 19:35:21 +00:00
if ( target = = State . RETRACTED ) {
this . message . markUnread ( ) ;
}
2020-04-12 15:12:59 +00:00
writeLogMessageMissed ( ) ;
2020-04-18 16:22:10 +00:00
finish ( ) ;
2020-04-08 07:42:06 +00:00
} else {
Log . d ( Config . LOGTAG , " ignoring retract because already in " + this . state ) ;
}
} else {
2021-02-18 08:36:51 +00:00
//TODO parse retract from self
2020-04-08 07:42:06 +00:00
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : received retract from " + from + " . expected retract from " + id . with + " . ignoring " ) ;
}
}
2020-05-30 12:56:12 +00:00
public void sendSessionInitiate ( ) {
sendSessionInitiate ( this . proposedMedia , State . SESSION_INITIALIZED ) ;
}
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-24 07:41:54 +00:00
if ( isTerminated ( ) ) {
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-05-08 16:36:52 +00:00
} catch ( final WebRTCWrapper . InitializationException e ) {
2020-05-01 11:56:24 +00:00
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : unable to initialize WebRTC " ) ;
webRTCWrapper . close ( ) ;
2021-05-06 16:40:35 +00:00
sendRetract ( Reason . ofThrowable ( e ) ) ;
2020-04-14 07:06:07 +00:00
return ;
}
try {
org . webrtc . SessionDescription webRTCSessionDescription = this . webRTCWrapper . createOffer ( ) . get ( ) ;
final SessionDescription sessionDescription = SessionDescription . parse ( webRTCSessionDescription . description ) ;
final RtpContentMap rtpContentMap = RtpContentMap . of ( sessionDescription ) ;
sendSessionInitiate ( rtpContentMap , targetState ) ;
this . webRTCWrapper . setLocalDescription ( webRTCSessionDescription ) . get ( ) ;
} catch ( final Exception e ) {
2020-04-29 13:32:27 +00:00
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : unable to sendSessionInitiate " , Throwables . getRootCause ( e ) ) ;
2020-04-14 07:06:07 +00:00
webRTCWrapper . close ( ) ;
2021-05-06 16:40:35 +00:00
final Reason reason = Reason . ofThrowable ( e ) ;
2020-04-14 07:06:07 +00:00
if ( isInState ( targetState ) ) {
2021-05-04 17:04:01 +00:00
sendSessionTerminate ( reason ) ;
2020-04-14 07:06:07 +00:00
} else {
2021-05-04 17:04:01 +00:00
sendRetract ( reason ) ;
2020-04-08 15:52:47 +00:00
}
2020-04-14 07:06:07 +00:00
}
2020-04-05 08:20:34 +00:00
}
2021-05-04 17:04:01 +00:00
private void sendRetract ( final Reason reason ) {
//TODO embed reason into retract
sendJingleMessage ( " retract " , id . with . asBareJid ( ) ) ;
transitionOrThrow ( reasonToState ( reason ) ) ;
this . finish ( ) ;
}
2021-03-02 20:13:49 +00:00
private void sendSessionInitiate ( final RtpContentMap rtpContentMap , final State targetState ) {
2020-04-06 11:01:17 +00:00
this . initiatorRtpContentMap = rtpContentMap ;
2021-03-02 20:13:49 +00:00
final RtpContentMap outgoingContentMap = encryptSessionInitiate ( rtpContentMap ) ;
final JinglePacket sessionInitiate = outgoingContentMap . toJinglePacket ( JinglePacket . Action . SESSION_INITIATE , id . sessionId ) ;
2021-05-04 17:04:01 +00:00
this . transitionOrThrow ( targetState ) ;
2020-04-05 11:58:05 +00:00
send ( sessionInitiate ) ;
2020-04-03 08:46:42 +00:00
}
2021-03-02 20:13:49 +00:00
private RtpContentMap encryptSessionInitiate ( final RtpContentMap rtpContentMap ) {
if ( this . omemoVerification . hasDeviceId ( ) ) {
final AxolotlService . OmemoVerifiedPayload < OmemoVerifiedRtpContentMap > verifiedPayload ;
try {
verifiedPayload = id . account . getAxolotlService ( ) . encrypt ( rtpContentMap , id . with , omemoVerification . getDeviceId ( ) ) ;
} catch ( final CryptoFailedException e ) {
2021-03-16 17:52:25 +00:00
Log . w ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : unable to use OMEMO DTLS verification on outgoing session initiate. falling back " , e ) ;
2021-03-02 20:13:49 +00:00
return rtpContentMap ;
}
this . omemoVerification . setSessionFingerprint ( verifiedPayload . getFingerprint ( ) ) ;
return verifiedPayload . getPayload ( ) ;
} else {
return rtpContentMap ;
}
}
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-26 08:38:19 +00:00
final State previous = this . state ;
2020-04-08 13:27:17 +00:00
final State target = reasonToState ( reason ) ;
transitionOrThrow ( target ) ;
2020-04-26 08:38:19 +00:00
if ( previous ! = State . NULL ) {
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 ) ;
2020-04-18 16:22:10 +00:00
finish ( ) ;
2020-04-08 13:27:17 +00:00
}
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-26 08:38:19 +00:00
} catch ( final Exception e ) {
2020-04-05 11:58:05 +00:00
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-17 08:56:27 +00:00
xmppConnectionService . sendIqPacket ( id . account , jinglePacket , this : : handleIqResponse ) ;
}
2020-04-09 13:22:03 +00:00
2020-04-17 08:56:27 +00:00
private synchronized void handleIqResponse ( final Account account , final IqPacket 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 ) ;
2020-04-24 07:41:54 +00:00
if ( isTerminated ( ) ) {
2020-04-17 08:56:27 +00:00
Log . i ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : ignoring error because session was already terminated " ) ;
return ;
2020-04-09 13:22:03 +00:00
}
2020-04-17 08:56:27 +00:00
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 ;
}
2020-04-22 16:42:07 +00:00
transitionOrThrow ( target ) ;
this . finish ( ) ;
2020-04-17 08:56:27 +00:00
} else if ( response . getType ( ) = = IqPacket . TYPE . TIMEOUT ) {
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : received IQ timeout in RTP session with " + id . with + " . terminating with connectivity error " ) ;
2020-04-24 07:41:54 +00:00
if ( isTerminated ( ) ) {
2020-04-17 08:56:27 +00:00
Log . i ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : ignoring error because session was already terminated " ) ;
return ;
}
this . webRTCWrapper . close ( ) ;
2020-04-22 16:42:07 +00:00
transitionOrThrow ( State . TERMINATED_CONNECTIVITY_ERROR ) ;
2020-04-18 16:22:10 +00:00
this . finish ( ) ;
2020-04-17 08:56:27 +00:00
}
2020-04-09 13:22:03 +00:00
}
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-18 16:22:10 +00:00
this . webRTCWrapper . close ( ) ;
2020-04-10 05:07:22 +00:00
transitionOrThrow ( State . TERMINATED_APPLICATION_FAILURE ) ;
respondWithOutOfOrder ( jinglePacket ) ;
2020-04-18 16:22:10 +00:00
this . finish ( ) ;
2020-04-10 05:07:22 +00:00
}
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-05-28 07:22:58 +00:00
public void throwStateTransitionException ( ) {
final StateTransitionException exception = this . stateTransitionException ;
if ( exception ! = null ) {
throw new IllegalStateException ( String . format ( " Transition to %s did not call finish " , exception . state ) , exception ) ;
}
}
2020-04-07 11:15:24 +00:00
public RtpEndUserState getEndUserState ( ) {
switch ( this . state ) {
2020-05-30 12:56:12 +00:00
case NULL :
2020-04-07 11:15:24 +00:00
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 :
2020-08-01 06:20:08 +00:00
final PeerConnection . PeerConnectionState state ;
try {
state = webRTCWrapper . getState ( ) ;
2020-08-01 12:18:00 +00:00
} catch ( final WebRTCWrapper . PeerConnectionNotInitialized e ) {
2020-08-01 06:20:08 +00:00
//We usually close the WebRTCWrapper *before* transitioning so we might still
//be in SESSION_ACCEPTED even though the peerConnection has been torn down
return RtpEndUserState . ENDING_CALL ;
}
2020-04-07 11:15:24 +00:00
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-06-12 05:56:59 +00:00
return rtpConnectionStarted = = 0 ? RtpEndUserState . CONNECTIVITY_ERROR : RtpEndUserState . CONNECTIVITY_LOST_ERROR ;
2020-04-07 19:26:51 +00:00
}
case REJECTED :
2021-02-11 15:56:57 +00:00
case REJECTED_RACED :
2020-04-07 19:26:51 +00:00
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 ;
2020-05-09 19:35:21 +00:00
case RETRACTED_RACED :
2021-02-11 15:56:57 +00:00
if ( isInitiator ( ) ) {
return RtpEndUserState . ENDED ;
} else {
return RtpEndUserState . RETRACTED ;
}
2020-04-07 19:26:51 +00:00
case TERMINATED_CONNECTIVITY_ERROR :
2020-06-12 05:56:59 +00:00
return rtpConnectionStarted = = 0 ? RtpEndUserState . CONNECTIVITY_ERROR : RtpEndUserState . CONNECTIVITY_LOST_ERROR ;
2020-04-09 13:22:03 +00:00
case TERMINATED_APPLICATION_FAILURE :
return RtpEndUserState . APPLICATION_ERROR ;
2021-05-04 08:10:30 +00:00
case TERMINATED_SECURITY_ERROR :
return RtpEndUserState . SECURITY_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 ( ) {
2020-04-29 06:51:38 +00:00
final State current = getState ( ) ;
if ( current = = State . NULL ) {
2020-05-30 12:56:12 +00:00
if ( isInitiator ( ) ) {
return Preconditions . checkNotNull (
this . proposedMedia ,
" RTP connection has not been initialized properly "
) ;
}
2020-04-15 16:28:04 +00:00
throw new IllegalStateException ( " RTP connection has not been initialized yet " ) ;
}
2020-04-29 06:51:38 +00:00
if ( Arrays . asList ( State . PROPOSED , State . PROCEED ) . contains ( current ) ) {
2020-05-30 12:56:12 +00:00
return Preconditions . checkNotNull (
this . proposedMedia ,
" RTP connection has not been initialized properly "
) ;
2020-04-16 17:49:34 +00:00
}
final RtpContentMap initiatorContentMap = initiatorRtpContentMap ;
if ( initiatorContentMap ! = null ) {
return initiatorContentMap . getMedia ( ) ;
2020-04-29 13:54:02 +00:00
} else if ( isTerminated ( ) ) {
return Collections . emptySet ( ) ; //we might fail before we ever got a chance to set media
2020-04-16 17:49:34 +00:00
} 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
2021-03-03 08:41:05 +00:00
public boolean isVerified ( ) {
final String fingerprint = this . omemoVerification . getFingerprint ( ) ;
if ( fingerprint = = null ) {
return false ;
}
final FingerprintStatus status = id . account . getAxolotlService ( ) . getFingerprintTrust ( fingerprint ) ;
2021-05-06 16:40:35 +00:00
return status ! = null & & status . isVerified ( ) ;
2021-03-03 08:41: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-22 19:59:20 +00:00
cancelRingingTimeout ( ) ;
2020-04-07 11:15:24 +00:00
acceptCallFromProposed ( ) ;
2020-04-02 19:12:38 +00:00
break ;
case SESSION_INITIALIZED :
2020-04-22 19:59:20 +00:00
cancelRingingTimeout ( ) ;
2020-04-07 11:15:24 +00:00
acceptCallFromSessionInitialized ( ) ;
2020-04-02 19:12:38 +00:00
break ;
2020-04-22 12:49:48 +00:00
case ACCEPTED :
2020-04-22 19:59:20 +00:00
Log . w ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : the call has already been accepted with another client. UI was just lagging behind " ) ;
2020-04-22 12:49:48 +00:00
break ;
case PROCEED :
case SESSION_ACCEPTED :
2020-04-22 19:59:20 +00:00
Log . w ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : the call has already been accepted. user probably double tapped the UI " ) ;
2020-04-22 12:49:48 +00:00
break ;
2020-04-02 19:12:38 +00:00
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-08-24 10:47:54 +00:00
public void notifyPhoneCall ( ) {
Log . d ( Config . LOGTAG , " a phone call has just been started. killing jingle rtp connections " ) ;
if ( Arrays . asList ( State . PROPOSED , State . SESSION_INITIALIZED ) . contains ( this . state ) ) {
rejectCall ( ) ;
} else {
endCall ( ) ;
}
}
2020-04-14 07:53:01 +00:00
public synchronized void rejectCall ( ) {
2020-06-18 18:32:58 +00:00
if ( isTerminated ( ) ) {
2020-06-22 16:07:27 +00:00
Log . w ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : received rejectCall() when session has already been terminated. nothing to do " ) ;
2020-06-18 18:32:58 +00:00
return ;
}
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-24 07:41:54 +00:00
if ( isTerminated ( ) ) {
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 ) ) {
2021-02-11 15:56:57 +00:00
if ( isInitiator ( ) ) {
retractFromProceed ( ) ;
} else {
rejectCallFromProceed ( ) ;
}
2020-04-09 07:04:59 +00:00
return ;
}
if ( isInitiator ( ) & & isInState ( State . SESSION_INITIALIZED , State . SESSION_INITIALIZED_PRE_APPROVED ) ) {
2020-04-18 16:22:10 +00:00
this . webRTCWrapper . close ( ) ;
2020-04-08 13:27:17 +00:00
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-18 16:22:10 +00:00
this . 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
}
2021-02-11 15:56:57 +00:00
private void retractFromProceed ( ) {
Log . d ( Config . LOGTAG , " retract from proceed " ) ;
this . sendJingleMessage ( " retract " ) ;
closeTransitionLogFinish ( State . RETRACTED_RACED ) ;
}
private void closeTransitionLogFinish ( final State state ) {
this . webRTCWrapper . close ( ) ;
transitionOrThrow ( state ) ;
writeLogMessage ( state ) ;
finish ( ) ;
}
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-18 19:27:50 +00:00
this . jingleConnectionManager . ensureConnectionIsRegistered ( this ) ;
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 " ) ;
2020-04-18 16:22:10 +00:00
finish ( ) ;
2020-04-07 19:26:51 +00:00
}
2021-02-11 15:56:57 +00:00
private void rejectCallFromProceed ( ) {
this . sendJingleMessage ( " reject " ) ;
closeTransitionLogFinish ( State . REJECTED_RACED ) ;
}
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 ( ) ;
2021-03-02 20:13:49 +00:00
messagePacket . setType ( MessagePacket . TYPE_CHAT ) ; //we want to carbon copy those
messagePacket . setTo ( to ) ;
final Element intent = messagePacket . addChild ( action , Namespace . JINGLE_MESSAGE ) . setAttribute ( " id " , id . sessionId ) ;
2020-04-10 05:45:23 +00:00
if ( " proceed " . equals ( action ) ) {
messagePacket . setId ( JINGLE_MESSAGE_PROCEED_ID_PREFIX + id . sessionId ) ;
2021-03-03 11:55:27 +00:00
if ( isOmemoEnabled ( ) ) {
final int deviceId = id . account . getAxolotlService ( ) . getOwnDeviceId ( ) ;
final Element device = intent . addChild ( " device " , Namespace . OMEMO_DTLS_SRTP_VERIFICATION ) ;
device . setAttribute ( " id " , deviceId ) ;
}
2020-04-10 05:45:23 +00:00
}
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
2021-03-03 11:55:27 +00:00
private boolean isOmemoEnabled ( ) {
final Conversational conversational = message . getConversation ( ) ;
if ( conversational instanceof Conversation ) {
return ( ( Conversation ) conversational ) . getNextEncryption ( ) = = Message . ENCRYPTION_AXOLOTL ;
}
return false ;
}
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-05-28 07:22:58 +00:00
this . stateTransitionException = new StateTransitionException ( 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-24 07:16:59 +00:00
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-05-09 09:14:39 +00:00
if ( newState = = PeerConnection . PeerConnectionState . CLOSED & & this . rtpConnectionEnded = = 0 ) {
this . rtpConnectionEnded = 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-24 07:41:54 +00:00
if ( isTerminated ( ) ) {
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-18 18:57:15 +00:00
new Thread ( this : : closeWebRTCSessionAfterFailedConnection ) . start ( ) ;
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-18 18:57:15 +00:00
private void closeWebRTCSessionAfterFailedConnection ( ) {
this . webRTCWrapper . close ( ) ;
2020-04-21 20:59:54 +00:00
synchronized ( this ) {
2020-04-24 07:41:54 +00:00
if ( isTerminated ( ) ) {
2020-04-21 20:59:54 +00:00
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : no need to send session-terminate after failed connection. Other party already did " ) ;
return ;
}
sendSessionTerminate ( Reason . CONNECTIVITY_ERROR ) ;
}
2020-04-18 18:57:15 +00:00
}
2020-05-09 09:14:39 +00:00
public long getRtpConnectionStarted ( ) {
return this . rtpConnectionStarted ;
}
public long getRtpConnectionEnded ( ) {
return this . rtpConnectionEnded ;
}
2020-04-13 10:53:23 +00:00
public AppRTCAudioManager getAudioManager ( ) {
return webRTCWrapper . getAudioManager ( ) ;
}
public boolean isMicrophoneEnabled ( ) {
return webRTCWrapper . isMicrophoneEnabled ( ) ;
}
2020-07-09 17:14:28 +00:00
public boolean setMicrophoneEnabled ( final boolean enabled ) {
return webRTCWrapper . setMicrophoneEnabled ( enabled ) ;
2020-04-13 16:30:12 +00:00
}
2020-04-15 17:16:47 +00:00
public boolean isVideoEnabled ( ) {
return webRTCWrapper . isVideoEnabled ( ) ;
}
2020-05-10 12:09:16 +00:00
public void setVideoEnabled ( final boolean enabled ) {
webRTCWrapper . setVideoEnabled ( enabled ) ;
}
2020-05-02 15:15:46 +00:00
public boolean isCameraSwitchable ( ) {
return webRTCWrapper . isCameraSwitchable ( ) ;
}
2020-05-03 09:08:11 +00:00
public boolean isFrontCamera ( ) {
return webRTCWrapper . isFrontCamera ( ) ;
}
public ListenableFuture < Boolean > switchCamera ( ) {
2020-05-02 15:15:46 +00:00
return webRTCWrapper . switchCamera ( ) ;
}
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-27 15:51:38 +00:00
final RtpEndUserState endUserState = getEndUserState ( ) ;
2020-04-29 06:51:38 +00:00
jingleConnectionManager . toneManager . transition ( isInitiator ( ) , endUserState , getMedia ( ) ) ;
2020-04-27 15:51:38 +00:00
xmppConnectionService . notifyJingleRtpConnectionUpdate ( id . account , id . with , id . sessionId , endUserState ) ;
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 ) {
2020-04-21 09:40:05 +00:00
if ( id . account . getXmppConnection ( ) . getFeatures ( ) . externalServiceDiscovery ( ) ) {
2020-04-08 15:52:47 +00:00
final IqPacket request = new IqPacket ( IqPacket . TYPE . GET ) ;
2020-05-17 08:24:46 +00:00
request . setTo ( id . account . getDomain ( ) ) ;
2020-04-08 15:52:47 +00:00
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 ;
}
2020-04-17 12:16:39 +00:00
if ( Arrays . asList ( " stun " , " stuns " , " turn " , " turns " ) . contains ( type ) & & Arrays . asList ( " udp " , " tcp " ) . contains ( transport ) ) {
2020-04-18 16:22:10 +00:00
if ( Arrays . asList ( " stuns " , " turns " ) . contains ( type ) & & " udp " . equals ( transport ) ) {
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : skipping invalid combination of udp/tls in external services " ) ;
2020-04-17 12:16:39 +00:00
continue ;
}
2020-05-01 18:17:20 +00:00
final PeerConnection . IceServer . Builder iceServerBuilder = PeerConnection . IceServer
2020-05-08 15:52:41 +00:00
. builder ( String . format ( " %s:%s:%s?transport=%s " , type , IP . wrapIPv6 ( host ) , port , transport ) ) ;
2020-05-01 18:17:20 +00:00
iceServerBuilder . setTlsCertPolicy ( PeerConnection . TlsCertPolicy . TLS_CERT_POLICY_INSECURE_NO_CHECK ) ;
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 ) ;
}
}
}
}
2020-05-11 10:20:32 +00:00
final List < PeerConnection . IceServer > iceServers = listBuilder . build ( ) ;
2020-04-08 15:52:47 +00:00
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-18 16:22:10 +00:00
private void finish ( ) {
2020-05-08 16:36:52 +00:00
if ( isTerminated ( ) ) {
this . cancelRingingTimeout ( ) ;
this . webRTCWrapper . verifyClosed ( ) ;
2020-06-11 19:17:13 +00:00
this . jingleConnectionManager . setTerminalSessionState ( id , getEndUserState ( ) , getMedia ( ) ) ;
2020-06-07 12:59:55 +00:00
this . jingleConnectionManager . finishConnectionOrThrow ( this ) ;
2020-05-08 16:36:52 +00:00
} else {
throw new IllegalStateException ( String . format ( " Unable to call finish from %s " , this . state ) ) ;
}
2020-04-18 16:22:10 +00:00
}
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 ) ;
2020-06-12 07:08:09 +00:00
xmppConnectionService . createMessageAsync ( message ) ;
2020-04-12 15:12:59 +00:00
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-05-08 15:22:27 +00:00
boolean isTerminated ( ) {
2020-04-24 07:41:54 +00:00
return TERMINATED . contains ( this . state ) ;
}
2020-04-29 06:51:38 +00:00
public Optional < VideoTrack > getLocalVideoTrack ( ) {
2020-04-14 17:06:39 +00:00
return webRTCWrapper . getLocalVideoTrack ( ) ;
}
public Optional < VideoTrack > getRemoteVideoTrack ( ) {
return webRTCWrapper . getRemoteVideoTrack ( ) ;
}
public EglBase . Context getEglBaseContext ( ) {
return webRTCWrapper . getEglBaseContext ( ) ;
}
2020-04-24 07:16:59 +00:00
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-08-01 07:50:51 +00:00
public void fireStateUpdate ( ) {
final RtpEndUserState endUserState = getEndUserState ( ) ;
xmppConnectionService . notifyJingleRtpConnectionUpdate ( id . account , id . with , id . sessionId , endUserState ) ;
}
2020-04-08 15:52:47 +00:00
private interface OnIceServersDiscovered {
void onIceServersDiscovered ( List < PeerConnection . IceServer > iceServers ) ;
}
2020-05-28 07:22:58 +00:00
private static class StateTransitionException extends Exception {
private final State state ;
private StateTransitionException ( final State state ) {
this . state = state ;
}
}
2020-04-02 09:30:16 +00:00
}