2014-04-08 21:15:55 +00:00
package eu.siacs.conversations.xmpp.jingle ;
2020-04-10 13:19:56 +00:00
import android.util.Base64 ;
2015-07-20 12:26:29 +00:00
import android.util.Log ;
2020-04-04 09:31:53 +00:00
import com.google.common.base.Objects ;
2020-04-27 09:53:31 +00:00
import com.google.common.base.Optional ;
2020-04-01 08:45:03 +00:00
import com.google.common.base.Preconditions ;
2020-04-19 12:23:52 +00:00
import com.google.common.cache.Cache ;
import com.google.common.cache.CacheBuilder ;
2020-04-15 08:49:38 +00:00
import com.google.common.collect.Collections2 ;
2020-05-10 12:09:16 +00:00
import com.google.common.collect.ComparisonChain ;
2020-04-15 10:07:19 +00:00
import com.google.common.collect.ImmutableSet ;
2020-04-15 08:49:38 +00:00
2020-04-07 11:15:24 +00:00
import java.lang.ref.WeakReference ;
2020-04-10 13:19:56 +00:00
import java.security.SecureRandom ;
2020-04-15 08:49:38 +00:00
import java.util.Collection ;
import java.util.Collections ;
2014-04-19 00:19:26 +00:00
import java.util.HashMap ;
2020-04-15 08:49:38 +00:00
import java.util.List ;
2020-04-01 08:45:03 +00:00
import java.util.Map ;
2020-04-15 08:49:38 +00:00
import java.util.Set ;
2020-04-01 08:45:03 +00:00
import java.util.concurrent.ConcurrentHashMap ;
2020-04-22 19:59:20 +00:00
import java.util.concurrent.Executors ;
import java.util.concurrent.ScheduledExecutorService ;
import java.util.concurrent.ScheduledFuture ;
2020-04-19 12:23:52 +00:00
import java.util.concurrent.TimeUnit ;
2015-07-20 12:26:29 +00:00
2014-08-31 14:28:21 +00:00
import eu.siacs.conversations.Config ;
2014-04-08 21:15:55 +00:00
import eu.siacs.conversations.entities.Account ;
2020-04-20 10:32:56 +00:00
import eu.siacs.conversations.entities.Contact ;
2020-04-12 16:07:31 +00:00
import eu.siacs.conversations.entities.Conversation ;
import eu.siacs.conversations.entities.Conversational ;
2014-04-08 21:15:55 +00:00
import eu.siacs.conversations.entities.Message ;
2020-04-16 08:51:34 +00:00
import eu.siacs.conversations.entities.RtpSessionStatus ;
2015-07-20 12:26:29 +00:00
import eu.siacs.conversations.entities.Transferable ;
2014-10-14 10:02:48 +00:00
import eu.siacs.conversations.services.AbstractConnectionManager ;
2014-04-08 21:15:55 +00:00
import eu.siacs.conversations.services.XmppConnectionService ;
import eu.siacs.conversations.xml.Element ;
2020-04-01 08:45:03 +00:00
import eu.siacs.conversations.xml.Namespace ;
2020-05-21 13:39:59 +00:00
import eu.siacs.conversations.xmpp.Jid ;
2014-04-08 21:15:55 +00:00
import eu.siacs.conversations.xmpp.OnIqPacketReceived ;
2020-04-01 16:35:36 +00:00
import eu.siacs.conversations.xmpp.jingle.stanzas.Content ;
import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription ;
2020-04-15 08:49:38 +00:00
import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription ;
2014-04-08 21:15:55 +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-10 14:02:01 +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 ;
2014-04-08 21:15:55 +00:00
import eu.siacs.conversations.xmpp.stanzas.IqPacket ;
2020-04-04 09:31:53 +00:00
import eu.siacs.conversations.xmpp.stanzas.MessagePacket ;
2014-04-08 21:15:55 +00:00
2014-10-13 23:06:45 +00:00
public class JingleConnectionManager extends AbstractConnectionManager {
2020-05-01 05:58:58 +00:00
static final ScheduledExecutorService SCHEDULED_EXECUTOR_SERVICE = Executors . newSingleThreadScheduledExecutor ( ) ;
2020-05-21 13:39:59 +00:00
final ToneManager toneManager ;
2020-04-07 09:36:28 +00:00
private final HashMap < RtpSessionProposal , DeviceDiscoveryState > rtpSessionProposals = new HashMap < > ( ) ;
2020-04-27 15:51:38 +00:00
private final ConcurrentHashMap < AbstractJingleConnection . Id , AbstractJingleConnection > connections = new ConcurrentHashMap < > ( ) ;
2019-12-05 13:03:54 +00:00
2020-06-11 19:17:13 +00:00
private final Cache < PersistableSessionId , TerminatedRtpSession > terminatedSessions = CacheBuilder . newBuilder ( )
. expireAfterWrite ( 24 , TimeUnit . HOURS )
2020-04-19 12:23:52 +00:00
. build ( ) ;
2019-12-05 13:03:54 +00:00
private HashMap < Jid , JingleCandidate > primaryCandidates = new HashMap < > ( ) ;
public JingleConnectionManager ( XmppConnectionService service ) {
super ( service ) ;
2020-05-21 13:39:59 +00:00
this . toneManager = new ToneManager ( service ) ;
2019-12-05 13:03:54 +00:00
}
2020-04-10 14:02:01 +00:00
static String nextRandomId ( ) {
final byte [ ] id = new byte [ 16 ] ;
new SecureRandom ( ) . nextBytes ( id ) ;
return Base64 . encodeToString ( id , Base64 . NO_WRAP | Base64 . NO_PADDING ) ;
}
2020-04-01 08:45:03 +00:00
public void deliverPacket ( final Account account , final JinglePacket packet ) {
2020-04-10 17:22:29 +00:00
final String sessionId = packet . getSessionId ( ) ;
if ( sessionId = = null ) {
respondWithJingleError ( account , packet , " unknown-session " , " item-not-found " , " cancel " ) ;
return ;
}
2020-04-01 08:45:03 +00:00
final AbstractJingleConnection . Id id = AbstractJingleConnection . Id . of ( account , packet ) ;
2020-04-02 14:29:33 +00:00
final AbstractJingleConnection existingJingleConnection = connections . get ( id ) ;
if ( existingJingleConnection ! = null ) {
existingJingleConnection . deliverPacket ( packet ) ;
} else if ( packet . getAction ( ) = = JinglePacket . Action . SESSION_INITIATE ) {
2020-04-03 06:16:55 +00:00
final Jid from = packet . getFrom ( ) ;
2020-04-01 16:35:36 +00:00
final Content content = packet . getJingleContent ( ) ;
final String descriptionNamespace = content = = null ? null : content . getDescriptionNamespace ( ) ;
final AbstractJingleConnection connection ;
if ( FileTransferDescription . NAMESPACES . contains ( descriptionNamespace ) ) {
2020-04-03 06:16:55 +00:00
connection = new JingleFileTransferConnection ( this , id , from ) ;
2020-04-10 14:28:15 +00:00
} else if ( Namespace . JINGLE_APPS_RTP . equals ( descriptionNamespace ) & & ! usesTor ( account ) ) {
2020-06-11 19:17:13 +00:00
final boolean sessionEnded = this . terminatedSessions . asMap ( ) . containsKey ( PersistableSessionId . of ( id ) ) ;
2020-04-20 10:32:56 +00:00
final boolean stranger = isWithStrangerAndStrangerNotificationsAreOff ( account , id . with ) ;
if ( isBusy ( ) | | sessionEnded | | stranger ) {
2020-04-21 10:00:13 +00:00
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : rejected session with " + id . with + " because busy. sessionEnded= " + sessionEnded + " , stranger= " + stranger ) ;
2020-04-10 14:02:01 +00:00
mXmppConnectionService . sendIqPacket ( account , packet . generateResponse ( IqPacket . TYPE . RESULT ) , null ) ;
final JinglePacket sessionTermination = new JinglePacket ( JinglePacket . Action . SESSION_TERMINATE , id . sessionId ) ;
sessionTermination . setTo ( id . with ) ;
sessionTermination . setReason ( Reason . BUSY , null ) ;
mXmppConnectionService . sendIqPacket ( account , sessionTermination , null ) ;
return ;
}
2020-04-03 06:16:55 +00:00
connection = new JingleRtpConnection ( this , id , from ) ;
2020-04-01 16:35:36 +00:00
} else {
2020-04-09 13:22:03 +00:00
respondWithJingleError ( account , packet , " unsupported-info " , " feature-not-implemented " , " cancel " ) ;
2020-04-01 16:35:36 +00:00
return ;
}
2020-04-01 08:45:03 +00:00
connections . put ( id , connection ) ;
2020-04-27 09:53:31 +00:00
mXmppConnectionService . updateConversationUi ( ) ;
2020-04-01 16:35:36 +00:00
connection . deliverPacket ( packet ) ;
2019-12-05 13:03:54 +00:00
} else {
2020-04-02 14:29:33 +00:00
Log . d ( Config . LOGTAG , " unable to route jingle packet: " + packet ) ;
2020-04-09 13:22:03 +00:00
respondWithJingleError ( account , packet , " unknown-session " , " item-not-found " , " cancel " ) ;
2020-04-02 14:29:33 +00:00
}
}
2020-04-10 14:28:15 +00:00
private boolean usesTor ( final Account account ) {
return account . isOnion ( ) | | mXmppConnectionService . useTorToConnect ( ) ;
}
2020-04-19 08:12:33 +00:00
public boolean isBusy ( ) {
2020-04-10 14:02:01 +00:00
for ( AbstractJingleConnection connection : this . connections . values ( ) ) {
if ( connection instanceof JingleRtpConnection ) {
2020-04-24 07:41:54 +00:00
if ( ( ( JingleRtpConnection ) connection ) . isTerminated ( ) ) {
continue ;
}
2020-04-10 14:02:01 +00:00
return true ;
}
}
synchronized ( this . rtpSessionProposals ) {
return this . rtpSessionProposals . containsValue ( DeviceDiscoveryState . DISCOVERED ) | | this . rtpSessionProposals . containsValue ( DeviceDiscoveryState . SEARCHING ) ;
}
}
2020-05-27 11:54:35 +00:00
private Optional < RtpSessionProposal > findMatchingSessionProposal ( final Account account , final Jid with , final Set < Media > media ) {
2020-05-10 12:09:16 +00:00
synchronized ( this . rtpSessionProposals ) {
for ( Map . Entry < RtpSessionProposal , DeviceDiscoveryState > entry : this . rtpSessionProposals . entrySet ( ) ) {
final RtpSessionProposal proposal = entry . getKey ( ) ;
final DeviceDiscoveryState state = entry . getValue ( ) ;
final boolean openProposal = state = = DeviceDiscoveryState . DISCOVERED | | state = = DeviceDiscoveryState . SEARCHING ;
if ( openProposal
& & proposal . account = = account
& & proposal . with . equals ( with . asBareJid ( ) )
& & proposal . media . equals ( media ) ) {
return Optional . of ( proposal ) ;
}
}
}
return Optional . absent ( ) ;
}
private boolean hasMatchingRtpSession ( final Account account , final Jid with , final Set < Media > media ) {
for ( AbstractJingleConnection connection : this . connections . values ( ) ) {
if ( connection instanceof JingleRtpConnection ) {
final JingleRtpConnection rtpConnection = ( JingleRtpConnection ) connection ;
if ( rtpConnection . isTerminated ( ) ) {
continue ;
}
if ( rtpConnection . getId ( ) . account = = account
& & rtpConnection . getId ( ) . with . asBareJid ( ) . equals ( with . asBareJid ( ) )
& & rtpConnection . getMedia ( ) . equals ( media ) ) {
return true ;
}
}
}
return false ;
}
2020-04-20 10:32:56 +00:00
private boolean isWithStrangerAndStrangerNotificationsAreOff ( final Account account , Jid with ) {
final boolean notifyForStrangers = mXmppConnectionService . getNotificationService ( ) . notificationsFromStrangers ( ) ;
if ( notifyForStrangers ) {
return false ;
}
final Contact contact = account . getRoster ( ) . getContact ( with ) ;
return ! contact . showInContactList ( ) ;
}
2020-04-24 07:41:54 +00:00
ScheduledFuture < ? > schedule ( final Runnable runnable , final long delay , final TimeUnit timeUnit ) {
2020-05-01 05:58:58 +00:00
return SCHEDULED_EXECUTOR_SERVICE . schedule ( runnable , delay , timeUnit ) ;
2020-04-22 19:59:20 +00:00
}
2020-04-24 07:41:54 +00:00
void respondWithJingleError ( final Account account , final IqPacket original , String jingleCondition , String condition , String conditionType ) {
2020-04-09 13:22:03 +00:00
final IqPacket response = original . generateResponse ( IqPacket . TYPE . ERROR ) ;
final Element error = response . addChild ( " error " ) ;
error . setAttribute ( " type " , conditionType ) ;
error . addChild ( condition , " urn:ietf:params:xml:ns:xmpp-stanzas " ) ;
error . addChild ( jingleCondition , " urn:xmpp:jingle:errors:1 " ) ;
account . getXmppConnection ( ) . sendIqPacket ( response , null ) ;
}
2020-05-09 19:35:21 +00:00
public void deliverMessage ( final Account account , final Jid to , final Jid from , final Element message , String remoteMsgId , String serverMsgId , long timestamp ) {
2020-04-02 14:29:33 +00:00
Preconditions . checkArgument ( Namespace . JINGLE_MESSAGE . equals ( message . getNamespace ( ) ) ) ;
final String sessionId = message . getAttribute ( " id " ) ;
if ( sessionId = = null ) {
return ;
}
2020-04-08 10:17:46 +00:00
if ( " accept " . equals ( message . getName ( ) ) ) {
for ( AbstractJingleConnection connection : connections . values ( ) ) {
if ( connection instanceof JingleRtpConnection ) {
final JingleRtpConnection rtpConnection = ( JingleRtpConnection ) connection ;
final AbstractJingleConnection . Id id = connection . getId ( ) ;
if ( id . account = = account & & id . sessionId . equals ( sessionId ) ) {
2020-04-12 16:07:31 +00:00
rtpConnection . deliveryMessage ( from , message , serverMsgId , timestamp ) ;
2020-04-08 10:17:46 +00:00
return ;
}
}
}
return ;
}
2020-04-15 08:49:38 +00:00
final boolean fromSelf = from . asBareJid ( ) . equals ( account . getJid ( ) . asBareJid ( ) ) ;
2020-05-09 19:35:21 +00:00
final boolean addressedDirectly = to ! = null & & to . equals ( account . getJid ( ) ) ;
2020-04-10 19:18:43 +00:00
final AbstractJingleConnection . Id id ;
2020-04-15 08:49:38 +00:00
if ( fromSelf ) {
2020-04-23 18:11:45 +00:00
if ( to ! = null & & to . isFullJid ( ) ) {
2020-04-10 19:18:43 +00:00
id = AbstractJingleConnection . Id . of ( account , to , sessionId ) ;
} else {
return ;
}
2020-04-02 14:29:33 +00:00
} else {
2020-04-10 19:18:43 +00:00
id = AbstractJingleConnection . Id . of ( account , from , sessionId ) ;
2020-04-02 14:29:33 +00:00
}
final AbstractJingleConnection existingJingleConnection = connections . get ( id ) ;
if ( existingJingleConnection ! = null ) {
if ( existingJingleConnection instanceof JingleRtpConnection ) {
2020-04-12 16:07:31 +00:00
( ( JingleRtpConnection ) existingJingleConnection ) . deliveryMessage ( from , message , serverMsgId , timestamp ) ;
2020-04-02 14:29:33 +00:00
} else {
Log . d ( Config . LOGTAG , account . getJid ( ) . asBareJid ( ) + " : " + existingJingleConnection . getClass ( ) . getName ( ) + " does not support jingle messages " ) ;
}
2020-04-10 14:02:01 +00:00
return ;
}
2020-04-10 19:18:43 +00:00
2020-04-15 08:49:38 +00:00
if ( fromSelf ) {
2020-04-16 08:51:34 +00:00
if ( " proceed " . equals ( message . getName ( ) ) ) {
final Conversation c = mXmppConnectionService . findOrCreateConversation ( account , id . with , false , false ) ;
final Message previousBusy = c . findRtpSession ( sessionId , Message . STATUS_RECEIVED ) ;
if ( previousBusy ! = null ) {
previousBusy . setBody ( new RtpSessionStatus ( true , 0 ) . toString ( ) ) ;
if ( serverMsgId ! = null ) {
previousBusy . setServerMsgId ( serverMsgId ) ;
}
previousBusy . setTime ( timestamp ) ;
mXmppConnectionService . updateMessage ( previousBusy , true ) ;
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : updated previous busy because call got picked up by another device " ) ;
return ;
}
}
2020-04-10 14:02:01 +00:00
Log . d ( Config . LOGTAG , account . getJid ( ) . asBareJid ( ) + " : ignore jingle message from self " ) ;
2020-04-15 08:49:38 +00:00
return ;
2020-04-10 14:02:01 +00:00
}
if ( " propose " . equals ( message . getName ( ) ) ) {
2020-04-15 08:49:38 +00:00
final Propose propose = Propose . upgrade ( message ) ;
final List < GenericDescription > descriptions = propose . getDescriptions ( ) ;
final Collection < RtpDescription > rtpDescriptions = Collections2 . transform (
Collections2 . filter ( descriptions , d - > d instanceof RtpDescription ) ,
input - > ( RtpDescription ) input
) ;
if ( rtpDescriptions . size ( ) > 0 & & rtpDescriptions . size ( ) = = descriptions . size ( ) & & ! usesTor ( account ) ) {
final Collection < Media > media = Collections2 . transform ( rtpDescriptions , RtpDescription : : getMedia ) ;
if ( media . contains ( Media . UNKNOWN ) ) {
2020-04-15 10:07:19 +00:00
Log . d ( Config . LOGTAG , account . getJid ( ) . asBareJid ( ) + " : encountered unknown media in session proposal. " + propose ) ;
2020-04-15 08:49:38 +00:00
return ;
}
2020-05-10 12:09:16 +00:00
final Optional < RtpSessionProposal > matchingSessionProposal = findMatchingSessionProposal ( account , id . with , ImmutableSet . copyOf ( media ) ) ;
if ( matchingSessionProposal . isPresent ( ) ) {
final String ourSessionId = matchingSessionProposal . get ( ) . sessionId ;
final String theirSessionId = id . sessionId ;
if ( ComparisonChain . start ( )
. compare ( ourSessionId , theirSessionId )
. compare ( account . getJid ( ) . toEscapedString ( ) , id . with . toEscapedString ( ) )
. result ( ) > 0 ) {
Log . d ( Config . LOGTAG , account . getJid ( ) . asBareJid ( ) + " : our session lost tie break. automatically accepting their session. winning Session= " + theirSessionId ) ;
//TODO a retract for this reason should probably include some indication of tie break
retractSessionProposal ( matchingSessionProposal . get ( ) ) ;
final JingleRtpConnection rtpConnection = new JingleRtpConnection ( this , id , from ) ;
this . connections . put ( id , rtpConnection ) ;
rtpConnection . setProposedMedia ( ImmutableSet . copyOf ( media ) ) ;
rtpConnection . deliveryMessage ( from , message , serverMsgId , timestamp ) ;
} else {
Log . d ( Config . LOGTAG , account . getJid ( ) . asBareJid ( ) + " : our session won tie break. waiting for other party to accept. winningSession= " + ourSessionId ) ;
}
return ;
}
2020-04-20 10:32:56 +00:00
final boolean stranger = isWithStrangerAndStrangerNotificationsAreOff ( account , id . with ) ;
if ( isBusy ( ) | | stranger ) {
2020-04-16 08:51:34 +00:00
writeLogMissedIncoming ( account , id . with . asBareJid ( ) , id . sessionId , serverMsgId , timestamp ) ;
2020-04-20 10:32:56 +00:00
if ( stranger ) {
2020-04-21 10:00:13 +00:00
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : ignoring call proposal from stranger " + id . with ) ;
2020-04-20 10:32:56 +00:00
return ;
}
2020-05-02 07:50:17 +00:00
final int activeDevices = account . activeDevicesWithRtpCapability ( ) ;
Log . d ( Config . LOGTAG , " active devices with rtp capability: " + activeDevices ) ;
2020-04-16 08:51:34 +00:00
if ( activeDevices = = 0 ) {
final MessagePacket reject = mXmppConnectionService . getMessageGenerator ( ) . sessionReject ( from , sessionId ) ;
mXmppConnectionService . sendMessagePacket ( account , reject ) ;
} else {
Log . d ( Config . LOGTAG , id . account . getJid ( ) . asBareJid ( ) + " : ignoring proposal because busy on this device but there are other devices " ) ;
}
2020-04-10 14:02:01 +00:00
} else {
2020-04-10 19:18:43 +00:00
final JingleRtpConnection rtpConnection = new JingleRtpConnection ( this , id , from ) ;
2020-04-10 14:02:01 +00:00
this . connections . put ( id , rtpConnection ) ;
2020-04-15 10:07:19 +00:00
rtpConnection . setProposedMedia ( ImmutableSet . copyOf ( media ) ) ;
2020-04-12 16:07:31 +00:00
rtpConnection . deliveryMessage ( from , message , serverMsgId , timestamp ) ;
2020-04-10 14:02:01 +00:00
}
2020-04-01 08:45:03 +00:00
} else {
2020-04-15 08:49:38 +00:00
Log . d ( Config . LOGTAG , account . getJid ( ) . asBareJid ( ) + " : unable to react to proposed session with " + rtpDescriptions . size ( ) + " rtp descriptions of " + descriptions . size ( ) + " total descriptions " ) ;
2019-12-05 13:03:54 +00:00
}
2020-05-09 19:35:21 +00:00
} else if ( addressedDirectly & & " proceed " . equals ( message . getName ( ) ) ) {
2020-04-04 09:31:53 +00:00
synchronized ( rtpSessionProposals ) {
2020-04-15 10:07:19 +00:00
final RtpSessionProposal proposal = getRtpSessionProposal ( account , from . asBareJid ( ) , sessionId ) ;
2020-04-15 08:49:38 +00:00
if ( proposal ! = null ) {
rtpSessionProposals . remove ( proposal ) ;
2020-04-04 09:31:53 +00:00
final JingleRtpConnection rtpConnection = new JingleRtpConnection ( this , id , account . getJid ( ) ) ;
2020-04-15 08:49:38 +00:00
rtpConnection . setProposedMedia ( proposal . media ) ;
2020-04-04 09:31:53 +00:00
this . connections . put ( id , rtpConnection ) ;
rtpConnection . transitionOrThrow ( AbstractJingleConnection . State . PROPOSED ) ;
2020-04-12 16:07:31 +00:00
rtpConnection . deliveryMessage ( from , message , serverMsgId , timestamp ) ;
2020-04-04 09:31:53 +00:00
} else {
2020-04-10 19:18:43 +00:00
Log . d ( Config . LOGTAG , account . getJid ( ) . asBareJid ( ) + " : no rtp session proposal found for " + from + " to deliver proceed " ) ;
2020-05-09 19:35:21 +00:00
if ( remoteMsgId = = null ) {
return ;
}
final MessagePacket errorMessage = new MessagePacket ( ) ;
errorMessage . setTo ( from ) ;
errorMessage . setId ( remoteMsgId ) ;
errorMessage . setType ( MessagePacket . TYPE_ERROR ) ;
final Element error = errorMessage . addChild ( " error " ) ;
error . setAttribute ( " code " , " 404 " ) ;
error . setAttribute ( " type " , " cancel " ) ;
error . addChild ( " item-not-found " , " urn:ietf:params:xml:ns:xmpp-stanzas " ) ;
mXmppConnectionService . sendMessagePacket ( account , errorMessage ) ;
2020-04-08 08:46:26 +00:00
}
}
2020-05-09 19:35:21 +00:00
} else if ( addressedDirectly & & " reject " . equals ( message . getName ( ) ) ) {
2020-05-21 13:39:59 +00:00
final RtpSessionProposal proposal = getRtpSessionProposal ( account , from . asBareJid ( ) , sessionId ) ;
2020-04-08 08:46:26 +00:00
synchronized ( rtpSessionProposals ) {
2020-05-21 13:39:59 +00:00
if ( proposal ! = null & & rtpSessionProposals . remove ( proposal ) ! = null ) {
2020-04-12 16:07:31 +00:00
writeLogMissedOutgoing ( account , proposal . with , proposal . sessionId , serverMsgId , timestamp ) ;
2020-05-21 13:39:59 +00:00
toneManager . transition ( RtpEndUserState . DECLINED_OR_BUSY , proposal . media ) ;
2020-04-08 08:46:26 +00:00
mXmppConnectionService . notifyJingleRtpConnectionUpdate ( account , proposal . with , proposal . sessionId , RtpEndUserState . DECLINED_OR_BUSY ) ;
} else {
2020-04-10 19:18:43 +00:00
Log . d ( Config . LOGTAG , account . getJid ( ) . asBareJid ( ) + " : no rtp session proposal found for " + from + " to deliver reject " ) ;
2020-04-04 09:31:53 +00:00
}
}
2020-04-02 14:29:33 +00:00
} else {
2020-05-10 12:09:16 +00:00
Log . d ( Config . LOGTAG , account . getJid ( ) . asBareJid ( ) + " : retrieved out of order jingle message " + message ) ;
2019-12-05 13:03:54 +00:00
}
2020-04-02 14:29:33 +00:00
2019-12-05 13:03:54 +00:00
}
2020-04-15 08:49:38 +00:00
private RtpSessionProposal getRtpSessionProposal ( final Account account , Jid from , String sessionId ) {
2020-04-15 10:07:19 +00:00
for ( RtpSessionProposal rtpSessionProposal : rtpSessionProposals . keySet ( ) ) {
2020-04-15 08:49:38 +00:00
if ( rtpSessionProposal . sessionId . equals ( sessionId ) & & rtpSessionProposal . with . equals ( from ) & & rtpSessionProposal . account . getJid ( ) . equals ( account . getJid ( ) ) ) {
return rtpSessionProposal ;
}
}
return null ;
}
2020-04-12 16:07:31 +00:00
private void writeLogMissedOutgoing ( final Account account , Jid with , final String sessionId , String serverMsgId , long timestamp ) {
final Conversation conversation = mXmppConnectionService . findOrCreateConversation (
account ,
with . asBareJid ( ) ,
false ,
false
) ;
final Message message = new Message (
conversation ,
Message . STATUS_SEND ,
Message . TYPE_RTP_SESSION ,
sessionId
) ;
2020-04-16 08:51:34 +00:00
message . setBody ( new RtpSessionStatus ( false , 0 ) . toString ( ) ) ;
message . setServerMsgId ( serverMsgId ) ;
message . setTime ( timestamp ) ;
writeMessage ( message ) ;
}
private void writeLogMissedIncoming ( final Account account , Jid with , final String sessionId , String serverMsgId , long timestamp ) {
final Conversation conversation = mXmppConnectionService . findOrCreateConversation (
account ,
with . asBareJid ( ) ,
false ,
false
) ;
final Message message = new Message (
conversation ,
Message . STATUS_RECEIVED ,
Message . TYPE_RTP_SESSION ,
sessionId
) ;
message . setBody ( new RtpSessionStatus ( false , 0 ) . toString ( ) ) ;
2020-04-12 16:07:31 +00:00
message . setServerMsgId ( serverMsgId ) ;
message . setTime ( timestamp ) ;
writeMessage ( message ) ;
}
private void writeMessage ( final Message message ) {
final Conversational conversational = message . getConversation ( ) ;
if ( conversational instanceof Conversation ) {
( ( Conversation ) conversational ) . add ( message ) ;
mXmppConnectionService . databaseBackend . createMessage ( message ) ;
mXmppConnectionService . updateConversationUi ( ) ;
} else {
throw new IllegalStateException ( " Somehow the conversation in a message was a stub " ) ;
}
}
2020-04-01 08:45:03 +00:00
public void startJingleFileTransfer ( final Message message ) {
Preconditions . checkArgument ( message . isFileOrImage ( ) , " Message is not of type file or image " ) ;
final Transferable old = message . getTransferable ( ) ;
2019-12-05 13:03:54 +00:00
if ( old ! = null ) {
old . cancel ( ) ;
}
2020-04-03 06:16:55 +00:00
final Account account = message . getConversation ( ) . getAccount ( ) ;
2020-04-01 08:45:03 +00:00
final AbstractJingleConnection . Id id = AbstractJingleConnection . Id . of ( message ) ;
2020-04-03 06:16:55 +00:00
final JingleFileTransferConnection connection = new JingleFileTransferConnection ( this , id , account . getJid ( ) ) ;
2019-12-05 13:03:54 +00:00
mXmppConnectionService . markMessage ( message , Message . STATUS_WAITING ) ;
2020-04-01 08:45:03 +00:00
this . connections . put ( id , connection ) ;
2020-04-03 06:16:55 +00:00
connection . init ( message ) ;
2019-12-05 13:03:54 +00:00
}
2020-05-02 07:50:17 +00:00
public Optional < OngoingRtpSession > getOngoingRtpConnection ( final Contact contact ) {
2020-04-27 15:51:38 +00:00
for ( final Map . Entry < AbstractJingleConnection . Id , AbstractJingleConnection > entry : this . connections . entrySet ( ) ) {
2020-04-27 09:53:31 +00:00
if ( entry . getValue ( ) instanceof JingleRtpConnection ) {
final AbstractJingleConnection . Id id = entry . getKey ( ) ;
if ( id . account = = contact . getAccount ( ) & & id . with . asBareJid ( ) . equals ( contact . getJid ( ) . asBareJid ( ) ) ) {
return Optional . of ( id ) ;
}
}
}
2020-05-02 07:50:17 +00:00
synchronized ( this . rtpSessionProposals ) {
for ( Map . Entry < RtpSessionProposal , DeviceDiscoveryState > entry : this . rtpSessionProposals . entrySet ( ) ) {
RtpSessionProposal proposal = entry . getKey ( ) ;
if ( proposal . account = = contact . getAccount ( ) & & contact . getJid ( ) . asBareJid ( ) . equals ( proposal . with ) ) {
final DeviceDiscoveryState preexistingState = entry . getValue ( ) ;
if ( preexistingState ! = null & & preexistingState ! = DeviceDiscoveryState . FAILED ) {
return Optional . of ( proposal ) ;
}
}
}
}
2020-04-27 09:53:31 +00:00
return Optional . absent ( ) ;
}
2020-04-01 08:45:03 +00:00
void finishConnection ( final AbstractJingleConnection connection ) {
2020-06-07 12:59:55 +00:00
this . connections . remove ( connection . getId ( ) ) ;
}
void finishConnectionOrThrow ( final AbstractJingleConnection connection ) {
2020-05-27 11:54:35 +00:00
final AbstractJingleConnection . Id id = connection . getId ( ) ;
if ( this . connections . remove ( id ) = = null ) {
throw new IllegalStateException ( String . format ( " Unable to finish connection with id=%s " , id . toString ( ) ) ) ;
}
2019-12-05 13:03:54 +00:00
}
2020-04-01 08:45:03 +00:00
void getPrimaryCandidate ( final Account account , final boolean initiator , final OnPrimaryCandidateFound listener ) {
2019-12-05 13:03:54 +00:00
if ( Config . DISABLE_PROXY_LOOKUP ) {
listener . onPrimaryCandidateFound ( false , null ) ;
return ;
}
if ( ! this . primaryCandidates . containsKey ( account . getJid ( ) . asBareJid ( ) ) ) {
final Jid proxy = account . getXmppConnection ( ) . findDiscoItemByFeature ( Namespace . BYTE_STREAMS ) ;
if ( proxy ! = null ) {
IqPacket iq = new IqPacket ( IqPacket . TYPE . GET ) ;
iq . setTo ( proxy ) ;
iq . query ( Namespace . BYTE_STREAMS ) ;
account . getXmppConnection ( ) . sendIqPacket ( iq , new OnIqPacketReceived ( ) {
@Override
public void onIqPacketReceived ( Account account , IqPacket packet ) {
2020-04-01 08:45:03 +00:00
final Element streamhost = packet . query ( ) . findChild ( " streamhost " , Namespace . BYTE_STREAMS ) ;
2019-12-05 13:03:54 +00:00
final String host = streamhost = = null ? null : streamhost . getAttribute ( " host " ) ;
final String port = streamhost = = null ? null : streamhost . getAttribute ( " port " ) ;
if ( host ! = null & & port ! = null ) {
try {
JingleCandidate candidate = new JingleCandidate ( nextRandomId ( ) , true ) ;
candidate . setHost ( host ) ;
candidate . setPort ( Integer . parseInt ( port ) ) ;
candidate . setType ( JingleCandidate . TYPE_PROXY ) ;
candidate . setJid ( proxy ) ;
candidate . setPriority ( 655360 + ( initiator ? 30 : 0 ) ) ;
primaryCandidates . put ( account . getJid ( ) . asBareJid ( ) , candidate ) ;
listener . onPrimaryCandidateFound ( true , candidate ) ;
} catch ( final NumberFormatException e ) {
listener . onPrimaryCandidateFound ( false , null ) ;
}
} else {
listener . onPrimaryCandidateFound ( false , null ) ;
}
}
} ) ;
} else {
listener . onPrimaryCandidateFound ( false , null ) ;
}
} else {
listener . onPrimaryCandidateFound ( true ,
this . primaryCandidates . get ( account . getJid ( ) . asBareJid ( ) ) ) ;
}
}
2020-04-08 07:42:06 +00:00
public void retractSessionProposal ( final Account account , final Jid with ) {
2020-04-04 09:31:53 +00:00
synchronized ( this . rtpSessionProposals ) {
2020-04-08 07:42:06 +00:00
RtpSessionProposal matchingProposal = null ;
for ( RtpSessionProposal proposal : this . rtpSessionProposals . keySet ( ) ) {
if ( proposal . account = = account & & with . asBareJid ( ) . equals ( proposal . with ) ) {
matchingProposal = proposal ;
break ;
}
}
if ( matchingProposal ! = null ) {
2020-05-10 12:09:16 +00:00
retractSessionProposal ( matchingProposal ) ;
2020-04-08 07:42:06 +00:00
}
}
}
2020-05-10 12:09:16 +00:00
private void retractSessionProposal ( RtpSessionProposal rtpSessionProposal ) {
final Account account = rtpSessionProposal . account ;
2020-05-21 13:39:59 +00:00
toneManager . transition ( RtpEndUserState . ENDED , rtpSessionProposal . media ) ;
2020-05-10 12:09:16 +00:00
Log . d ( Config . LOGTAG , account . getJid ( ) . asBareJid ( ) + " : retracting rtp session proposal with " + rtpSessionProposal . with ) ;
this . rtpSessionProposals . remove ( rtpSessionProposal ) ;
final MessagePacket messagePacket = mXmppConnectionService . getMessageGenerator ( ) . sessionRetract ( rtpSessionProposal ) ;
writeLogMissedOutgoing ( account , rtpSessionProposal . with , rtpSessionProposal . sessionId , null , System . currentTimeMillis ( ) ) ;
mXmppConnectionService . sendMessagePacket ( account , messagePacket ) ;
}
2020-05-30 12:56:12 +00:00
public String initializeRtpSession ( final Account account , final Jid with , final Set < Media > media ) {
final AbstractJingleConnection . Id id = AbstractJingleConnection . Id . of ( account , with ) ;
final JingleRtpConnection rtpConnection = new JingleRtpConnection ( this , id , account . getJid ( ) ) ;
rtpConnection . setProposedMedia ( media ) ;
this . connections . put ( id , rtpConnection ) ;
rtpConnection . sendSessionInitiate ( ) ;
return id . sessionId ;
}
2020-04-15 08:49:38 +00:00
public void proposeJingleRtpSession ( final Account account , final Jid with , final Set < Media > media ) {
2020-04-08 07:42:06 +00:00
synchronized ( this . rtpSessionProposals ) {
for ( Map . Entry < RtpSessionProposal , DeviceDiscoveryState > entry : this . rtpSessionProposals . entrySet ( ) ) {
RtpSessionProposal proposal = entry . getKey ( ) ;
if ( proposal . account = = account & & with . asBareJid ( ) . equals ( proposal . with ) ) {
final DeviceDiscoveryState preexistingState = entry . getValue ( ) ;
if ( preexistingState ! = null & & preexistingState ! = DeviceDiscoveryState . FAILED ) {
2020-04-27 15:51:38 +00:00
final RtpEndUserState endUserState = preexistingState . toEndUserState ( ) ;
2020-05-21 13:39:59 +00:00
toneManager . transition ( endUserState , media ) ;
2020-04-08 07:42:06 +00:00
mXmppConnectionService . notifyJingleRtpConnectionUpdate (
account ,
with ,
proposal . sessionId ,
2020-04-27 15:51:38 +00:00
endUserState
2020-04-08 07:42:06 +00:00
) ;
return ;
}
}
}
2020-04-19 08:12:33 +00:00
if ( isBusy ( ) ) {
2020-05-10 12:09:16 +00:00
if ( hasMatchingRtpSession ( account , with , media ) ) {
Log . d ( Config . LOGTAG , " ignoring request to propose jingle session because the other party already created one for us " ) ;
return ;
}
2020-04-19 08:12:33 +00:00
throw new IllegalStateException ( " There is already a running RTP session. This should have been caught by the UI " ) ;
}
2020-04-15 08:49:38 +00:00
final RtpSessionProposal proposal = RtpSessionProposal . of ( account , with . asBareJid ( ) , media ) ;
2020-04-07 09:36:28 +00:00
this . rtpSessionProposals . put ( proposal , DeviceDiscoveryState . SEARCHING ) ;
2020-04-08 07:42:06 +00:00
mXmppConnectionService . notifyJingleRtpConnectionUpdate (
account ,
proposal . with ,
proposal . sessionId ,
RtpEndUserState . FINDING_DEVICE
) ;
2020-04-04 09:31:53 +00:00
final MessagePacket messagePacket = mXmppConnectionService . getMessageGenerator ( ) . sessionProposal ( proposal ) ;
2020-04-08 07:42:06 +00:00
Log . d ( Config . LOGTAG , messagePacket . toString ( ) ) ;
2020-04-04 09:31:53 +00:00
mXmppConnectionService . sendMessagePacket ( account , messagePacket ) ;
}
}
2019-12-05 13:03:54 +00:00
public void deliverIbbPacket ( Account account , IqPacket packet ) {
2020-04-01 13:17:38 +00:00
final String sid ;
final Element payload ;
2020-04-01 08:45:03 +00:00
if ( packet . hasChild ( " open " , Namespace . IBB ) ) {
payload = packet . findChild ( " open " , Namespace . IBB ) ;
2019-12-05 13:03:54 +00:00
sid = payload . getAttribute ( " sid " ) ;
2020-04-01 08:45:03 +00:00
} else if ( packet . hasChild ( " data " , Namespace . IBB ) ) {
payload = packet . findChild ( " data " , Namespace . IBB ) ;
2019-12-05 13:03:54 +00:00
sid = payload . getAttribute ( " sid " ) ;
2020-04-01 08:45:03 +00:00
} else if ( packet . hasChild ( " close " , Namespace . IBB ) ) {
payload = packet . findChild ( " close " , Namespace . IBB ) ;
2019-12-05 13:03:54 +00:00
sid = payload . getAttribute ( " sid " ) ;
2020-04-01 13:17:38 +00:00
} else {
payload = null ;
sid = null ;
2019-12-05 13:03:54 +00:00
}
if ( sid ! = null ) {
2020-04-01 08:45:03 +00:00
for ( final AbstractJingleConnection connection : this . connections . values ( ) ) {
if ( connection instanceof JingleFileTransferConnection ) {
final JingleFileTransferConnection fileTransfer = ( JingleFileTransferConnection ) connection ;
final JingleTransport transport = fileTransfer . getTransport ( ) ;
2019-12-05 13:03:54 +00:00
if ( transport instanceof JingleInBandTransport ) {
2020-04-01 13:17:38 +00:00
final JingleInBandTransport inBandTransport = ( JingleInBandTransport ) transport ;
if ( inBandTransport . matches ( account , sid ) ) {
inBandTransport . deliverPayload ( packet , payload ) ;
}
2019-12-05 13:03:54 +00:00
return ;
}
}
}
}
Log . d ( Config . LOGTAG , " unable to deliver ibb packet: " + packet . toString ( ) ) ;
account . getXmppConnection ( ) . sendIqPacket ( packet . generateResponse ( IqPacket . TYPE . ERROR ) , null ) ;
}
2020-04-13 16:30:12 +00:00
public void notifyRebound ( ) {
for ( final AbstractJingleConnection connection : this . connections . values ( ) ) {
connection . notifyRebound ( ) ;
2019-12-05 13:03:54 +00:00
}
}
2020-04-04 09:31:53 +00:00
2020-04-07 11:15:24 +00:00
public WeakReference < JingleRtpConnection > findJingleRtpConnection ( Account account , Jid with , String sessionId ) {
2020-05-17 08:24:46 +00:00
final AbstractJingleConnection . Id id = AbstractJingleConnection . Id . of ( account , with , sessionId ) ;
2020-04-07 11:15:24 +00:00
final AbstractJingleConnection connection = connections . get ( id ) ;
if ( connection instanceof JingleRtpConnection ) {
return new WeakReference < > ( ( JingleRtpConnection ) connection ) ;
}
return null ;
}
2020-04-07 09:36:28 +00:00
public void updateProposedSessionDiscovered ( Account account , Jid from , String sessionId , final DeviceDiscoveryState target ) {
synchronized ( this . rtpSessionProposals ) {
2020-04-15 10:07:19 +00:00
final RtpSessionProposal sessionProposal = getRtpSessionProposal ( account , from . asBareJid ( ) , sessionId ) ;
final DeviceDiscoveryState currentState = sessionProposal = = null ? null : rtpSessionProposals . get ( sessionProposal ) ;
2020-04-07 09:36:28 +00:00
if ( currentState = = null ) {
2020-04-08 07:42:06 +00:00
Log . d ( Config . LOGTAG , " unable to find session proposal for session id " + sessionId ) ;
2020-04-07 09:36:28 +00:00
return ;
}
if ( currentState = = DeviceDiscoveryState . DISCOVERED ) {
2020-04-08 07:42:06 +00:00
Log . d ( Config . LOGTAG , " session proposal already at discovered. not going to fall back " ) ;
2020-04-07 09:36:28 +00:00
return ;
}
this . rtpSessionProposals . put ( sessionProposal , target ) ;
2020-04-27 15:51:38 +00:00
final RtpEndUserState endUserState = target . toEndUserState ( ) ;
2020-05-21 13:39:59 +00:00
toneManager . transition ( endUserState , sessionProposal . media ) ;
2020-04-27 15:51:38 +00:00
mXmppConnectionService . notifyJingleRtpConnectionUpdate ( account , sessionProposal . with , sessionProposal . sessionId , endUserState ) ;
2020-04-08 07:42:06 +00:00
Log . d ( Config . LOGTAG , account . getJid ( ) . asBareJid ( ) + " : flagging session " + sessionId + " as " + target ) ;
2020-04-07 09:36:28 +00:00
}
}
2020-04-07 19:26:51 +00:00
public void rejectRtpSession ( final String sessionId ) {
2020-04-08 07:42:06 +00:00
for ( final AbstractJingleConnection connection : this . connections . values ( ) ) {
2020-04-07 19:26:51 +00:00
if ( connection . getId ( ) . sessionId . equals ( sessionId ) ) {
if ( connection instanceof JingleRtpConnection ) {
( ( JingleRtpConnection ) connection ) . rejectCall ( ) ;
}
}
}
}
2020-04-10 13:19:56 +00:00
public void endRtpSession ( final String sessionId ) {
for ( final AbstractJingleConnection connection : this . connections . values ( ) ) {
if ( connection . getId ( ) . sessionId . equals ( sessionId ) ) {
if ( connection instanceof JingleRtpConnection ) {
( ( JingleRtpConnection ) connection ) . endCall ( ) ;
}
}
}
}
2020-04-10 05:45:23 +00:00
public void failProceed ( Account account , final Jid with , String sessionId ) {
final AbstractJingleConnection . Id id = AbstractJingleConnection . Id . of ( account , with , sessionId ) ;
final AbstractJingleConnection existingJingleConnection = connections . get ( id ) ;
if ( existingJingleConnection instanceof JingleRtpConnection ) {
( ( JingleRtpConnection ) existingJingleConnection ) . deliverFailedProceed ( ) ;
}
}
2020-04-18 19:27:50 +00:00
void ensureConnectionIsRegistered ( final AbstractJingleConnection connection ) {
if ( connections . containsValue ( connection ) ) {
return ;
}
2020-04-21 10:00:13 +00:00
final IllegalStateException e = new IllegalStateException ( " JingleConnection has not been registered with connection manager " ) ;
Log . e ( Config . LOGTAG , " ensureConnectionIsRegistered() failed. Going to throw " , e ) ;
throw e ;
2020-04-18 19:27:50 +00:00
}
2020-06-11 19:17:13 +00:00
void setTerminalSessionState ( AbstractJingleConnection . Id id , final RtpEndUserState state , final Set < Media > media ) {
this . terminatedSessions . put ( PersistableSessionId . of ( id ) , new TerminatedRtpSession ( state , media ) ) ;
}
public TerminatedRtpSession getTerminalSessionState ( final Jid with , final String sessionId ) {
return this . terminatedSessions . getIfPresent ( new PersistableSessionId ( with , sessionId ) ) ;
2020-04-19 12:23:52 +00:00
}
private static class PersistableSessionId {
private final Jid with ;
private final String sessionId ;
private PersistableSessionId ( Jid with , String sessionId ) {
this . with = with ;
this . sessionId = sessionId ;
}
public static PersistableSessionId of ( AbstractJingleConnection . Id id ) {
return new PersistableSessionId ( id . with , id . sessionId ) ;
}
@Override
public boolean equals ( Object o ) {
if ( this = = o ) return true ;
if ( o = = null | | getClass ( ) ! = o . getClass ( ) ) return false ;
PersistableSessionId that = ( PersistableSessionId ) o ;
return Objects . equal ( with , that . with ) & &
Objects . equal ( sessionId , that . sessionId ) ;
}
@Override
public int hashCode ( ) {
return Objects . hashCode ( with , sessionId ) ;
}
}
2020-06-11 19:17:13 +00:00
public static class TerminatedRtpSession {
public final RtpEndUserState state ;
public final Set < Media > media ;
TerminatedRtpSession ( RtpEndUserState state , Set < Media > media ) {
this . state = state ;
this . media = media ;
}
}
2020-04-10 14:02:01 +00:00
public enum DeviceDiscoveryState {
SEARCHING , DISCOVERED , FAILED ;
public RtpEndUserState toEndUserState ( ) {
switch ( this ) {
case SEARCHING :
return RtpEndUserState . FINDING_DEVICE ;
case DISCOVERED :
return RtpEndUserState . RINGING ;
default :
return RtpEndUserState . CONNECTIVITY_ERROR ;
}
}
}
2020-05-02 07:50:17 +00:00
public static class RtpSessionProposal implements OngoingRtpSession {
2020-04-04 09:31:53 +00:00
public final Jid with ;
public final String sessionId ;
2020-04-15 08:49:38 +00:00
public final Set < Media > media ;
2020-04-16 08:51:34 +00:00
private final Account account ;
2020-04-04 09:31:53 +00:00
private RtpSessionProposal ( Account account , Jid with , String sessionId ) {
2020-04-15 10:07:19 +00:00
this ( account , with , sessionId , Collections . emptySet ( ) ) ;
2020-04-15 08:49:38 +00:00
}
private RtpSessionProposal ( Account account , Jid with , String sessionId , Set < Media > media ) {
2020-04-04 09:31:53 +00:00
this . account = account ;
this . with = with ;
this . sessionId = sessionId ;
2020-04-15 08:49:38 +00:00
this . media = media ;
2020-04-04 09:31:53 +00:00
}
2020-04-15 08:49:38 +00:00
public static RtpSessionProposal of ( Account account , Jid with , Set < Media > media ) {
return new RtpSessionProposal ( account , with , nextRandomId ( ) , media ) ;
2020-04-04 09:31:53 +00:00
}
@Override
public boolean equals ( Object o ) {
if ( this = = o ) return true ;
if ( o = = null | | getClass ( ) ! = o . getClass ( ) ) return false ;
RtpSessionProposal proposal = ( RtpSessionProposal ) o ;
return Objects . equal ( account . getJid ( ) , proposal . account . getJid ( ) ) & &
Objects . equal ( with , proposal . with ) & &
Objects . equal ( sessionId , proposal . sessionId ) ;
}
@Override
public int hashCode ( ) {
return Objects . hashCode ( account . getJid ( ) , with , sessionId ) ;
}
2020-05-02 07:50:17 +00:00
@Override
public Account getAccount ( ) {
return account ;
}
@Override
public Jid getWith ( ) {
return with ;
}
@Override
public String getSessionId ( ) {
return sessionId ;
}
2020-04-04 09:31:53 +00:00
}
2014-04-08 21:15:55 +00:00
}